refactor: abstraction for transcoding
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
package media
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func IsIOSPlayableMime(mime string) bool {
|
||||
switch mime {
|
||||
case "video/mp4", "video/quicktime", "audio/mp4", "audio/aac", "audio/x-m4a", "audio/mpeg":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func IsAudio(mime string) bool {
|
||||
return strings.HasPrefix(mime, "audio/")
|
||||
}
|
||||
|
||||
type TranscodeInput struct {
|
||||
SourceURL string
|
||||
MimeType string
|
||||
}
|
||||
|
||||
type TranscodeOutput struct {
|
||||
TempLocalFilePath string
|
||||
OutputMimeType string
|
||||
// Extension such as ".m4a" or ".mp4"
|
||||
OutputExt string
|
||||
}
|
||||
|
||||
var (
|
||||
ErrInvalidInput error = errors.New("invalid input")
|
||||
)
|
||||
|
||||
// TranscodeToMp4 takes in any audio or video source URL and
|
||||
// returns the filepath of the transcoded media
|
||||
// WARNING: caller responsible for deleting TempLocalFilePath
|
||||
func TranscodeToMp4(ctx context.Context, input TranscodeInput) (*TranscodeOutput, error) {
|
||||
if input.SourceURL == "" || input.MimeType == "" {
|
||||
return nil, ErrInvalidInput
|
||||
}
|
||||
|
||||
isAudio := IsAudio(input.MimeType)
|
||||
var outputExt, outputMime string
|
||||
if isAudio {
|
||||
outputExt = ".m4a"
|
||||
outputMime = "audio/mp4"
|
||||
} else {
|
||||
outputExt = ".mp4"
|
||||
outputMime = "video/mp4"
|
||||
}
|
||||
|
||||
tmp, err := os.CreateTemp("", "transcode-*"+outputExt)
|
||||
if err != nil {
|
||||
slog.Error("transcode: failed to create temp file", "error", err)
|
||||
return nil, err
|
||||
}
|
||||
tmpPath := tmp.Name()
|
||||
tmp.Close()
|
||||
|
||||
var args []string
|
||||
if isAudio {
|
||||
args = []string{
|
||||
"-y", "-i", input.SourceURL,
|
||||
"-vn",
|
||||
"-c:a", "aac", "-b:a", "128k",
|
||||
"-movflags", "+faststart",
|
||||
tmpPath,
|
||||
}
|
||||
} else {
|
||||
// Cap encoder parallelism and lookahead to keep memory bounded — screen
|
||||
// recordings come in at native display resolution (often 1440p–4K) and
|
||||
// libx264's per-thread lookahead/reference buffers blow past the worker's
|
||||
// memory limit otherwise. Output is also downscaled to 1080p max, which
|
||||
// mobile playback won't notice; the original WebM stays in GCS untouched.
|
||||
args = []string{
|
||||
"-y", "-i", input.SourceURL,
|
||||
"-vf", "scale='min(1920,iw)':-2:flags=lanczos",
|
||||
"-c:v", "libx264", "-preset", "veryfast", "-crf", "23",
|
||||
"-pix_fmt", "yuv420p", "-profile:v", "baseline", "-level", "3.1",
|
||||
"-x264-params", "rc-lookahead=20:ref=2",
|
||||
"-threads", "2", "-filter_threads", "2",
|
||||
"-c:a", "aac", "-b:a", "128k",
|
||||
"-movflags", "+faststart",
|
||||
tmpPath,
|
||||
}
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx, "ffmpeg", args...)
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stderr = &stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return nil, errors.Join(err, errors.New(stderr.String()))
|
||||
}
|
||||
|
||||
return &TranscodeOutput{
|
||||
TempLocalFilePath: tmpPath,
|
||||
OutputMimeType: outputMime,
|
||||
OutputExt: outputExt,
|
||||
}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user