package media import ( "bytes" "context" "errors" "os" "os/exec" "strings" "github.com/flowy-live/llink/internal/utils/flog" ) 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 OutputExt string // e.g. ".m4a" or ".mp4" } var ( ErrInvalidInput error = errors.New("invalid input") ) // TranscodeToMp4 writes the result to a temp file; caller is 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 { flog.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 { // 1440p–4K screen recordings + libx264's lookahead buffers can OOM the // worker. Bound parallelism/lookahead and downscale to 1080p; 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 }