| 
									
										
										
										
											2024-07-12 09:39:47 +00:00
										 |  |  | // GoToSocial | 
					
						
							|  |  |  | // Copyright (C) GoToSocial Authors admin@gotosocial.org | 
					
						
							|  |  |  | // SPDX-License-Identifier: AGPL-3.0-or-later | 
					
						
							|  |  |  | // | 
					
						
							|  |  |  | // 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 ( | 
					
						
							|  |  |  | 	"context" | 
					
						
							|  |  |  | 	"encoding/json" | 
					
						
							|  |  |  | 	"errors" | 
					
						
							| 
									
										
										
										
											2024-08-30 14:03:59 +02:00
										 |  |  | 	"os" | 
					
						
							| 
									
										
										
										
											2024-07-12 09:39:47 +00:00
										 |  |  | 	"path" | 
					
						
							|  |  |  | 	"strconv" | 
					
						
							|  |  |  | 	"strings" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	"codeberg.org/gruf/go-byteutil" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	_ffmpeg "github.com/superseriousbusiness/gotosocial/internal/media/ffmpeg" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | 
					
						
							|  |  |  | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | 
					
						
							|  |  |  | 	"github.com/tetratelabs/wazero" | 
					
						
							|  |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-02 11:46:41 +00:00
										 |  |  | // ffmpegClearMetadata generates a copy of input media with all metadata cleared. | 
					
						
							|  |  |  | // NOTE: given that we are not performing an encode, this only clears global level metadata, | 
					
						
							|  |  |  | // any metadata encoded into the media stream itself will not be cleared. This is the best we | 
					
						
							|  |  |  | // can do without absolutely tanking performance by requiring transcodes :( | 
					
						
							|  |  |  | func ffmpegClearMetadata(ctx context.Context, outpath, inpath string) error { | 
					
						
							| 
									
										
										
										
											2024-08-30 14:03:59 +02:00
										 |  |  | 	return ffmpeg(ctx, inpath, outpath, | 
					
						
							| 
									
										
										
										
											2024-07-28 19:10:41 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 		// Only log errors. | 
					
						
							| 
									
										
										
										
											2024-07-12 09:39:47 +00:00
										 |  |  | 		"-loglevel", "error", | 
					
						
							| 
									
										
										
										
											2024-07-19 15:28:43 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-26 15:30:24 +00:00
										 |  |  | 		// Input file path. | 
					
						
							| 
									
										
										
										
											2024-08-02 11:46:41 +00:00
										 |  |  | 		"-i", inpath, | 
					
						
							| 
									
										
										
										
											2024-07-19 15:28:43 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 		// Drop all metadata. | 
					
						
							| 
									
										
										
										
											2024-07-12 09:39:47 +00:00
										 |  |  | 		"-map_metadata", "-1", | 
					
						
							| 
									
										
										
										
											2024-07-19 15:28:43 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 		// Copy input codecs, | 
					
						
							|  |  |  | 		// i.e. no transcode. | 
					
						
							| 
									
										
										
										
											2024-07-12 09:39:47 +00:00
										 |  |  | 		"-codec", "copy", | 
					
						
							| 
									
										
										
										
											2024-07-19 15:28:43 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 		// Overwrite. | 
					
						
							| 
									
										
										
										
											2024-07-12 09:39:47 +00:00
										 |  |  | 		"-y", | 
					
						
							| 
									
										
										
										
											2024-07-19 15:28:43 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 		// Output. | 
					
						
							| 
									
										
										
										
											2024-07-12 09:39:47 +00:00
										 |  |  | 		outpath, | 
					
						
							| 
									
										
										
										
											2024-08-02 11:46:41 +00:00
										 |  |  | 	) | 
					
						
							| 
									
										
										
										
											2024-07-12 09:39:47 +00:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-08 17:12:13 +00:00
										 |  |  | // ffmpegGenerateWebpThumb generates a thumbnail webp from input media of any type, useful for any media. | 
					
						
							| 
									
										
										
										
											2024-08-30 14:03:59 +02:00
										 |  |  | func ffmpegGenerateWebpThumb(ctx context.Context, inpath, outpath string, width, height int, pixfmt string) error { | 
					
						
							| 
									
										
										
										
											2024-07-12 09:39:47 +00:00
										 |  |  | 	// Generate thumb with ffmpeg. | 
					
						
							| 
									
										
										
										
											2024-08-30 14:03:59 +02:00
										 |  |  | 	return ffmpeg(ctx, inpath, outpath, | 
					
						
							| 
									
										
										
										
											2024-07-28 19:10:41 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 		// Only log errors. | 
					
						
							| 
									
										
										
										
											2024-07-12 09:39:47 +00:00
										 |  |  | 		"-loglevel", "error", | 
					
						
							| 
									
										
										
										
											2024-07-19 15:28:43 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 		// Input file. | 
					
						
							| 
									
										
										
										
											2024-08-30 14:03:59 +02:00
										 |  |  | 		"-i", inpath, | 
					
						
							| 
									
										
										
										
											2024-07-19 15:28:43 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 		// Encode using libwebp. | 
					
						
							|  |  |  | 		// (NOT as libwebp_anim). | 
					
						
							|  |  |  | 		"-codec:v", "libwebp", | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-10-18 15:44:08 +02:00
										 |  |  | 		// Only one frame | 
					
						
							|  |  |  | 		"-frames:v", "1", | 
					
						
							| 
									
										
										
										
											2024-07-19 15:28:43 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-10-18 15:44:08 +02:00
										 |  |  | 		// Scale to dimensions | 
					
						
							|  |  |  | 		// (scale filter: https://ffmpeg.org/ffmpeg-filters.html#scale) | 
					
						
							|  |  |  | 		"-filter:v", "scale="+strconv.Itoa(width)+":"+strconv.Itoa(height)+","+ | 
					
						
							| 
									
										
										
										
											2024-07-19 15:28:43 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-08 17:12:13 +00:00
										 |  |  | 			// Attempt to use original pixel format | 
					
						
							| 
									
										
										
										
											2024-07-19 15:28:43 +00:00
										 |  |  | 			// (format filter: https://ffmpeg.org/ffmpeg-filters.html#format) | 
					
						
							| 
									
										
										
										
											2024-08-08 17:12:13 +00:00
										 |  |  | 			"format=pix_fmts="+pixfmt, | 
					
						
							| 
									
										
										
										
											2024-07-19 15:28:43 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-08 17:12:13 +00:00
										 |  |  | 		// Quality not specified, | 
					
						
							|  |  |  | 		// i.e. use default which | 
					
						
							|  |  |  | 		// should be 75% webp quality. | 
					
						
							| 
									
										
										
										
											2024-07-19 15:28:43 +00:00
										 |  |  | 		// (codec options: https://ffmpeg.org/ffmpeg-codecs.html#toc-Codec-Options) | 
					
						
							|  |  |  | 		// (libwebp codec: https://ffmpeg.org/ffmpeg-codecs.html#Options-36) | 
					
						
							| 
									
										
										
										
											2024-08-08 17:12:13 +00:00
										 |  |  | 		// "-qscale:v", "75", | 
					
						
							| 
									
										
										
										
											2024-07-19 15:28:43 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 		// Overwrite. | 
					
						
							| 
									
										
										
										
											2024-07-12 09:39:47 +00:00
										 |  |  | 		"-y", | 
					
						
							| 
									
										
										
										
											2024-07-19 15:28:43 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 		// Output. | 
					
						
							| 
									
										
										
										
											2024-07-12 09:39:47 +00:00
										 |  |  | 		outpath, | 
					
						
							| 
									
										
										
										
											2024-08-08 17:12:13 +00:00
										 |  |  | 	) | 
					
						
							| 
									
										
										
										
											2024-07-12 09:39:47 +00:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // ffmpegGenerateStatic generates a static png from input image of any type, useful for emoji. | 
					
						
							| 
									
										
										
										
											2024-08-30 14:03:59 +02:00
										 |  |  | func ffmpegGenerateStatic(ctx context.Context, inpath string) (string, error) { | 
					
						
							| 
									
										
										
										
											2024-07-28 08:31:49 +00:00
										 |  |  | 	var outpath string | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Generate thumb output path REPLACING extension. | 
					
						
							| 
									
										
										
										
											2024-08-30 14:03:59 +02:00
										 |  |  | 	if i := strings.IndexByte(inpath, '.'); i != -1 { | 
					
						
							|  |  |  | 		outpath = inpath[:i] + "_static.png" | 
					
						
							| 
									
										
										
										
											2024-07-28 08:31:49 +00:00
										 |  |  | 	} else { | 
					
						
							|  |  |  | 		return "", gtserror.New("input file missing extension") | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-12 09:39:47 +00:00
										 |  |  | 	// Generate static with ffmpeg. | 
					
						
							| 
									
										
										
										
											2024-08-30 14:03:59 +02:00
										 |  |  | 	if err := ffmpeg(ctx, inpath, outpath, | 
					
						
							| 
									
										
										
										
											2024-07-28 19:10:41 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 		// Only log errors. | 
					
						
							| 
									
										
										
										
											2024-07-12 09:39:47 +00:00
										 |  |  | 		"-loglevel", "error", | 
					
						
							| 
									
										
										
										
											2024-07-19 15:28:43 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 		// Input file. | 
					
						
							| 
									
										
										
										
											2024-08-30 14:03:59 +02:00
										 |  |  | 		"-i", inpath, | 
					
						
							| 
									
										
										
										
											2024-07-19 15:28:43 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 		// Only first frame. | 
					
						
							|  |  |  | 		"-frames:v", "1", | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// Encode using png. | 
					
						
							|  |  |  | 		// (NOT as apng). | 
					
						
							|  |  |  | 		"-codec:v", "png", | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// Overwrite. | 
					
						
							| 
									
										
										
										
											2024-07-12 09:39:47 +00:00
										 |  |  | 		"-y", | 
					
						
							| 
									
										
										
										
											2024-07-19 15:28:43 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 		// Output. | 
					
						
							| 
									
										
										
										
											2024-07-12 09:39:47 +00:00
										 |  |  | 		outpath, | 
					
						
							|  |  |  | 	); err != nil { | 
					
						
							|  |  |  | 		return "", err | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return outpath, nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-30 14:03:59 +02:00
										 |  |  | // ffmpeg calls `ffmpeg [args...]` (WASM) with in + out paths mounted in runtime. | 
					
						
							|  |  |  | func ffmpeg(ctx context.Context, inpath string, outpath string, args ...string) error { | 
					
						
							| 
									
										
										
										
											2024-07-12 09:39:47 +00:00
										 |  |  | 	var stderr byteutil.Buffer | 
					
						
							| 
									
										
										
										
											2024-08-23 15:15:35 +00:00
										 |  |  | 	rc, err := _ffmpeg.Ffmpeg(ctx, _ffmpeg.Args{ | 
					
						
							| 
									
										
										
										
											2024-07-12 09:39:47 +00:00
										 |  |  | 		Stderr: &stderr, | 
					
						
							|  |  |  | 		Args:   args, | 
					
						
							|  |  |  | 		Config: func(modcfg wazero.ModuleConfig) wazero.ModuleConfig { | 
					
						
							| 
									
										
										
										
											2024-08-30 14:03:59 +02:00
										 |  |  | 			fscfg := wazero.NewFSConfig() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// Needs read-only access to | 
					
						
							|  |  |  | 			// /dev/urandom for some types. | 
					
						
							|  |  |  | 			urandom := &allowFiles{ | 
					
						
							|  |  |  | 				{ | 
					
						
							|  |  |  | 					abs:  "/dev/urandom", | 
					
						
							|  |  |  | 					flag: os.O_RDONLY, | 
					
						
							|  |  |  | 					perm: 0, | 
					
						
							|  |  |  | 				}, | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			fscfg = fscfg.WithFSMount(urandom, "/dev") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// In+out dirs are always the same (tmp), | 
					
						
							|  |  |  | 			// so we can share one file system for | 
					
						
							|  |  |  | 			// both + grant different perms to inpath | 
					
						
							|  |  |  | 			// (read only) and outpath (read+write). | 
					
						
							|  |  |  | 			shared := &allowFiles{ | 
					
						
							|  |  |  | 				{ | 
					
						
							|  |  |  | 					abs:  inpath, | 
					
						
							|  |  |  | 					flag: os.O_RDONLY, | 
					
						
							|  |  |  | 					perm: 0, | 
					
						
							|  |  |  | 				}, | 
					
						
							|  |  |  | 				{ | 
					
						
							|  |  |  | 					abs:  outpath, | 
					
						
							|  |  |  | 					flag: os.O_RDWR | os.O_CREATE | os.O_TRUNC, | 
					
						
							|  |  |  | 					perm: 0666, | 
					
						
							|  |  |  | 				}, | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			fscfg = fscfg.WithFSMount(shared, path.Dir(inpath)) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			return modcfg.WithFSConfig(fscfg) | 
					
						
							| 
									
										
										
										
											2024-07-12 09:39:47 +00:00
										 |  |  | 		}, | 
					
						
							|  |  |  | 	}) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return gtserror.Newf("error running: %w", err) | 
					
						
							|  |  |  | 	} else if rc != 0 { | 
					
						
							|  |  |  | 		return gtserror.Newf("non-zero return code %d (%s)", rc, stderr.B) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	return nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // ffprobe calls `ffprobe` (WASM) on filepath, returning parsed JSON output. | 
					
						
							| 
									
										
										
										
											2024-07-15 14:24:53 +00:00
										 |  |  | func ffprobe(ctx context.Context, filepath string) (*result, error) { | 
					
						
							| 
									
										
										
										
											2024-07-12 09:39:47 +00:00
										 |  |  | 	var stdout byteutil.Buffer | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Run ffprobe on our given file at path. | 
					
						
							| 
									
										
										
										
											2024-08-23 15:15:35 +00:00
										 |  |  | 	_, err := _ffmpeg.Ffprobe(ctx, _ffmpeg.Args{ | 
					
						
							| 
									
										
										
										
											2024-07-12 09:39:47 +00:00
										 |  |  | 		Stdout: &stdout, | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		Args: []string{ | 
					
						
							| 
									
										
										
										
											2024-07-28 19:10:41 +00:00
										 |  |  | 			// Don't show any excess logging | 
					
						
							|  |  |  | 			// information, all goes in JSON. | 
					
						
							| 
									
										
										
										
											2024-07-12 09:39:47 +00:00
										 |  |  | 			"-loglevel", "quiet", | 
					
						
							| 
									
										
										
										
											2024-07-28 19:10:41 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 			// Print in compact JSON format. | 
					
						
							| 
									
										
										
										
											2024-07-15 14:24:53 +00:00
										 |  |  | 			"-print_format", "json=compact=1", | 
					
						
							| 
									
										
										
										
											2024-07-28 19:10:41 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 			// Show error in our | 
					
						
							|  |  |  | 			// chosen format type. | 
					
						
							| 
									
										
										
										
											2024-07-12 09:39:47 +00:00
										 |  |  | 			"-show_error", | 
					
						
							| 
									
										
										
										
											2024-07-28 19:10:41 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 			// Show specifically container format, total duration and bitrate. | 
					
						
							|  |  |  | 			"-show_entries", "format=format_name,duration,bit_rate" + ":" + | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-08 17:12:13 +00:00
										 |  |  | 				// Show specifically stream codec names, types, frame rate, duration, dimens, and pixel format. | 
					
						
							|  |  |  | 				"stream=codec_name,codec_type,r_frame_rate,duration_ts,width,height,pix_fmt" + ":" + | 
					
						
							| 
									
										
										
										
											2024-07-28 19:10:41 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-23 15:13:01 +00:00
										 |  |  | 				// Show orientation tag. | 
					
						
							|  |  |  | 				"tags=orientation" + ":" + | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				// Show rotation data. | 
					
						
							|  |  |  | 				"side_data=rotation", | 
					
						
							| 
									
										
										
										
											2024-07-28 19:10:41 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-30 14:18:29 +00:00
										 |  |  | 			// Limit to reading the first | 
					
						
							|  |  |  | 			// 1s of data looking for "rotation" | 
					
						
							|  |  |  | 			// side_data tags (expensive part). | 
					
						
							|  |  |  | 			"-read_intervals", "%+1", | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-28 19:10:41 +00:00
										 |  |  | 			// Input file. | 
					
						
							|  |  |  | 			"-i", filepath, | 
					
						
							| 
									
										
										
										
											2024-07-12 09:39:47 +00:00
										 |  |  | 		}, | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		Config: func(modcfg wazero.ModuleConfig) wazero.ModuleConfig { | 
					
						
							|  |  |  | 			fscfg := wazero.NewFSConfig() | 
					
						
							| 
									
										
										
										
											2024-08-30 14:03:59 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 			// Needs read-only access | 
					
						
							|  |  |  | 			// to file being probed. | 
					
						
							|  |  |  | 			in := &allowFiles{ | 
					
						
							|  |  |  | 				{ | 
					
						
							|  |  |  | 					abs:  filepath, | 
					
						
							|  |  |  | 					flag: os.O_RDONLY, | 
					
						
							|  |  |  | 					perm: 0, | 
					
						
							|  |  |  | 				}, | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			fscfg = fscfg.WithFSMount(in, path.Dir(filepath)) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			return modcfg.WithFSConfig(fscfg) | 
					
						
							| 
									
										
										
										
											2024-07-12 09:39:47 +00:00
										 |  |  | 		}, | 
					
						
							|  |  |  | 	}) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return nil, gtserror.Newf("error running: %w", err) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	var result ffprobeResult | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Unmarshal the ffprobe output as our result type. | 
					
						
							|  |  |  | 	if err := json.Unmarshal(stdout.B, &result); err != nil { | 
					
						
							|  |  |  | 		return nil, gtserror.Newf("error unmarshaling json: %w", err) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-15 14:24:53 +00:00
										 |  |  | 	// Convert raw result data. | 
					
						
							|  |  |  | 	res, err := result.Process() | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return nil, err | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return res, nil | 
					
						
							| 
									
										
										
										
											2024-07-12 09:39:47 +00:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-08 17:12:13 +00:00
										 |  |  | const ( | 
					
						
							|  |  |  | 	// possible orientation values | 
					
						
							|  |  |  | 	// specified in "orientation" | 
					
						
							|  |  |  | 	// tag of images. | 
					
						
							|  |  |  | 	// | 
					
						
							|  |  |  | 	// FlipH      = flips horizontally | 
					
						
							|  |  |  | 	// FlipV      = flips vertically | 
					
						
							|  |  |  | 	// Transpose  = flips horizontally and rotates 90 counter-clockwise. | 
					
						
							|  |  |  | 	// Transverse = flips vertically and rotates 90 counter-clockwise. | 
					
						
							|  |  |  | 	orientationUnspecified = 0 | 
					
						
							|  |  |  | 	orientationNormal      = 1 | 
					
						
							|  |  |  | 	orientationFlipH       = 2 | 
					
						
							|  |  |  | 	orientationRotate180   = 3 | 
					
						
							|  |  |  | 	orientationFlipV       = 4 | 
					
						
							|  |  |  | 	orientationTranspose   = 5 | 
					
						
							|  |  |  | 	orientationRotate270   = 6 | 
					
						
							|  |  |  | 	orientationTransverse  = 7 | 
					
						
							|  |  |  | 	orientationRotate90    = 8 | 
					
						
							|  |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-15 14:24:53 +00:00
										 |  |  | // result contains parsed ffprobe result | 
					
						
							|  |  |  | // data in a more useful data format. | 
					
						
							|  |  |  | type result struct { | 
					
						
							| 
									
										
										
										
											2024-08-08 17:12:13 +00:00
										 |  |  | 	format      string | 
					
						
							|  |  |  | 	audio       []audioStream | 
					
						
							|  |  |  | 	video       []videoStream | 
					
						
							|  |  |  | 	duration    float64 | 
					
						
							|  |  |  | 	bitrate     uint64 | 
					
						
							|  |  |  | 	orientation int | 
					
						
							| 
									
										
										
										
											2024-07-12 09:39:47 +00:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-15 14:24:53 +00:00
										 |  |  | type stream struct { | 
					
						
							|  |  |  | 	codec string | 
					
						
							| 
									
										
										
										
											2024-07-12 09:39:47 +00:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-15 14:24:53 +00:00
										 |  |  | type audioStream struct { | 
					
						
							|  |  |  | 	stream | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | type videoStream struct { | 
					
						
							|  |  |  | 	stream | 
					
						
							| 
									
										
										
										
											2024-08-08 17:12:13 +00:00
										 |  |  | 	pixfmt    string | 
					
						
							| 
									
										
										
										
											2024-07-15 14:24:53 +00:00
										 |  |  | 	width     int | 
					
						
							|  |  |  | 	height    int | 
					
						
							|  |  |  | 	framerate float32 | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // GetFileType determines file type and extension to use for media data. This | 
					
						
							|  |  |  | // function helps to abstract away the horrible complexities that are possible | 
					
						
							|  |  |  | // media container (i.e. the file) types and and possible sub-types within that. | 
					
						
							|  |  |  | // | 
					
						
							|  |  |  | // Note the checks for (len(res.video) > 0) may catch some audio files with embedded | 
					
						
							|  |  |  | // album art as video, but i blame that on the hellscape that is media filetypes. | 
					
						
							| 
									
										
										
										
											2024-11-04 13:58:15 +00:00
										 |  |  | func (res *result) GetFileType() (gtsmodel.FileType, string, string) { | 
					
						
							| 
									
										
										
										
											2024-07-15 14:24:53 +00:00
										 |  |  | 	switch res.format { | 
					
						
							|  |  |  | 	case "mpeg": | 
					
						
							| 
									
										
										
										
											2024-11-04 13:58:15 +00:00
										 |  |  | 		return gtsmodel.FileTypeVideo, | 
					
						
							|  |  |  | 			"video/mpeg", "mpeg" | 
					
						
							| 
									
										
										
										
											2024-07-15 14:24:53 +00:00
										 |  |  | 	case "mjpeg": | 
					
						
							| 
									
										
										
										
											2024-11-04 13:58:15 +00:00
										 |  |  | 		return gtsmodel.FileTypeVideo, | 
					
						
							|  |  |  | 			"video/x-motion-jpeg", "mjpeg" | 
					
						
							| 
									
										
										
										
											2024-07-15 14:24:53 +00:00
										 |  |  | 	case "mov,mp4,m4a,3gp,3g2,mj2": | 
					
						
							|  |  |  | 		switch { | 
					
						
							|  |  |  | 		case len(res.video) > 0: | 
					
						
							| 
									
										
										
										
											2024-08-08 10:12:16 +02:00
										 |  |  | 			if len(res.audio) == 0 && | 
					
						
							|  |  |  | 				res.duration <= 30 { | 
					
						
							|  |  |  | 				// Short, soundless | 
					
						
							|  |  |  | 				// video file aka gifv. | 
					
						
							| 
									
										
										
										
											2024-11-04 13:58:15 +00:00
										 |  |  | 				return gtsmodel.FileTypeGifv, | 
					
						
							|  |  |  | 					"video/mp4", "mp4" | 
					
						
							| 
									
										
										
										
											2024-08-08 10:12:16 +02:00
										 |  |  | 			} else { | 
					
						
							|  |  |  | 				// Video file (with or without audio). | 
					
						
							| 
									
										
										
										
											2024-11-04 13:58:15 +00:00
										 |  |  | 				return gtsmodel.FileTypeVideo, | 
					
						
							|  |  |  | 					"video/mp4", "mp4" | 
					
						
							| 
									
										
										
										
											2024-08-08 10:12:16 +02:00
										 |  |  | 			} | 
					
						
							| 
									
										
										
										
											2024-07-15 14:24:53 +00:00
										 |  |  | 		case len(res.audio) > 0 && | 
					
						
							|  |  |  | 			res.audio[0].codec == "aac": | 
					
						
							|  |  |  | 			// m4a only supports [aac] audio. | 
					
						
							| 
									
										
										
										
											2024-11-04 13:58:15 +00:00
										 |  |  | 			return gtsmodel.FileTypeAudio, | 
					
						
							|  |  |  | 				"audio/mp4", "m4a" | 
					
						
							| 
									
										
										
										
											2024-07-15 11:47:57 +02:00
										 |  |  | 		} | 
					
						
							| 
									
										
										
										
											2024-07-15 14:24:53 +00:00
										 |  |  | 	case "apng": | 
					
						
							| 
									
										
										
										
											2024-11-04 13:58:15 +00:00
										 |  |  | 		return gtsmodel.FileTypeImage, | 
					
						
							|  |  |  | 			"image/apng", "apng" | 
					
						
							| 
									
										
										
										
											2024-07-15 14:24:53 +00:00
										 |  |  | 	case "png_pipe": | 
					
						
							| 
									
										
										
										
											2024-11-04 13:58:15 +00:00
										 |  |  | 		return gtsmodel.FileTypeImage, | 
					
						
							|  |  |  | 			"image/png", "png" | 
					
						
							| 
									
										
										
										
											2024-07-15 14:24:53 +00:00
										 |  |  | 	case "image2", "image2pipe", "jpeg_pipe": | 
					
						
							| 
									
										
										
										
											2024-11-04 13:58:15 +00:00
										 |  |  | 		return gtsmodel.FileTypeImage, | 
					
						
							|  |  |  | 			"image/jpeg", "jpeg" | 
					
						
							| 
									
										
										
										
											2024-07-15 14:24:53 +00:00
										 |  |  | 	case "webp", "webp_pipe": | 
					
						
							| 
									
										
										
										
											2024-11-04 13:58:15 +00:00
										 |  |  | 		return gtsmodel.FileTypeImage, | 
					
						
							|  |  |  | 			"image/webp", "webp" | 
					
						
							| 
									
										
										
										
											2024-07-15 14:24:53 +00:00
										 |  |  | 	case "gif": | 
					
						
							| 
									
										
										
										
											2024-11-04 13:58:15 +00:00
										 |  |  | 		return gtsmodel.FileTypeImage, | 
					
						
							|  |  |  | 			"image/gif", "gif" | 
					
						
							| 
									
										
										
										
											2024-07-15 14:24:53 +00:00
										 |  |  | 	case "mp3": | 
					
						
							|  |  |  | 		if len(res.audio) > 0 { | 
					
						
							|  |  |  | 			switch res.audio[0].codec { | 
					
						
							|  |  |  | 			case "mp2": | 
					
						
							| 
									
										
										
										
											2024-11-04 13:58:15 +00:00
										 |  |  | 				return gtsmodel.FileTypeAudio, | 
					
						
							|  |  |  | 					"audio/mp2", "mp2" | 
					
						
							| 
									
										
										
										
											2024-07-15 14:24:53 +00:00
										 |  |  | 			case "mp3": | 
					
						
							| 
									
										
										
										
											2024-11-04 13:58:15 +00:00
										 |  |  | 				return gtsmodel.FileTypeAudio, | 
					
						
							|  |  |  | 					"audio/mp3", "mp3" | 
					
						
							| 
									
										
										
										
											2024-07-15 14:24:53 +00:00
										 |  |  | 			} | 
					
						
							| 
									
										
										
										
											2024-07-15 11:47:57 +02:00
										 |  |  | 		} | 
					
						
							| 
									
										
										
										
											2024-07-15 14:24:53 +00:00
										 |  |  | 	case "asf": | 
					
						
							|  |  |  | 		switch { | 
					
						
							|  |  |  | 		case len(res.video) > 0: | 
					
						
							| 
									
										
										
										
											2024-11-04 13:58:15 +00:00
										 |  |  | 			return gtsmodel.FileTypeVideo, | 
					
						
							|  |  |  | 				"video/x-ms-wmv", "wmv" | 
					
						
							| 
									
										
										
										
											2024-07-15 14:24:53 +00:00
										 |  |  | 		case len(res.audio) > 0: | 
					
						
							| 
									
										
										
										
											2024-11-04 13:58:15 +00:00
										 |  |  | 			return gtsmodel.FileTypeAudio, | 
					
						
							|  |  |  | 				"audio/x-ms-wma", "wma" | 
					
						
							| 
									
										
										
										
											2024-07-15 14:24:53 +00:00
										 |  |  | 		} | 
					
						
							|  |  |  | 	case "ogg": | 
					
						
							| 
									
										
										
										
											2024-09-27 11:16:34 +00:00
										 |  |  | 		if len(res.video) > 0 { | 
					
						
							|  |  |  | 			switch res.video[0].codec { | 
					
						
							|  |  |  | 			case "theora", "dirac": // daala, tarkin | 
					
						
							| 
									
										
										
										
											2024-11-04 13:58:15 +00:00
										 |  |  | 				return gtsmodel.FileTypeVideo, | 
					
						
							|  |  |  | 					"video/ogg", "ogv" | 
					
						
							| 
									
										
										
										
											2024-09-27 11:16:34 +00:00
										 |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		if len(res.audio) > 0 { | 
					
						
							|  |  |  | 			switch res.audio[0].codec { | 
					
						
							|  |  |  | 			case "opus", "libopus": | 
					
						
							| 
									
										
										
										
											2024-11-04 13:58:15 +00:00
										 |  |  | 				return gtsmodel.FileTypeAudio, | 
					
						
							|  |  |  | 					"audio/opus", "opus" | 
					
						
							| 
									
										
										
										
											2024-09-27 11:16:34 +00:00
										 |  |  | 			default: | 
					
						
							| 
									
										
										
										
											2024-11-04 13:58:15 +00:00
										 |  |  | 				return gtsmodel.FileTypeAudio, | 
					
						
							|  |  |  | 					"audio/ogg", "ogg" | 
					
						
							| 
									
										
										
										
											2024-09-27 11:16:34 +00:00
										 |  |  | 			} | 
					
						
							| 
									
										
										
										
											2024-07-15 14:24:53 +00:00
										 |  |  | 		} | 
					
						
							|  |  |  | 	case "matroska,webm": | 
					
						
							|  |  |  | 		switch { | 
					
						
							|  |  |  | 		case len(res.video) > 0: | 
					
						
							| 
									
										
										
										
											2024-10-28 13:09:21 +00:00
										 |  |  | 			var isWebm bool | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-15 14:24:53 +00:00
										 |  |  | 			switch res.video[0].codec { | 
					
						
							|  |  |  | 			case "vp8", "vp9", "av1": | 
					
						
							| 
									
										
										
										
											2024-10-28 13:09:21 +00:00
										 |  |  | 				if len(res.audio) > 0 { | 
					
						
							|  |  |  | 					switch res.audio[0].codec { | 
					
						
							|  |  |  | 					case "vorbis", "opus", "libopus": | 
					
						
							|  |  |  | 						// webm only supports [VP8/VP9/AV1] + | 
					
						
							|  |  |  | 						//                    [vorbis/opus] | 
					
						
							|  |  |  | 						isWebm = true | 
					
						
							|  |  |  | 					} | 
					
						
							|  |  |  | 				} else { | 
					
						
							|  |  |  | 					// no audio with correct | 
					
						
							|  |  |  | 					// video codec also fine. | 
					
						
							|  |  |  | 					isWebm = true | 
					
						
							| 
									
										
										
										
											2024-07-15 14:24:53 +00:00
										 |  |  | 				} | 
					
						
							|  |  |  | 			} | 
					
						
							| 
									
										
										
										
											2024-10-28 13:09:21 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 			if isWebm { | 
					
						
							| 
									
										
										
										
											2024-11-04 13:58:15 +00:00
										 |  |  | 				// Check valid webm codec config. | 
					
						
							|  |  |  | 				return gtsmodel.FileTypeVideo, | 
					
						
							|  |  |  | 					"video/webm", "webm" | 
					
						
							| 
									
										
										
										
											2024-10-28 13:09:21 +00:00
										 |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// All else falls under generic mkv. | 
					
						
							| 
									
										
										
										
											2024-11-04 13:58:15 +00:00
										 |  |  | 			return gtsmodel.FileTypeVideo, | 
					
						
							|  |  |  | 				"video/x-matroska", "mkv" | 
					
						
							| 
									
										
										
										
											2024-07-15 14:24:53 +00:00
										 |  |  | 		case len(res.audio) > 0: | 
					
						
							| 
									
										
										
										
											2024-11-04 13:58:15 +00:00
										 |  |  | 			return gtsmodel.FileTypeAudio, | 
					
						
							|  |  |  | 				"audio/x-matroska", "mka" | 
					
						
							| 
									
										
										
										
											2024-07-15 11:47:57 +02:00
										 |  |  | 		} | 
					
						
							| 
									
										
										
										
											2024-07-15 14:24:53 +00:00
										 |  |  | 	case "avi": | 
					
						
							| 
									
										
										
										
											2024-11-04 13:58:15 +00:00
										 |  |  | 		return gtsmodel.FileTypeVideo, | 
					
						
							|  |  |  | 			"video/x-msvideo", "avi" | 
					
						
							| 
									
										
										
										
											2024-07-21 13:42:51 +01:00
										 |  |  | 	case "flac": | 
					
						
							| 
									
										
										
										
											2024-11-04 13:58:15 +00:00
										 |  |  | 		return gtsmodel.FileTypeAudio, | 
					
						
							|  |  |  | 			"audio/flac", "flac" | 
					
						
							| 
									
										
										
										
											2024-07-15 11:47:57 +02:00
										 |  |  | 	} | 
					
						
							| 
									
										
										
										
											2024-11-04 13:58:15 +00:00
										 |  |  | 	return gtsmodel.FileTypeUnknown, | 
					
						
							|  |  |  | 		"", res.format | 
					
						
							| 
									
										
										
										
											2024-07-15 11:47:57 +02:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-15 14:24:53 +00:00
										 |  |  | // ImageMeta extracts image metadata contained within ffprobe'd media result streams. | 
					
						
							|  |  |  | func (res *result) ImageMeta() (width int, height int, framerate float32) { | 
					
						
							|  |  |  | 	for _, stream := range res.video { | 
					
						
							| 
									
										
										
										
											2024-08-02 11:46:41 +00:00
										 |  |  | 		// Use widest found width. | 
					
						
							| 
									
										
										
										
											2024-07-15 14:24:53 +00:00
										 |  |  | 		if stream.width > width { | 
					
						
							|  |  |  | 			width = stream.width | 
					
						
							| 
									
										
										
										
											2024-07-12 09:39:47 +00:00
										 |  |  | 		} | 
					
						
							| 
									
										
										
										
											2024-08-02 11:46:41 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 		// Use tallest found height. | 
					
						
							| 
									
										
										
										
											2024-07-15 14:24:53 +00:00
										 |  |  | 		if stream.height > height { | 
					
						
							|  |  |  | 			height = stream.height | 
					
						
							| 
									
										
										
										
											2024-07-12 09:39:47 +00:00
										 |  |  | 		} | 
					
						
							| 
									
										
										
										
											2024-08-02 11:46:41 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 		// Use lowest non-zero (valid) framerate. | 
					
						
							| 
									
										
										
										
											2024-07-15 14:24:53 +00:00
										 |  |  | 		if fr := float32(stream.framerate); fr > 0 { | 
					
						
							| 
									
										
										
										
											2024-07-12 09:39:47 +00:00
										 |  |  | 			if framerate == 0 || fr < framerate { | 
					
						
							|  |  |  | 				framerate = fr | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2024-08-02 11:46:41 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	// If image is rotated by | 
					
						
							|  |  |  | 	// any odd multiples of 90, | 
					
						
							|  |  |  | 	// flip width / height to | 
					
						
							|  |  |  | 	// get the correct scale. | 
					
						
							| 
									
										
										
										
											2024-08-08 17:12:13 +00:00
										 |  |  | 	switch res.orientation { | 
					
						
							|  |  |  | 	case orientationRotate90, | 
					
						
							|  |  |  | 		orientationRotate270, | 
					
						
							|  |  |  | 		orientationTransverse, | 
					
						
							|  |  |  | 		orientationTranspose: | 
					
						
							| 
									
										
										
										
											2024-08-02 11:46:41 +00:00
										 |  |  | 		width, height = height, width | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-12 09:39:47 +00:00
										 |  |  | 	return | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-08 17:12:13 +00:00
										 |  |  | // PixFmt returns the first valid pixel format | 
					
						
							|  |  |  | // contained among the result vidoe streams. | 
					
						
							|  |  |  | func (res *result) PixFmt() string { | 
					
						
							|  |  |  | 	for _, str := range res.video { | 
					
						
							|  |  |  | 		if str.pixfmt != "" { | 
					
						
							|  |  |  | 			return str.pixfmt | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	return "" | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-15 14:24:53 +00:00
										 |  |  | // Process converts raw ffprobe result data into our more usable result{} type. | 
					
						
							|  |  |  | func (res *ffprobeResult) Process() (*result, error) { | 
					
						
							|  |  |  | 	if res.Error != nil { | 
					
						
							|  |  |  | 		return nil, res.Error | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2024-07-12 09:39:47 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-15 14:24:53 +00:00
										 |  |  | 	if res.Format == nil { | 
					
						
							|  |  |  | 		return nil, errors.New("missing format data") | 
					
						
							| 
									
										
										
										
											2024-07-15 11:47:57 +02:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-15 14:24:53 +00:00
										 |  |  | 	var r result | 
					
						
							|  |  |  | 	var err error | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Copy over container format. | 
					
						
							|  |  |  | 	r.format = res.Format.FormatName | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Parsed media bitrate (if it was set). | 
					
						
							|  |  |  | 	if str := res.Format.BitRate; str != "" { | 
					
						
							|  |  |  | 		r.bitrate, err = strconv.ParseUint(str, 10, 64) | 
					
						
							|  |  |  | 		if err != nil { | 
					
						
							|  |  |  | 			return nil, gtserror.Newf("invalid bitrate %s: %w", str, err) | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2024-07-15 11:47:57 +02:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-15 14:24:53 +00:00
										 |  |  | 	// Parse media duration (if it was set). | 
					
						
							|  |  |  | 	if str := res.Format.Duration; str != "" { | 
					
						
							|  |  |  | 		r.duration, err = strconv.ParseFloat(str, 32) | 
					
						
							|  |  |  | 		if err != nil { | 
					
						
							|  |  |  | 			return nil, gtserror.Newf("invalid duration %s: %w", str, err) | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2024-07-12 09:39:47 +00:00
										 |  |  | 	} | 
					
						
							| 
									
										
										
										
											2024-07-15 11:47:57 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-31 20:43:39 +02:00
										 |  |  | 	// Check extra packet / frame information | 
					
						
							| 
									
										
										
										
											2024-09-23 15:13:01 +00:00
										 |  |  | 	// for provided orientation (if provided). | 
					
						
							| 
									
										
										
										
											2024-07-31 20:43:39 +02:00
										 |  |  | 	for _, pf := range res.PacketsAndFrames { | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-08 17:12:13 +00:00
										 |  |  | 		// Ensure frame contains tags. | 
					
						
							|  |  |  | 		if pf.Tags.Orientation == "" { | 
					
						
							|  |  |  | 			continue | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2024-07-31 20:43:39 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-08 17:12:13 +00:00
										 |  |  | 		// Trim any space from orientation value. | 
					
						
							|  |  |  | 		str := strings.TrimSpace(pf.Tags.Orientation) | 
					
						
							| 
									
										
										
										
											2024-07-31 20:43:39 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-08 17:12:13 +00:00
										 |  |  | 		// Parse as integer value. | 
					
						
							| 
									
										
										
										
											2024-09-23 15:13:01 +00:00
										 |  |  | 		orient, _ := strconv.Atoi(str) | 
					
						
							|  |  |  | 		if orient < 0 || orient >= 9 { | 
					
						
							| 
									
										
										
										
											2024-08-08 17:12:13 +00:00
										 |  |  | 			return nil, errors.New("invalid orientation data") | 
					
						
							| 
									
										
										
										
											2024-07-31 20:43:39 +02:00
										 |  |  | 		} | 
					
						
							| 
									
										
										
										
											2024-08-08 17:12:13 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-23 15:13:01 +00:00
										 |  |  | 		// Ensure different value has | 
					
						
							|  |  |  | 		// not already been specified. | 
					
						
							|  |  |  | 		if r.orientation != 0 && | 
					
						
							|  |  |  | 			orient != r.orientation { | 
					
						
							|  |  |  | 			return nil, errors.New("multiple sets of orientation / rotation data") | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// Set new orientation. | 
					
						
							|  |  |  | 		r.orientation = orient | 
					
						
							| 
									
										
										
										
											2024-07-31 20:43:39 +02:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-15 14:24:53 +00:00
										 |  |  | 	// Preallocate streams to max possible lengths. | 
					
						
							|  |  |  | 	r.audio = make([]audioStream, 0, len(res.Streams)) | 
					
						
							|  |  |  | 	r.video = make([]videoStream, 0, len(res.Streams)) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Convert streams to separate types. | 
					
						
							|  |  |  | 	for _, s := range res.Streams { | 
					
						
							|  |  |  | 		switch s.CodecType { | 
					
						
							|  |  |  | 		case "audio": | 
					
						
							|  |  |  | 			// Append audio stream data to result. | 
					
						
							|  |  |  | 			r.audio = append(r.audio, audioStream{ | 
					
						
							|  |  |  | 				stream: stream{codec: s.CodecName}, | 
					
						
							|  |  |  | 			}) | 
					
						
							|  |  |  | 		case "video": | 
					
						
							|  |  |  | 			// Parse stream framerate, bearing in | 
					
						
							|  |  |  | 			// mind that some static container formats | 
					
						
							|  |  |  | 			// (e.g. jpeg) still return a framerate, so | 
					
						
							|  |  |  | 			// we also check for a non-1 timebase (dts). | 
					
						
							| 
									
										
										
										
											2024-07-31 20:43:39 +02:00
										 |  |  | 			var framerate float32 | 
					
						
							| 
									
										
										
										
											2024-07-15 14:24:53 +00:00
										 |  |  | 			if str := s.RFrameRate; str != "" && | 
					
						
							|  |  |  | 				s.DurationTS > 1 { | 
					
						
							|  |  |  | 				var num, den uint32 | 
					
						
							|  |  |  | 				den = 1 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				// Check for inequality (numerator / denominator). | 
					
						
							|  |  |  | 				if p := strings.SplitN(str, "/", 2); len(p) == 2 { | 
					
						
							|  |  |  | 					n, _ := strconv.ParseUint(p[0], 10, 32) | 
					
						
							|  |  |  | 					d, _ := strconv.ParseUint(p[1], 10, 32) | 
					
						
							| 
									
										
										
										
											2024-10-16 14:13:58 +02:00
										 |  |  | 					num, den = uint32(n), uint32(d) // #nosec G115 -- ParseUint is configured to check | 
					
						
							| 
									
										
										
										
											2024-07-15 14:24:53 +00:00
										 |  |  | 				} else { | 
					
						
							|  |  |  | 					n, _ := strconv.ParseUint(p[0], 10, 32) | 
					
						
							| 
									
										
										
										
											2024-10-16 14:13:58 +02:00
										 |  |  | 					num = uint32(n) // #nosec G115 -- ParseUint is configured to check | 
					
						
							| 
									
										
										
										
											2024-07-15 14:24:53 +00:00
										 |  |  | 				} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				// Set final divised framerate. | 
					
						
							|  |  |  | 				framerate = float32(num / den) | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-23 15:13:01 +00:00
										 |  |  | 			// Check for embedded sidedata | 
					
						
							|  |  |  | 			// which may contain rotation data. | 
					
						
							|  |  |  | 			for _, d := range s.SideDataList { | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				// Ensure frame side | 
					
						
							|  |  |  | 				// data IS rotation data. | 
					
						
							|  |  |  | 				if d.Rotation == 0 { | 
					
						
							|  |  |  | 					continue | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				// Drop any decimal | 
					
						
							|  |  |  | 				// rotation value. | 
					
						
							|  |  |  | 				rot := int(d.Rotation) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				// Round rotation to multiple of 90. | 
					
						
							|  |  |  | 				// More granularity is not needed. | 
					
						
							|  |  |  | 				if q := rot % 90; q > 45 { | 
					
						
							|  |  |  | 					rot += (90 - q) | 
					
						
							|  |  |  | 				} else { | 
					
						
							|  |  |  | 					rot -= q | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				// Drop any value above 360 | 
					
						
							|  |  |  | 				// or below -360, these are | 
					
						
							|  |  |  | 				// just repeat full turns. | 
					
						
							|  |  |  | 				// | 
					
						
							|  |  |  | 				// Then convert to | 
					
						
							|  |  |  | 				// orientation value. | 
					
						
							|  |  |  | 				var orient int | 
					
						
							|  |  |  | 				switch rot % 360 { | 
					
						
							|  |  |  | 				case 0: | 
					
						
							|  |  |  | 					orient = orientationNormal | 
					
						
							|  |  |  | 				case 90, -270: | 
					
						
							|  |  |  | 					orient = orientationRotate90 | 
					
						
							|  |  |  | 				case 180: | 
					
						
							|  |  |  | 					orient = orientationRotate180 | 
					
						
							|  |  |  | 				case 270, -90: | 
					
						
							|  |  |  | 					orient = orientationRotate270 | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				// Ensure different value has | 
					
						
							|  |  |  | 				// not already been specified. | 
					
						
							|  |  |  | 				if r.orientation != 0 && | 
					
						
							|  |  |  | 					orient != r.orientation { | 
					
						
							|  |  |  | 					return nil, errors.New("multiple sets of orientation / rotation data") | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				// Set new orientation. | 
					
						
							|  |  |  | 				r.orientation = orient | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-15 14:24:53 +00:00
										 |  |  | 			// Append video stream data to result. | 
					
						
							|  |  |  | 			r.video = append(r.video, videoStream{ | 
					
						
							|  |  |  | 				stream:    stream{codec: s.CodecName}, | 
					
						
							| 
									
										
										
										
											2024-08-08 17:12:13 +00:00
										 |  |  | 				pixfmt:    s.PixFmt, | 
					
						
							| 
									
										
										
										
											2024-08-02 11:46:41 +00:00
										 |  |  | 				width:     s.Width, | 
					
						
							|  |  |  | 				height:    s.Height, | 
					
						
							| 
									
										
										
										
											2024-07-15 14:24:53 +00:00
										 |  |  | 				framerate: framerate, | 
					
						
							|  |  |  | 			}) | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2024-07-15 11:47:57 +02:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-15 14:24:53 +00:00
										 |  |  | 	return &r, nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // ffprobeResult contains parsed JSON data from | 
					
						
							|  |  |  | // result of calling `ffprobe` on a media file. | 
					
						
							|  |  |  | type ffprobeResult struct { | 
					
						
							| 
									
										
										
										
											2024-07-28 19:10:41 +00:00
										 |  |  | 	PacketsAndFrames []ffprobePacketOrFrame `json:"packets_and_frames"` | 
					
						
							|  |  |  | 	Streams          []ffprobeStream        `json:"streams"` | 
					
						
							|  |  |  | 	Format           *ffprobeFormat         `json:"format"` | 
					
						
							|  |  |  | 	Error            *ffprobeError          `json:"error"` | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | type ffprobePacketOrFrame struct { | 
					
						
							| 
									
										
										
										
											2024-08-08 17:12:13 +00:00
										 |  |  | 	Type string      `json:"type"` | 
					
						
							|  |  |  | 	Tags ffprobeTags `json:"tags"` | 
					
						
							| 
									
										
										
										
											2024-09-23 15:13:01 +00:00
										 |  |  | 	// SideDataList []ffprobeSideData `json:"side_data_list"` | 
					
						
							| 
									
										
										
										
											2024-07-28 19:10:41 +00:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-08 17:12:13 +00:00
										 |  |  | type ffprobeTags struct { | 
					
						
							|  |  |  | 	Orientation string `json:"orientation"` | 
					
						
							| 
									
										
										
										
											2024-07-15 14:24:53 +00:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | type ffprobeStream struct { | 
					
						
							| 
									
										
										
										
											2024-09-23 15:13:01 +00:00
										 |  |  | 	CodecName    string            `json:"codec_name"` | 
					
						
							|  |  |  | 	CodecType    string            `json:"codec_type"` | 
					
						
							|  |  |  | 	PixFmt       string            `json:"pix_fmt"` | 
					
						
							|  |  |  | 	RFrameRate   string            `json:"r_frame_rate"` | 
					
						
							|  |  |  | 	DurationTS   uint              `json:"duration_ts"` | 
					
						
							|  |  |  | 	Width        int               `json:"width"` | 
					
						
							|  |  |  | 	Height       int               `json:"height"` | 
					
						
							|  |  |  | 	SideDataList []ffprobeSideData `json:"side_data_list"` | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | type ffprobeSideData struct { | 
					
						
							|  |  |  | 	Rotation float64 `json:"rotation"` | 
					
						
							| 
									
										
										
										
											2024-07-12 09:39:47 +00:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | type ffprobeFormat struct { | 
					
						
							|  |  |  | 	FormatName string `json:"format_name"` | 
					
						
							|  |  |  | 	Duration   string `json:"duration"` | 
					
						
							|  |  |  | 	BitRate    string `json:"bit_rate"` | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | type ffprobeError struct { | 
					
						
							|  |  |  | 	Code   int    `json:"code"` | 
					
						
							|  |  |  | 	String string `json:"string"` | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-15 14:24:53 +00:00
										 |  |  | func isUnsupportedTypeErr(err error) bool { | 
					
						
							|  |  |  | 	ffprobeErr, ok := err.(*ffprobeError) | 
					
						
							|  |  |  | 	return ok && ffprobeErr.Code == -1094995529 | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-12 09:39:47 +00:00
										 |  |  | func (err *ffprobeError) Error() string { | 
					
						
							|  |  |  | 	return err.String + " (" + strconv.Itoa(err.Code) + ")" | 
					
						
							|  |  |  | } |