mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-10-31 03:12: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) | ||||
|   - [Libraries](#libraries) | ||||
|   - [Image Attribution](#image-attribution) | ||||
|   - [Developers](#developers) | ||||
|   - [Team](#team) | ||||
|   - [Special Thanks](#special-thanks) | ||||
| - [Sponsorship + Funding](#sponsorship--funding) | ||||
|   - [OpenCollective](#opencollective) | ||||
|  | @ -210,6 +210,7 @@ For bugs and feature requests, please check to see if there's [already an issue] | |||
| 
 | ||||
| The following libraries and frameworks are used by GoToSocial, with gratitude 💕 | ||||
| 
 | ||||
| - [abema/go-mp4](https://github.com/abema/go-mp4); mp4 parsing. [MIT License](https://spdx.org/licenses/MIT.html). | ||||
| - [buckket/go-blurhash](https://github.com/buckket/go-blurhash); used for generating image blurhashes. [GPL-3.0 License](https://spdx.org/licenses/GPL-3.0-only.html). | ||||
| - [coreos/go-oidc](https://github.com/coreos/go-oidc); OIDC client library. [Apache-2.0 License](https://spdx.org/licenses/Apache-2.0.html). | ||||
| - [disintegration/imaging](https://github.com/disintegration/imaging); image resizing. [MIT License](https://spdx.org/licenses/MIT.html). | ||||
|  |  | |||
							
								
								
									
										1
									
								
								go.mod
									
										
									
									
									
								
							
							
						
						
									
										1
									
								
								go.mod
									
										
									
									
									
								
							|  | @ -13,6 +13,7 @@ require ( | |||
| 	codeberg.org/gruf/go-mutexes v1.1.4 | ||||
| 	codeberg.org/gruf/go-runners v1.3.1 | ||||
| 	codeberg.org/gruf/go-store/v2 v2.0.10 | ||||
| 	github.com/abema/go-mp4 v0.8.0 | ||||
| 	github.com/buckket/go-blurhash v1.1.0 | ||||
| 	github.com/coreos/go-oidc/v3 v3.4.0 | ||||
| 	github.com/cornelk/hashmap v1.0.8 | ||||
|  |  | |||
							
								
								
									
										8
									
								
								go.sum
									
										
									
									
									
								
							
							
						
						
									
										8
									
								
								go.sum
									
										
									
									
									
								
							|  | @ -110,6 +110,8 @@ github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030I | |||
| github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= | ||||
| github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= | ||||
| github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= | ||||
| github.com/abema/go-mp4 v0.8.0 h1:JHYkOvTfBpTnqJHiFFOXe8d6wiFy5MtDnA10fgccNqY= | ||||
| github.com/abema/go-mp4 v0.8.0/go.mod h1:vPl9t5ZK7K0x68jh12/+ECWBCXoWuIDtNgPtU2f04ws= | ||||
| github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= | ||||
| github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= | ||||
| github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= | ||||
|  | @ -491,6 +493,8 @@ github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108 | |||
| github.com/onsi/ginkgo v1.13.0/go.mod h1:+REjRxOmWfHCjfv9TTWB1jD1Frx4XydAD3zm1lskyM0= | ||||
| github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= | ||||
| github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= | ||||
| github.com/orcaman/writerseeker v0.0.0-20200621085525-1d3f536ff85e h1:s2RNOM/IGdY0Y6qfTeUKhDawdHDpK9RGBdx80qN4Ttw= | ||||
| github.com/orcaman/writerseeker v0.0.0-20200621085525-1d3f536ff85e/go.mod h1:nBdnFKj15wFbf94Rwfq4m30eAcyY9V/IyKAGQFtqkW0= | ||||
| github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= | ||||
| github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= | ||||
| github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= | ||||
|  | @ -568,6 +572,7 @@ github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKs | |||
| github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= | ||||
| github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs= | ||||
| github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= | ||||
| github.com/sunfish-shogi/bufseekio v0.0.0-20210207115823-a4185644b365/go.mod h1:dEzdXgvImkQ3WLI+0KQpmEx8T/C/ma9KeS3AfmU899I= | ||||
| github.com/superseriousbusiness/activity v1.2.1-gts h1:wh7v0zYa1mJmqB35PSfvgl4cs51Dh5PyfKvcZLSxMQU= | ||||
| github.com/superseriousbusiness/activity v1.2.1-gts/go.mod h1:AZw0Xb4Oju8rmaJCZ21gc5CPg47MmNgyac+Hx5jo8VM= | ||||
| github.com/superseriousbusiness/exif-terminator v0.5.0 h1:57SO/geyaOl2v/lJSQLVcQbdghpyFuK8ZTtaHL81fUQ= | ||||
|  | @ -1177,11 +1182,14 @@ gopkg.in/mcuadros/go-syslog.v2 v2.3.0 h1:kcsiS+WsTKyIEPABJBJtoG0KkOS6yzvJ+/eZlhD | |||
| gopkg.in/mcuadros/go-syslog.v2 v2.3.0/go.mod h1:l5LPIyOOyIdQquNg+oU6Z3524YwrcqEm0aKH+5zpt2U= | ||||
| gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= | ||||
| gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= | ||||
| gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg= | ||||
| gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98= | ||||
| gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= | ||||
| gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= | ||||
| gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= | ||||
| gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= | ||||
| gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= | ||||
| gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= | ||||
| gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= | ||||
| gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= | ||||
| gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= | ||||
|  |  | |||
|  | @ -65,7 +65,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch1() { | |||
| 	b, err := io.ReadAll(result.Body) | ||||
| 	suite.NoError(err) | ||||
| 
 | ||||
| 	suite.Equal(`{"uri":"http://localhost:8080","account_domain":"localhost:8080","title":"Example Instance","description":"\u003cp\u003eThis is the GoToSocial testrig. It doesn't federate or anything.\u003c/p\u003e\u003cp\u003eWhen the testrig is shut down, all data on it will be deleted.\u003c/p\u003e\u003cp\u003eDon't use this in production!\u003c/p\u003e","short_description":"\u003cp\u003eThis is the GoToSocial testrig. It doesn't federate or anything.\u003c/p\u003e\u003cp\u003eWhen the testrig is shut down, all data on it will be deleted.\u003c/p\u003e\u003cp\u003eDon't use this in production!\u003c/p\u003e","email":"someone@example.org","version":"0.0.0-testrig","registrations":true,"approval_required":true,"invites_enabled":false,"configuration":{"statuses":{"max_characters":5000,"max_media_attachments":6,"characters_reserved_per_url":25},"media_attachments":{"supported_mime_types":["image/jpeg","image/gif","image/png","image/webp"],"image_size_limit":10485760,"image_matrix_limit":16777216,"video_size_limit":41943040,"video_frame_rate_limit":60,"video_matrix_limit":16777216},"polls":{"max_options":6,"max_characters_per_option":50,"min_expiration":300,"max_expiration":2629746},"accounts":{"allow_custom_css":true},"emojis":{"emoji_size_limit":51200}},"urls":{"streaming_api":"wss://localhost:8080"},"stats":{"domain_count":2,"status_count":16,"user_count":4},"thumbnail":"http://localhost:8080/assets/logo.png","contact_account":{"id":"01F8MH17FWEB39HZJ76B6VXSKF","username":"admin","acct":"admin","display_name":"","locked":false,"bot":false,"created_at":"2022-05-17T13:10:59.000Z","note":"","url":"http://localhost:8080/@admin","avatar":"","avatar_static":"","header":"http://localhost:8080/assets/default_header.png","header_static":"http://localhost:8080/assets/default_header.png","followers_count":1,"following_count":1,"statuses_count":4,"last_status_at":"2021-10-20T10:41:37.000Z","emojis":[],"fields":[],"enable_rss":true,"role":"admin"},"max_toot_chars":5000}`, string(b)) | ||||
| 	suite.Equal(`{"uri":"http://localhost:8080","account_domain":"localhost:8080","title":"Example Instance","description":"\u003cp\u003eThis is the GoToSocial testrig. It doesn't federate or anything.\u003c/p\u003e\u003cp\u003eWhen the testrig is shut down, all data on it will be deleted.\u003c/p\u003e\u003cp\u003eDon't use this in production!\u003c/p\u003e","short_description":"\u003cp\u003eThis is the GoToSocial testrig. It doesn't federate or anything.\u003c/p\u003e\u003cp\u003eWhen the testrig is shut down, all data on it will be deleted.\u003c/p\u003e\u003cp\u003eDon't use this in production!\u003c/p\u003e","email":"someone@example.org","version":"0.0.0-testrig","registrations":true,"approval_required":true,"invites_enabled":false,"configuration":{"statuses":{"max_characters":5000,"max_media_attachments":6,"characters_reserved_per_url":25},"media_attachments":{"supported_mime_types":["image/jpeg","image/gif","image/png","image/webp","video/mp4"],"image_size_limit":10485760,"image_matrix_limit":16777216,"video_size_limit":41943040,"video_frame_rate_limit":60,"video_matrix_limit":16777216},"polls":{"max_options":6,"max_characters_per_option":50,"min_expiration":300,"max_expiration":2629746},"accounts":{"allow_custom_css":true},"emojis":{"emoji_size_limit":51200}},"urls":{"streaming_api":"wss://localhost:8080"},"stats":{"domain_count":2,"status_count":16,"user_count":4},"thumbnail":"http://localhost:8080/assets/logo.png","contact_account":{"id":"01F8MH17FWEB39HZJ76B6VXSKF","username":"admin","acct":"admin","display_name":"","locked":false,"bot":false,"created_at":"2022-05-17T13:10:59.000Z","note":"","url":"http://localhost:8080/@admin","avatar":"","avatar_static":"","header":"http://localhost:8080/assets/default_header.png","header_static":"http://localhost:8080/assets/default_header.png","followers_count":1,"following_count":1,"statuses_count":4,"last_status_at":"2021-10-20T10:41:37.000Z","emojis":[],"fields":[],"enable_rss":true,"role":"admin"},"max_toot_chars":5000}`, string(b)) | ||||
| } | ||||
| 
 | ||||
| func (suite *InstancePatchTestSuite) TestInstancePatch2() { | ||||
|  | @ -95,7 +95,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch2() { | |||
| 	b, err := io.ReadAll(result.Body) | ||||
| 	suite.NoError(err) | ||||
| 
 | ||||
| 	suite.Equal(`{"uri":"http://localhost:8080","account_domain":"localhost:8080","title":"Geoff's Instance","description":"\u003cp\u003eThis is the GoToSocial testrig. It doesn't federate or anything.\u003c/p\u003e\u003cp\u003eWhen the testrig is shut down, all data on it will be deleted.\u003c/p\u003e\u003cp\u003eDon't use this in production!\u003c/p\u003e","short_description":"\u003cp\u003eThis is the GoToSocial testrig. It doesn't federate or anything.\u003c/p\u003e\u003cp\u003eWhen the testrig is shut down, all data on it will be deleted.\u003c/p\u003e\u003cp\u003eDon't use this in production!\u003c/p\u003e","email":"admin@example.org","version":"0.0.0-testrig","registrations":true,"approval_required":true,"invites_enabled":false,"configuration":{"statuses":{"max_characters":5000,"max_media_attachments":6,"characters_reserved_per_url":25},"media_attachments":{"supported_mime_types":["image/jpeg","image/gif","image/png","image/webp"],"image_size_limit":10485760,"image_matrix_limit":16777216,"video_size_limit":41943040,"video_frame_rate_limit":60,"video_matrix_limit":16777216},"polls":{"max_options":6,"max_characters_per_option":50,"min_expiration":300,"max_expiration":2629746},"accounts":{"allow_custom_css":true},"emojis":{"emoji_size_limit":51200}},"urls":{"streaming_api":"wss://localhost:8080"},"stats":{"domain_count":2,"status_count":16,"user_count":4},"thumbnail":"http://localhost:8080/assets/logo.png","contact_account":{"id":"01F8MH17FWEB39HZJ76B6VXSKF","username":"admin","acct":"admin","display_name":"","locked":false,"bot":false,"created_at":"2022-05-17T13:10:59.000Z","note":"","url":"http://localhost:8080/@admin","avatar":"","avatar_static":"","header":"http://localhost:8080/assets/default_header.png","header_static":"http://localhost:8080/assets/default_header.png","followers_count":1,"following_count":1,"statuses_count":4,"last_status_at":"2021-10-20T10:41:37.000Z","emojis":[],"fields":[],"enable_rss":true,"role":"admin"},"max_toot_chars":5000}`, string(b)) | ||||
| 	suite.Equal(`{"uri":"http://localhost:8080","account_domain":"localhost:8080","title":"Geoff's Instance","description":"\u003cp\u003eThis is the GoToSocial testrig. It doesn't federate or anything.\u003c/p\u003e\u003cp\u003eWhen the testrig is shut down, all data on it will be deleted.\u003c/p\u003e\u003cp\u003eDon't use this in production!\u003c/p\u003e","short_description":"\u003cp\u003eThis is the GoToSocial testrig. It doesn't federate or anything.\u003c/p\u003e\u003cp\u003eWhen the testrig is shut down, all data on it will be deleted.\u003c/p\u003e\u003cp\u003eDon't use this in production!\u003c/p\u003e","email":"admin@example.org","version":"0.0.0-testrig","registrations":true,"approval_required":true,"invites_enabled":false,"configuration":{"statuses":{"max_characters":5000,"max_media_attachments":6,"characters_reserved_per_url":25},"media_attachments":{"supported_mime_types":["image/jpeg","image/gif","image/png","image/webp","video/mp4"],"image_size_limit":10485760,"image_matrix_limit":16777216,"video_size_limit":41943040,"video_frame_rate_limit":60,"video_matrix_limit":16777216},"polls":{"max_options":6,"max_characters_per_option":50,"min_expiration":300,"max_expiration":2629746},"accounts":{"allow_custom_css":true},"emojis":{"emoji_size_limit":51200}},"urls":{"streaming_api":"wss://localhost:8080"},"stats":{"domain_count":2,"status_count":16,"user_count":4},"thumbnail":"http://localhost:8080/assets/logo.png","contact_account":{"id":"01F8MH17FWEB39HZJ76B6VXSKF","username":"admin","acct":"admin","display_name":"","locked":false,"bot":false,"created_at":"2022-05-17T13:10:59.000Z","note":"","url":"http://localhost:8080/@admin","avatar":"","avatar_static":"","header":"http://localhost:8080/assets/default_header.png","header_static":"http://localhost:8080/assets/default_header.png","followers_count":1,"following_count":1,"statuses_count":4,"last_status_at":"2021-10-20T10:41:37.000Z","emojis":[],"fields":[],"enable_rss":true,"role":"admin"},"max_toot_chars":5000}`, string(b)) | ||||
| } | ||||
| 
 | ||||
| func (suite *InstancePatchTestSuite) TestInstancePatch3() { | ||||
|  | @ -125,7 +125,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch3() { | |||
| 	b, err := io.ReadAll(result.Body) | ||||
| 	suite.NoError(err) | ||||
| 
 | ||||
| 	suite.Equal(`{"uri":"http://localhost:8080","account_domain":"localhost:8080","title":"GoToSocial Testrig Instance","description":"\u003cp\u003eThis is the GoToSocial testrig. It doesn't federate or anything.\u003c/p\u003e\u003cp\u003eWhen the testrig is shut down, all data on it will be deleted.\u003c/p\u003e\u003cp\u003eDon't use this in production!\u003c/p\u003e","short_description":"\u003cp\u003eThis is some html, which is \u003cem\u003eallowed\u003c/em\u003e in short descriptions.\u003c/p\u003e","email":"admin@example.org","version":"0.0.0-testrig","registrations":true,"approval_required":true,"invites_enabled":false,"configuration":{"statuses":{"max_characters":5000,"max_media_attachments":6,"characters_reserved_per_url":25},"media_attachments":{"supported_mime_types":["image/jpeg","image/gif","image/png","image/webp"],"image_size_limit":10485760,"image_matrix_limit":16777216,"video_size_limit":41943040,"video_frame_rate_limit":60,"video_matrix_limit":16777216},"polls":{"max_options":6,"max_characters_per_option":50,"min_expiration":300,"max_expiration":2629746},"accounts":{"allow_custom_css":true},"emojis":{"emoji_size_limit":51200}},"urls":{"streaming_api":"wss://localhost:8080"},"stats":{"domain_count":2,"status_count":16,"user_count":4},"thumbnail":"http://localhost:8080/assets/logo.png","contact_account":{"id":"01F8MH17FWEB39HZJ76B6VXSKF","username":"admin","acct":"admin","display_name":"","locked":false,"bot":false,"created_at":"2022-05-17T13:10:59.000Z","note":"","url":"http://localhost:8080/@admin","avatar":"","avatar_static":"","header":"http://localhost:8080/assets/default_header.png","header_static":"http://localhost:8080/assets/default_header.png","followers_count":1,"following_count":1,"statuses_count":4,"last_status_at":"2021-10-20T10:41:37.000Z","emojis":[],"fields":[],"enable_rss":true,"role":"admin"},"max_toot_chars":5000}`, string(b)) | ||||
| 	suite.Equal(`{"uri":"http://localhost:8080","account_domain":"localhost:8080","title":"GoToSocial Testrig Instance","description":"\u003cp\u003eThis is the GoToSocial testrig. It doesn't federate or anything.\u003c/p\u003e\u003cp\u003eWhen the testrig is shut down, all data on it will be deleted.\u003c/p\u003e\u003cp\u003eDon't use this in production!\u003c/p\u003e","short_description":"\u003cp\u003eThis is some html, which is \u003cem\u003eallowed\u003c/em\u003e in short descriptions.\u003c/p\u003e","email":"admin@example.org","version":"0.0.0-testrig","registrations":true,"approval_required":true,"invites_enabled":false,"configuration":{"statuses":{"max_characters":5000,"max_media_attachments":6,"characters_reserved_per_url":25},"media_attachments":{"supported_mime_types":["image/jpeg","image/gif","image/png","image/webp","video/mp4"],"image_size_limit":10485760,"image_matrix_limit":16777216,"video_size_limit":41943040,"video_frame_rate_limit":60,"video_matrix_limit":16777216},"polls":{"max_options":6,"max_characters_per_option":50,"min_expiration":300,"max_expiration":2629746},"accounts":{"allow_custom_css":true},"emojis":{"emoji_size_limit":51200}},"urls":{"streaming_api":"wss://localhost:8080"},"stats":{"domain_count":2,"status_count":16,"user_count":4},"thumbnail":"http://localhost:8080/assets/logo.png","contact_account":{"id":"01F8MH17FWEB39HZJ76B6VXSKF","username":"admin","acct":"admin","display_name":"","locked":false,"bot":false,"created_at":"2022-05-17T13:10:59.000Z","note":"","url":"http://localhost:8080/@admin","avatar":"","avatar_static":"","header":"http://localhost:8080/assets/default_header.png","header_static":"http://localhost:8080/assets/default_header.png","followers_count":1,"following_count":1,"statuses_count":4,"last_status_at":"2021-10-20T10:41:37.000Z","emojis":[],"fields":[],"enable_rss":true,"role":"admin"},"max_toot_chars":5000}`, string(b)) | ||||
| } | ||||
| 
 | ||||
| func (suite *InstancePatchTestSuite) TestInstancePatch4() { | ||||
|  | @ -216,7 +216,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch6() { | |||
| 	b, err := io.ReadAll(result.Body) | ||||
| 	suite.NoError(err) | ||||
| 
 | ||||
| 	suite.Equal(`{"uri":"http://localhost:8080","account_domain":"localhost:8080","title":"GoToSocial Testrig Instance","description":"\u003cp\u003eThis is the GoToSocial testrig. It doesn't federate or anything.\u003c/p\u003e\u003cp\u003eWhen the testrig is shut down, all data on it will be deleted.\u003c/p\u003e\u003cp\u003eDon't use this in production!\u003c/p\u003e","short_description":"\u003cp\u003eThis is the GoToSocial testrig. It doesn't federate or anything.\u003c/p\u003e\u003cp\u003eWhen the testrig is shut down, all data on it will be deleted.\u003c/p\u003e\u003cp\u003eDon't use this in production!\u003c/p\u003e","email":"","version":"0.0.0-testrig","registrations":true,"approval_required":true,"invites_enabled":false,"configuration":{"statuses":{"max_characters":5000,"max_media_attachments":6,"characters_reserved_per_url":25},"media_attachments":{"supported_mime_types":["image/jpeg","image/gif","image/png","image/webp"],"image_size_limit":10485760,"image_matrix_limit":16777216,"video_size_limit":41943040,"video_frame_rate_limit":60,"video_matrix_limit":16777216},"polls":{"max_options":6,"max_characters_per_option":50,"min_expiration":300,"max_expiration":2629746},"accounts":{"allow_custom_css":true},"emojis":{"emoji_size_limit":51200}},"urls":{"streaming_api":"wss://localhost:8080"},"stats":{"domain_count":2,"status_count":16,"user_count":4},"thumbnail":"http://localhost:8080/assets/logo.png","contact_account":{"id":"01F8MH17FWEB39HZJ76B6VXSKF","username":"admin","acct":"admin","display_name":"","locked":false,"bot":false,"created_at":"2022-05-17T13:10:59.000Z","note":"","url":"http://localhost:8080/@admin","avatar":"","avatar_static":"","header":"http://localhost:8080/assets/default_header.png","header_static":"http://localhost:8080/assets/default_header.png","followers_count":1,"following_count":1,"statuses_count":4,"last_status_at":"2021-10-20T10:41:37.000Z","emojis":[],"fields":[],"enable_rss":true,"role":"admin"},"max_toot_chars":5000}`, string(b)) | ||||
| 	suite.Equal(`{"uri":"http://localhost:8080","account_domain":"localhost:8080","title":"GoToSocial Testrig Instance","description":"\u003cp\u003eThis is the GoToSocial testrig. It doesn't federate or anything.\u003c/p\u003e\u003cp\u003eWhen the testrig is shut down, all data on it will be deleted.\u003c/p\u003e\u003cp\u003eDon't use this in production!\u003c/p\u003e","short_description":"\u003cp\u003eThis is the GoToSocial testrig. It doesn't federate or anything.\u003c/p\u003e\u003cp\u003eWhen the testrig is shut down, all data on it will be deleted.\u003c/p\u003e\u003cp\u003eDon't use this in production!\u003c/p\u003e","email":"","version":"0.0.0-testrig","registrations":true,"approval_required":true,"invites_enabled":false,"configuration":{"statuses":{"max_characters":5000,"max_media_attachments":6,"characters_reserved_per_url":25},"media_attachments":{"supported_mime_types":["image/jpeg","image/gif","image/png","image/webp","video/mp4"],"image_size_limit":10485760,"image_matrix_limit":16777216,"video_size_limit":41943040,"video_frame_rate_limit":60,"video_matrix_limit":16777216},"polls":{"max_options":6,"max_characters_per_option":50,"min_expiration":300,"max_expiration":2629746},"accounts":{"allow_custom_css":true},"emojis":{"emoji_size_limit":51200}},"urls":{"streaming_api":"wss://localhost:8080"},"stats":{"domain_count":2,"status_count":16,"user_count":4},"thumbnail":"http://localhost:8080/assets/logo.png","contact_account":{"id":"01F8MH17FWEB39HZJ76B6VXSKF","username":"admin","acct":"admin","display_name":"","locked":false,"bot":false,"created_at":"2022-05-17T13:10:59.000Z","note":"","url":"http://localhost:8080/@admin","avatar":"","avatar_static":"","header":"http://localhost:8080/assets/default_header.png","header_static":"http://localhost:8080/assets/default_header.png","followers_count":1,"following_count":1,"statuses_count":4,"last_status_at":"2021-10-20T10:41:37.000Z","emojis":[],"fields":[],"enable_rss":true,"role":"admin"},"max_toot_chars":5000}`, string(b)) | ||||
| } | ||||
| 
 | ||||
| func (suite *InstancePatchTestSuite) TestInstancePatch7() { | ||||
|  | @ -279,7 +279,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch8() { | |||
| 	} | ||||
| 	suite.NotEmpty(instanceAccount.AvatarMediaAttachmentID) | ||||
| 
 | ||||
| 	expectedInstanceResponse := fmt.Sprintf(`{"uri":"http://localhost:8080","account_domain":"localhost:8080","title":"GoToSocial Testrig Instance","description":"\u003cp\u003eThis is the GoToSocial testrig. It doesn't federate or anything.\u003c/p\u003e\u003cp\u003eWhen the testrig is shut down, all data on it will be deleted.\u003c/p\u003e\u003cp\u003eDon't use this in production!\u003c/p\u003e","short_description":"\u003cp\u003eThis is the GoToSocial testrig. It doesn't federate or anything.\u003c/p\u003e\u003cp\u003eWhen the testrig is shut down, all data on it will be deleted.\u003c/p\u003e\u003cp\u003eDon't use this in production!\u003c/p\u003e","email":"admin@example.org","version":"0.0.0-testrig","registrations":true,"approval_required":true,"invites_enabled":false,"configuration":{"statuses":{"max_characters":5000,"max_media_attachments":6,"characters_reserved_per_url":25},"media_attachments":{"supported_mime_types":["image/jpeg","image/gif","image/png","image/webp"],"image_size_limit":10485760,"image_matrix_limit":16777216,"video_size_limit":41943040,"video_frame_rate_limit":60,"video_matrix_limit":16777216},"polls":{"max_options":6,"max_characters_per_option":50,"min_expiration":300,"max_expiration":2629746},"accounts":{"allow_custom_css":true},"emojis":{"emoji_size_limit":51200}},"urls":{"streaming_api":"wss://localhost:8080"},"stats":{"domain_count":2,"status_count":16,"user_count":4},"thumbnail":"http://localhost:8080/fileserver/%s/attachment/original/%s.gif","thumbnail_type":"image/gif","thumbnail_description":"A bouncing little green peglin.","contact_account":{"id":"01F8MH17FWEB39HZJ76B6VXSKF","username":"admin","acct":"admin","display_name":"","locked":false,"bot":false,"created_at":"2022-05-17T13:10:59.000Z","note":"","url":"http://localhost:8080/@admin","avatar":"","avatar_static":"","header":"http://localhost:8080/assets/default_header.png","header_static":"http://localhost:8080/assets/default_header.png","followers_count":1,"following_count":1,"statuses_count":4,"last_status_at":"2021-10-20T10:41:37.000Z","emojis":[],"fields":[],"enable_rss":true,"role":"admin"},"max_toot_chars":5000}`, instanceAccount.ID, instanceAccount.AvatarMediaAttachmentID) | ||||
| 	expectedInstanceResponse := fmt.Sprintf(`{"uri":"http://localhost:8080","account_domain":"localhost:8080","title":"GoToSocial Testrig Instance","description":"\u003cp\u003eThis is the GoToSocial testrig. It doesn't federate or anything.\u003c/p\u003e\u003cp\u003eWhen the testrig is shut down, all data on it will be deleted.\u003c/p\u003e\u003cp\u003eDon't use this in production!\u003c/p\u003e","short_description":"\u003cp\u003eThis is the GoToSocial testrig. It doesn't federate or anything.\u003c/p\u003e\u003cp\u003eWhen the testrig is shut down, all data on it will be deleted.\u003c/p\u003e\u003cp\u003eDon't use this in production!\u003c/p\u003e","email":"admin@example.org","version":"0.0.0-testrig","registrations":true,"approval_required":true,"invites_enabled":false,"configuration":{"statuses":{"max_characters":5000,"max_media_attachments":6,"characters_reserved_per_url":25},"media_attachments":{"supported_mime_types":["image/jpeg","image/gif","image/png","image/webp","video/mp4"],"image_size_limit":10485760,"image_matrix_limit":16777216,"video_size_limit":41943040,"video_frame_rate_limit":60,"video_matrix_limit":16777216},"polls":{"max_options":6,"max_characters_per_option":50,"min_expiration":300,"max_expiration":2629746},"accounts":{"allow_custom_css":true},"emojis":{"emoji_size_limit":51200}},"urls":{"streaming_api":"wss://localhost:8080"},"stats":{"domain_count":2,"status_count":16,"user_count":4},"thumbnail":"http://localhost:8080/fileserver/%s/attachment/original/%s.gif","thumbnail_type":"image/gif","thumbnail_description":"A bouncing little green peglin.","contact_account":{"id":"01F8MH17FWEB39HZJ76B6VXSKF","username":"admin","acct":"admin","display_name":"","locked":false,"bot":false,"created_at":"2022-05-17T13:10:59.000Z","note":"","url":"http://localhost:8080/@admin","avatar":"","avatar_static":"","header":"http://localhost:8080/assets/default_header.png","header_static":"http://localhost:8080/assets/default_header.png","followers_count":1,"following_count":1,"statuses_count":4,"last_status_at":"2021-10-20T10:41:37.000Z","emojis":[],"fields":[],"enable_rss":true,"role":"admin"},"max_toot_chars":5000}`, instanceAccount.ID, instanceAccount.AvatarMediaAttachmentID) | ||||
| 	suite.Equal(expectedInstanceResponse, string(b)) | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -38,16 +38,7 @@ const ( | |||
| 	thumbnailMaxHeight = 512 | ||||
| ) | ||||
| 
 | ||||
| type imageMeta struct { | ||||
| 	width    int | ||||
| 	height   int | ||||
| 	size     int | ||||
| 	aspect   float64 | ||||
| 	blurhash string // defined only for calls to deriveThumbnail if createBlurhash is true | ||||
| 	small    []byte // defined only for calls to deriveStaticEmoji or deriveThumbnail | ||||
| } | ||||
| 
 | ||||
| func decodeGif(r io.Reader) (*imageMeta, error) { | ||||
| func decodeGif(r io.Reader) (*mediaMeta, error) { | ||||
| 	gif, err := gif.DecodeAll(r) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
|  | @ -59,7 +50,7 @@ func decodeGif(r io.Reader) (*imageMeta, error) { | |||
| 	size := width * height | ||||
| 	aspect := float64(width) / float64(height) | ||||
| 
 | ||||
| 	return &imageMeta{ | ||||
| 	return &mediaMeta{ | ||||
| 		width:  width, | ||||
| 		height: height, | ||||
| 		size:   size, | ||||
|  | @ -67,7 +58,7 @@ func decodeGif(r io.Reader) (*imageMeta, error) { | |||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
| func decodeImage(r io.Reader, contentType string) (*imageMeta, error) { | ||||
| func decodeImage(r io.Reader, contentType string) (*mediaMeta, error) { | ||||
| 	var i image.Image | ||||
| 	var err error | ||||
| 
 | ||||
|  | @ -96,7 +87,7 @@ func decodeImage(r io.Reader, contentType string) (*imageMeta, error) { | |||
| 	size := width * height | ||||
| 	aspect := float64(width) / float64(height) | ||||
| 
 | ||||
| 	return &imageMeta{ | ||||
| 	return &mediaMeta{ | ||||
| 		width:  width, | ||||
| 		height: height, | ||||
| 		size:   size, | ||||
|  | @ -104,8 +95,37 @@ func decodeImage(r io.Reader, contentType string) (*imageMeta, error) { | |||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
| // deriveThumbnail returns a byte slice and metadata for a thumbnail | ||||
| // of a given jpeg, png, gif or webp, or an error if something goes wrong. | ||||
| // deriveStaticEmojji takes a given gif or png of an emoji, decodes it, and re-encodes it as a static png. | ||||
| func deriveStaticEmoji(r io.Reader, contentType string) (*mediaMeta, error) { | ||||
| 	var i image.Image | ||||
| 	var err error | ||||
| 
 | ||||
| 	switch contentType { | ||||
| 	case mimeImagePng: | ||||
| 		i, err = StrippedPngDecode(r) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 	case mimeImageGif: | ||||
| 		i, err = gif.Decode(r) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 	default: | ||||
| 		return nil, fmt.Errorf("content type %s not allowed for emoji", contentType) | ||||
| 	} | ||||
| 
 | ||||
| 	out := &bytes.Buffer{} | ||||
| 	if err := png.Encode(out, i); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return &mediaMeta{ | ||||
| 		small: out.Bytes(), | ||||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
| // deriveThumbnailFromImage returns a byte slice and metadata for a thumbnail | ||||
| // of a given piece of media, or an error if something goes wrong. | ||||
| // | ||||
| // If createBlurhash is true, then a blurhash will also be generated from a tiny | ||||
| // version of the image. This costs precious CPU cycles, so only use it if you | ||||
|  | @ -113,7 +133,7 @@ func decodeImage(r io.Reader, contentType string) (*imageMeta, error) { | |||
| // | ||||
| // If createBlurhash is false, then the blurhash field on the returned ImageAndMeta | ||||
| // will be an empty string. | ||||
| func deriveThumbnail(r io.Reader, contentType string, createBlurhash bool) (*imageMeta, error) { | ||||
| func deriveThumbnailFromImage(r io.Reader, contentType string, createBlurhash bool) (*mediaMeta, error) { | ||||
| 	var i image.Image | ||||
| 	var err error | ||||
| 
 | ||||
|  | @ -126,7 +146,7 @@ func deriveThumbnail(r io.Reader, contentType string, createBlurhash bool) (*ima | |||
| 		}) | ||||
| 		i, err = imaging.Decode(strippedPngReader, imaging.AutoOrientation(true)) | ||||
| 	default: | ||||
| 		err = fmt.Errorf("content type %s can't be thumbnailed", contentType) | ||||
| 		err = fmt.Errorf("content type %s can't be thumbnailed as an image", contentType) | ||||
| 	} | ||||
| 
 | ||||
| 	if err != nil { | ||||
|  | @ -149,7 +169,7 @@ func deriveThumbnail(r io.Reader, contentType string, createBlurhash bool) (*ima | |||
| 	size := thumbX * thumbY | ||||
| 	aspect := float64(thumbX) / float64(thumbY) | ||||
| 
 | ||||
| 	im := &imageMeta{ | ||||
| 	im := &mediaMeta{ | ||||
| 		width:  thumbX, | ||||
| 		height: thumbY, | ||||
| 		size:   size, | ||||
|  | @ -178,32 +198,3 @@ func deriveThumbnail(r io.Reader, contentType string, createBlurhash bool) (*ima | |||
| 
 | ||||
| 	return im, nil | ||||
| } | ||||
| 
 | ||||
| // deriveStaticEmojji takes a given gif or png of an emoji, decodes it, and re-encodes it as a static png. | ||||
| func deriveStaticEmoji(r io.Reader, contentType string) (*imageMeta, error) { | ||||
| 	var i image.Image | ||||
| 	var err error | ||||
| 
 | ||||
| 	switch contentType { | ||||
| 	case mimeImagePng: | ||||
| 		i, err = StrippedPngDecode(r) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 	case mimeImageGif: | ||||
| 		i, err = gif.Decode(r) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 	default: | ||||
| 		return nil, fmt.Errorf("content type %s not allowed for emoji", contentType) | ||||
| 	} | ||||
| 
 | ||||
| 	out := &bytes.Buffer{} | ||||
| 	if err := png.Encode(out, i); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return &imageMeta{ | ||||
| 		small: out.Bytes(), | ||||
| 	}, nil | ||||
| } | ||||
|  |  | |||
|  | @ -376,6 +376,78 @@ func (suite *ManagerTestSuite) TestSimpleJpegProcessBlocking() { | |||
| 	suite.Equal(processedThumbnailBytesExpected, processedThumbnailBytes) | ||||
| } | ||||
| 
 | ||||
| func (suite *ManagerTestSuite) TestSlothVineProcessBlocking() { | ||||
| 	ctx := context.Background() | ||||
| 
 | ||||
| 	data := func(_ context.Context) (io.ReadCloser, int64, error) { | ||||
| 		// load bytes from a test video | ||||
| 		b, err := os.ReadFile("./test/test-mp4-original.mp4") | ||||
| 		if err != nil { | ||||
| 			panic(err) | ||||
| 		} | ||||
| 		return io.NopCloser(bytes.NewBuffer(b)), int64(len(b)), nil | ||||
| 	} | ||||
| 
 | ||||
| 	accountID := "01FS1X72SK9ZPW0J1QQ68BD264" | ||||
| 
 | ||||
| 	// process the media with no additional info provided | ||||
| 	processingMedia, err := suite.manager.ProcessMedia(ctx, data, nil, accountID, nil) | ||||
| 	suite.NoError(err) | ||||
| 	// fetch the attachment id from the processing media | ||||
| 	attachmentID := processingMedia.AttachmentID() | ||||
| 
 | ||||
| 	// do a blocking call to fetch the attachment | ||||
| 	attachment, err := processingMedia.LoadAttachment(ctx) | ||||
| 	suite.NoError(err) | ||||
| 	suite.NotNil(attachment) | ||||
| 
 | ||||
| 	// make sure it's got the stuff set on it that we expect | ||||
| 	// the attachment ID and accountID we expect | ||||
| 	suite.Equal(attachmentID, attachment.ID) | ||||
| 	suite.Equal(accountID, attachment.AccountID) | ||||
| 
 | ||||
| 	// file meta should be correctly derived from the video | ||||
| 	suite.EqualValues(gtsmodel.Original{ | ||||
| 		Width: 338, Height: 240, Size: 81120, Aspect: 1.4083333333333334, | ||||
| 	}, attachment.FileMeta.Original) | ||||
| 	suite.EqualValues(gtsmodel.Small{ | ||||
| 		Width: 338, Height: 240, Size: 81120, Aspect: 1.4083333333333334, | ||||
| 	}, attachment.FileMeta.Small) | ||||
| 	suite.Equal("video/mp4", attachment.File.ContentType) | ||||
| 	suite.Equal("image/jpeg", attachment.Thumbnail.ContentType) | ||||
| 	suite.Equal(312413, attachment.File.FileSize) | ||||
| 	suite.Equal("", attachment.Blurhash) | ||||
| 
 | ||||
| 	// now make sure the attachment is in the database | ||||
| 	dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachmentID) | ||||
| 	suite.NoError(err) | ||||
| 	suite.NotNil(dbAttachment) | ||||
| 
 | ||||
| 	// make sure the processed file is in storage | ||||
| 	processedFullBytes, err := suite.storage.Get(ctx, attachment.File.Path) | ||||
| 	suite.NoError(err) | ||||
| 	suite.NotEmpty(processedFullBytes) | ||||
| 
 | ||||
| 	// load the processed bytes from our test folder, to compare | ||||
| 	processedFullBytesExpected, err := os.ReadFile("./test/test-mp4-processed.mp4") | ||||
| 	suite.NoError(err) | ||||
| 	suite.NotEmpty(processedFullBytesExpected) | ||||
| 
 | ||||
| 	// the bytes in storage should be what we expected | ||||
| 	suite.Equal(processedFullBytesExpected, processedFullBytes) | ||||
| 
 | ||||
| 	// now do the same for the thumbnail and make sure it's what we expected | ||||
| 	processedThumbnailBytes, err := suite.storage.Get(ctx, attachment.Thumbnail.Path) | ||||
| 	suite.NoError(err) | ||||
| 	suite.NotEmpty(processedThumbnailBytes) | ||||
| 	 | ||||
| 	processedThumbnailBytesExpected, err := os.ReadFile("./test/test-mp4-thumbnail.jpg") | ||||
| 	suite.NoError(err) | ||||
| 	suite.NotEmpty(processedThumbnailBytesExpected) | ||||
| 
 | ||||
| 	suite.Equal(processedThumbnailBytesExpected, processedThumbnailBytes) | ||||
| } | ||||
| 
 | ||||
| func (suite *ManagerTestSuite) TestSimpleJpegProcessBlockingNoContentLengthGiven() { | ||||
| 	ctx := context.Background() | ||||
| 
 | ||||
|  |  | |||
|  | @ -88,11 +88,11 @@ func (p *ProcessingMedia) LoadAttachment(ctx context.Context) (*gtsmodel.MediaAt | |||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	if err := p.loadThumb(ctx); err != nil { | ||||
| 	if err := p.loadFullSize(ctx); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	if err := p.loadFullSize(ctx); err != nil { | ||||
| 	if err := p.loadThumb(ctx); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
|  | @ -128,7 +128,6 @@ func (p *ProcessingMedia) loadThumb(ctx context.Context) error { | |||
| 	switch processState(thumbState) { | ||||
| 	case received: | ||||
| 		// we haven't processed a thumbnail for this media yet so do it now | ||||
| 
 | ||||
| 		// check if we need to create a blurhash or if there's already one set | ||||
| 		var createBlurhash bool | ||||
| 		if p.attachment.Blurhash == "" { | ||||
|  | @ -136,26 +135,45 @@ func (p *ProcessingMedia) loadThumb(ctx context.Context) error { | |||
| 			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) | ||||
| 			if err != nil { | ||||
| 				p.err = fmt.Errorf("loadThumb: error fetching file from storage: %s", err) | ||||
| 				atomic.StoreInt32(&p.thumbState, int32(errored)) | ||||
| 				return p.err | ||||
| 			} | ||||
| 		defer stored.Close() | ||||
| 
 | ||||
| 		// stream the file from storage straight into the derive thumbnail function | ||||
| 		thumb, err := deriveThumbnail(stored, p.attachment.File.ContentType, createBlurhash) | ||||
| 			thumb, err = deriveThumbnailFromImage(stored, ct, createBlurhash) | ||||
| 
 | ||||
| 			// try to close the stored stream we had open, no matter what | ||||
| 			if closeErr := stored.Close(); closeErr != nil { | ||||
| 				log.Errorf("error closing stream: %s", closeErr) | ||||
| 			} | ||||
| 
 | ||||
| 			// now check if we managed to get a thumbnail | ||||
| 			if err != nil { | ||||
| 				p.err = fmt.Errorf("loadThumb: error deriving thumbnail: %s", err) | ||||
| 				atomic.StoreInt32(&p.thumbState, int32(errored)) | ||||
| 				return p.err | ||||
| 			} | ||||
| 
 | ||||
| 		// Close stored media now we're done | ||||
| 		if err := stored.Close(); err != nil { | ||||
| 			log.Errorf("loadThumb: error closing stored full size: %s", err) | ||||
| 		case mimeVideoMp4: | ||||
| 			// create a generic thumbnail based on video height + width | ||||
| 			thumb, err = deriveThumbnailFromVideo(p.attachment.FileMeta.Original.Height, p.attachment.FileMeta.Original.Width) | ||||
| 			if err != nil { | ||||
| 				p.err = fmt.Errorf("loadThumb: error deriving thumbnail: %s", err) | ||||
| 				atomic.StoreInt32(&p.thumbState, int32(errored)) | ||||
| 				return p.err | ||||
| 			} | ||||
| 		default: | ||||
| 			p.err = fmt.Errorf("loadThumb: content type %s not a processible image type", ct) | ||||
| 			atomic.StoreInt32(&p.thumbState, int32(errored)) | ||||
| 			return p.err | ||||
| 		} | ||||
| 
 | ||||
| 		// put the thumbnail in storage | ||||
|  | @ -195,7 +213,7 @@ func (p *ProcessingMedia) loadFullSize(ctx context.Context) error { | |||
| 	switch processState(fullSizeState) { | ||||
| 	case received: | ||||
| 		var err error | ||||
| 		var decoded *imageMeta | ||||
| 		var decoded *mediaMeta | ||||
| 
 | ||||
| 		// stream the original file out of storage... | ||||
| 		stored, err := p.storage.GetStream(ctx, p.attachment.File.Path) | ||||
|  | @ -218,6 +236,8 @@ func (p *ProcessingMedia) loadFullSize(ctx context.Context) error { | |||
| 			decoded, err = decodeImage(stored, ct) | ||||
| 		case mimeImageGif: | ||||
| 			decoded, err = decodeGif(stored) | ||||
| 		case mimeVideoMp4: | ||||
| 			decoded, err = decodeVideo(stored, ct) | ||||
| 		default: | ||||
| 			err = fmt.Errorf("loadFullSize: content type %s not a processible image type", ct) | ||||
| 		} | ||||
|  | @ -295,7 +315,7 @@ func (p *ProcessingMedia) store(ctx context.Context) error { | |||
| 	} | ||||
| 
 | ||||
| 	// bail if this is a type we can't process | ||||
| 	if !supportedImage(contentType) { | ||||
| 	if !supportedAttachment(contentType) { | ||||
| 		return fmt.Errorf("store: media type %s not (yet) supported", contentType) | ||||
| 	} | ||||
| 
 | ||||
|  | @ -338,6 +358,10 @@ func (p *ProcessingMedia) store(ctx context.Context) error { | |||
| 			// can't terminate if we don't know the file size, so just store the multiReader | ||||
| 			readerToStore = multiReader | ||||
| 		} | ||||
| 	case mimeMp4: | ||||
| 		p.attachment.Type = gtsmodel.FileTypeVideo | ||||
| 		// nothing to terminate, we can just store the multireader | ||||
| 		readerToStore = multiReader | ||||
| 	default: | ||||
| 		return fmt.Errorf("store: couldn't process %s", extension) | ||||
| 	} | ||||
|  |  | |||
							
								
								
									
										
											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 | ||||
| const ( | ||||
| 	mimeImage = "image" | ||||
| 	mimeVideo = "video" | ||||
| 
 | ||||
| 	mimeJpeg      = "jpeg" | ||||
| 	mimeImageJpeg = mimeImage + "/" + mimeJpeg | ||||
|  | @ -46,6 +47,9 @@ const ( | |||
| 
 | ||||
| 	mimeWebp      = "webp" | ||||
| 	mimeImageWebp = mimeImage + "/" + mimeWebp | ||||
| 
 | ||||
| 	mimeMp4      = "mp4" | ||||
| 	mimeVideoMp4 = mimeVideo + "/" + mimeMp4 | ||||
| ) | ||||
| 
 | ||||
| type processState int32 | ||||
|  | @ -128,3 +132,12 @@ type DataFunc func(ctx context.Context) (reader io.ReadCloser, fileSize int64, e | |||
| // | ||||
| // This can be set to nil, and will then not be executed. | ||||
| type PostDataCallbackFunc func(ctx context.Context) error | ||||
| 
 | ||||
| type mediaMeta struct { | ||||
| 	width    int | ||||
| 	height   int | ||||
| 	size     int | ||||
| 	aspect   float64 | ||||
| 	blurhash string | ||||
| 	small    []byte | ||||
| } | ||||
|  |  | |||
|  | @ -37,6 +37,7 @@ func AllSupportedMIMETypes() []string { | |||
| 		mimeImageGif, | ||||
| 		mimeImagePng, | ||||
| 		mimeImageWebp, | ||||
| 		mimeVideoMp4, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
|  | @ -61,16 +62,10 @@ func parseContentType(fileHeader []byte) (string, error) { | |||
| 	return kind.MIME.Value, nil | ||||
| } | ||||
| 
 | ||||
| // supportedImage checks mime type of an image against a slice of accepted types, | ||||
| // and returns True if the mime type is accepted. | ||||
| func supportedImage(mimeType string) bool { | ||||
| 	acceptedImageTypes := []string{ | ||||
| 		mimeImageJpeg, | ||||
| 		mimeImageGif, | ||||
| 		mimeImagePng, | ||||
| 		mimeImageWebp, | ||||
| 	} | ||||
| 	for _, accepted := range acceptedImageTypes { | ||||
| // supportedAttachment checks mime type of an attachment against a | ||||
| // slice of accepted types, and returns True if the mime type is accepted. | ||||
| func supportedAttachment(mimeType string) bool { | ||||
| 	for _, accepted := range AllSupportedMIMETypes() { | ||||
| 		if mimeType == accepted { | ||||
| 			return true | ||||
| 		} | ||||
|  |  | |||
							
								
								
									
										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/storage | ||||
| codeberg.org/gruf/go-store/v2/util | ||||
| # github.com/abema/go-mp4 v0.8.0 | ||||
| ## explicit; go 1.14 | ||||
| github.com/abema/go-mp4 | ||||
| github.com/abema/go-mp4/bitio | ||||
| github.com/abema/go-mp4/util | ||||
| # github.com/aymerick/douceur v0.2.0 | ||||
| ## explicit | ||||
| github.com/aymerick/douceur/css | ||||
|  |  | |||
|  | @ -232,6 +232,9 @@ main { | |||
| 		} | ||||
| 
 | ||||
| 		input.sensitive-checkbox:checked { /* Media is shown */ | ||||
| 			& ~ .video-play { | ||||
| 				display: flex; | ||||
| 			} | ||||
| 			& ~ .sensitive { | ||||
| 				.closed { | ||||
| 					transition: 0.8s; | ||||
|  | @ -256,6 +259,32 @@ main { | |||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		.video-play { | ||||
| 			.icon-span { | ||||
| 				align-self: center; | ||||
| 				display: initial; | ||||
| 				z-index: 4; | ||||
| 
 | ||||
| 				.icon { | ||||
| 					color: $white1; | ||||
| 				} | ||||
| 
 | ||||
| 				.icon-bg { | ||||
| 					color: $gray1; | ||||
| 					font-size: 1.1em; | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 			display: none; | ||||
| 			position: absolute; | ||||
| 			height: 100%; | ||||
| 			width: 100%; | ||||
| 			justify-content: center; | ||||
| 			align-items: center; | ||||
| 			font-size: 7em; | ||||
| 			pointer-events: none; | ||||
| 		} | ||||
| 
 | ||||
| 		.sensitive { | ||||
| 			position: absolute; | ||||
| 			height: 100%; | ||||
|  |  | |||
|  | @ -21,6 +21,7 @@ | |||
| const Photoswipe = require("photoswipe/dist/umd/photoswipe.umd.min.js"); | ||||
| const PhotoswipeLightbox = require("photoswipe/dist/umd/photoswipe-lightbox.umd.min.js"); | ||||
| const PhotoswipeCaptionPlugin = require("photoswipe-dynamic-caption-plugin").default; | ||||
| const PhotoswipeVideoPlugin = require("photoswipe-video-plugin").default; | ||||
| 
 | ||||
| let [_, _user, type, id] = window.location.pathname.split("/"); | ||||
| if (type == "statuses") { | ||||
|  | @ -39,6 +40,7 @@ const lightbox = new PhotoswipeLightbox({ | |||
| new PhotoswipeCaptionPlugin(lightbox, { | ||||
| 	type: 'auto', | ||||
| }); | ||||
| new PhotoswipeVideoPlugin(lightbox, {}); | ||||
| 
 | ||||
| lightbox.init(); | ||||
| 
 | ||||
|  |  | |||
|  | @ -22,6 +22,7 @@ | |||
|     "modern-normalize": "^1.1.0", | ||||
|     "photoswipe": "^5.3.3", | ||||
|     "photoswipe-dynamic-caption-plugin": "^1.2.7", | ||||
|     "photoswipe-video-plugin": "^1.0.2", | ||||
|     "react": "^18.2.0", | ||||
|     "react-dom": "^18.2.0", | ||||
|     "react-error-boundary": "^3.1.4", | ||||
|  |  | |||
|  | @ -4201,6 +4201,11 @@ photoswipe-dynamic-caption-plugin@^1.2.7: | |||
|   resolved "https://registry.yarnpkg.com/photoswipe-dynamic-caption-plugin/-/photoswipe-dynamic-caption-plugin-1.2.7.tgz#53aa5059f1c4dccc8aa36196ff3e09baa5e537c2" | ||||
|   integrity sha512-5XXdXLf2381nwe7KqQvcyStiUBi9TitYXppUQTrzPwYAi4lZsmWNnNKMclM7I4QGlX6fXo42v3bgb6rlK9pY1Q== | ||||
| 
 | ||||
| photoswipe-video-plugin@^1.0.2: | ||||
|   version "1.0.2" | ||||
|   resolved "https://registry.yarnpkg.com/photoswipe-video-plugin/-/photoswipe-video-plugin-1.0.2.tgz#156b6a72ffa86e6c6e2b486e8ec5b48f6696941a" | ||||
|   integrity sha512-skNHaalLU7rptZ3zq4XfS5hPqSDD65ctvpf2X8buvC8BpOt6XKSIgRkLzTwgQOUm9yQ8kQ4mMget7CIqGcqtDg== | ||||
| 
 | ||||
| photoswipe@^5.3.3: | ||||
|   version "5.3.3" | ||||
|   resolved "https://registry.yarnpkg.com/photoswipe/-/photoswipe-5.3.3.tgz#86351a33502a3ab7d1e483127fe596b20054218a" | ||||
|  |  | |||
|  | @ -22,7 +22,7 @@ | |||
| 		{{range .}} | ||||
| 		<div class="media-wrapper"> | ||||
| 			{{if not .Description}} | ||||
| 			<div class="no-image-desc" aria-hidden="true" ><i class="fa fa-info-circle"></i><span>Missing image description</span></div> | ||||
| 			<div class="no-image-desc" aria-hidden="true" ><i class="fa fa-info-circle"></i><span>Missing media description</span></div> | ||||
| 			{{end}}	 | ||||
| 			<input type="checkbox" id="sensitiveMedia-{{.ID}}" class="sensitive-checkbox hidden" {{if not $.Sensitive}}checked{{end}}/> | ||||
| 			<div class="sensitive"> | ||||
|  | @ -35,7 +35,21 @@ | |||
| 					<label for="sensitiveMedia-{{.ID}}" class="button" role="button" tabindex="0">Show sensitive media</label> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 			<a href="{{.URL}}" target="_blank" {{if .Description}}title="{{.Description}}"{{end}} data-pswp-width="{{.Meta.Original.Width}}px" data-pswp-height="{{.Meta.Original.Height}}px" data-cropped="true"> | ||||
| 			{{ if eq .Type "video" }} | ||||
| 			<div class="video-play"> | ||||
| 				<span class="icon-span fa-stack" aria-hidden="true"> | ||||
| 					<i class="icon-bg fa fa-fw fa-circle fa-stack-1x"></i> | ||||
| 					<i class="icon fa fa-fw fa-play-circle fa-stack-1x"></i> | ||||
| 				</span> | ||||
| 			</div> | ||||
| 			{{ end }} | ||||
| 			<a href="{{.URL}}" | ||||
| 				 target="_blank" | ||||
| 				 {{if .Description}}title="{{.Description}}"{{end}} | ||||
| 				 data-pswp-width="{{.Meta.Original.Width}}px" | ||||
| 				 data-pswp-height="{{.Meta.Original.Height}}px" | ||||
| 				 {{if eq .Type "video"}}data-pswp-type="video"{{end}} | ||||
| 				 data-cropped="true"> | ||||
| 				<img src="{{.PreviewURL}}" {{if .Description}}alt="{{.Description}}"{{end}} data-blurhash="{{.Blurhash}}"/> | ||||
| 			</a> | ||||
| 		</div> | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue