diff --git a/util/ffmpeg/convert.go b/util/ffmpeg/convert.go new file mode 100644 index 00000000..b0df485d --- /dev/null +++ b/util/ffmpeg/convert.go @@ -0,0 +1,95 @@ +// Copyright (c) 2022 Sumner Evans +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package ffmpeg + +import ( + "fmt" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "strings" + + log "maunium.net/go/maulogger/v2" + "maunium.net/go/mautrix/util" +) + +var ffmpegDefaultParams = []string{"-hide_banner", "-loglevel", "warning"} + +// Convert a media file on the disk using ffmpeg. +// +// Args: +// * inputFile: The full path to the file. +// * outputExtension: The extension that the output file should be. +// * inputArgs: Arguments to tell ffmpeg how to parse the input file. +// * outputArgs: Arguments to tell ffmpeg how to convert the file to reach the wanted output. +// * removeInput: Whether the input file should be removed after converting. +// +// Returns: the path to the converted file. +func ConvertPath(inputFile string, outputExtension string, inputArgs []string, outputArgs []string, removeInput bool) (string, error) { + outputFilename := strings.TrimSuffix(inputFile, filepath.Ext(inputFile)) + "." + outputExtension + + args := []string{} + args = append(args, ffmpegDefaultParams...) + args = append(args, inputArgs...) + args = append(args, "-i", inputFile) + args = append(args, outputArgs...) + args = append(args, outputFilename) + + log.Info(args) + + cmd := exec.Command("ffmpeg", args...) + vcLog := log.Sub("ffmpeg").Writer(log.LevelWarn) + cmd.Stdout = vcLog + cmd.Stderr = vcLog + err := cmd.Run() + if err != nil { + return "", fmt.Errorf("ffmpeg error: %+v", err) + } + + if removeInput { + os.Remove(inputFile) + } + + return outputFilename, nil +} + +// Convert media data using ffmpeg. +// +// Args: +// * data: The media data to convert +// * outputExtension: The extension that the output file should be. +// * inputArgs: Arguments to tell ffmpeg how to parse the input file. +// * outputArgs: Arguments to tell ffmpeg how to convert the file to reach the wanted output. +// * inputMime: The mimetype of the input data. +// +// Returns: the converted data +func ConvertBytes(data []byte, outputExtension string, inputArgs []string, outputArgs []string, inputMime string) ([]byte, error) { + tempdir, err := ioutil.TempDir("", "mautrix_ffmpeg_*") + if err != nil { + return nil, err + } + defer os.RemoveAll(tempdir) + inputFileName := fmt.Sprintf("%s/input.%s", tempdir, util.ExtensionFromMimetype(inputMime)) + + inputFile, err := os.OpenFile(inputFileName, os.O_EXCL|os.O_WRONLY, 0600) + if err != nil { + return nil, fmt.Errorf("failed to open input file: %w", err) + } + _, err = inputFile.Write(data) + if err != nil { + inputFile.Close() + return nil, fmt.Errorf("failed to write data to input file: %w", err) + } + inputFile.Close() + + outputPath, err := ConvertPath(inputFileName, outputExtension, inputArgs, outputArgs, false) + if err != nil { + return nil, err + } + return ioutil.ReadFile(outputPath) +} diff --git a/util/mimetypes.go b/util/mimetypes.go new file mode 100644 index 00000000..2837ff7a --- /dev/null +++ b/util/mimetypes.go @@ -0,0 +1,49 @@ +// Copyright (c) 2022 Sumner Evans +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package util + +import ( + "mime" + "strings" +) + +// MimeExtensionSanityOverrides includes extensions for various common mimetypes. +// +// This is necessary because sometimes the OS mimetype database and Go interact in weird ways, +// which causes very obscure extensions to be first in the array for common mimetypes +// (e.g. image/jpeg -> .jpe, text/plain -> ,v). +var MimeExtensionSanityOverrides = map[string]string{ + "image/png": ".png", + "image/webp": ".webp", + "image/jpeg": ".jpg", + "image/tiff": ".tiff", + "image/heif": ".heic", + "image/heic": ".heic", + + "audio/mpeg": ".mp3", + "audio/ogg": ".ogg", + "audio/webm": ".webm", + "video/mp4": ".mp4", + "video/mpeg": ".mpeg", + "video/webm": ".webm", + + "text/plain": ".txt", + "text/html": ".html", + + "application/xml": ".xml", +} + +func ExtensionFromMimetype(mimetype string) string { + ext, ok := MimeExtensionSanityOverrides[strings.Split(mimetype, ";")[0]] + if !ok { + exts, _ := mime.ExtensionsByType(mimetype) + if len(exts) > 0 { + ext = exts[0] + } + } + return ext +}