feat: initialise project structure with core encoding queue, database models, and web management API
Build / Build and Push (push) Successful in 50s
Build / Build and Push (push) Successful in 50s
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
package encoder
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (m *FFmpegManager) BuildAudioCmd(inputPath, outputPath, ffmpegFlags string) (*exec.Cmd, error) {
|
||||
args := []string{"-i", inputPath, "-c:a", "libmp3lame", "-b:a", "320k"}
|
||||
|
||||
if ffmpegFlags != "" {
|
||||
userArgs := ShlexSplit(ffmpegFlags)
|
||||
args = append(args, userArgs...)
|
||||
}
|
||||
|
||||
args = append(args, outputPath, "-y")
|
||||
|
||||
return exec.Command(m.BinaryPath, args...), nil
|
||||
}
|
||||
|
||||
func (m *FFmpegManager) ShouldSkipAudio(inputPath string) bool {
|
||||
// Probe the codec
|
||||
cmd := exec.Command("ffprobe", "-v", "quiet", "-select_streams", "a:0",
|
||||
"-show_entries", "stream=codec_name", "-of", "csv=p=0", inputPath)
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
codec := strings.TrimSpace(string(out))
|
||||
|
||||
// If it's already mp3, check bitrate
|
||||
if codec == "mp3" {
|
||||
cmdBr := exec.Command("ffprobe", "-v", "quiet", "-select_streams", "a:0",
|
||||
"-show_entries", "stream=bit_rate", "-of", "csv=p=0", inputPath)
|
||||
outBr, err := cmdBr.Output()
|
||||
if err == nil {
|
||||
br, _ := strconv.Atoi(strings.TrimSpace(string(outBr)))
|
||||
if br >= 300000 {
|
||||
return true // It's ~320k mp3
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package encoder
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// FFmpegManager handles execution and probing
|
||||
type FFmpegManager struct {
|
||||
BinaryPath string
|
||||
}
|
||||
|
||||
func NewManager(binaryPath string) *FFmpegManager {
|
||||
if binaryPath == "" {
|
||||
binaryPath = "ffmpeg"
|
||||
}
|
||||
return &FFmpegManager{BinaryPath: binaryPath}
|
||||
}
|
||||
|
||||
func (m *FFmpegManager) ProbeCodec(filePath, streamType string) (string, error) {
|
||||
cmd := exec.Command("ffprobe", "-v", "quiet", "-select_streams", streamType+":0",
|
||||
"-show_entries", "stream=codec_name", "-of", "csv=p=0", filePath)
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("ffprobe error: %w", err)
|
||||
}
|
||||
return strings.TrimSpace(string(out)), nil
|
||||
}
|
||||
|
||||
func (m *FFmpegManager) ProbeResolution(filePath string) (int, int, error) {
|
||||
cmd := exec.Command("ffprobe", "-v", "quiet", "-select_streams", "v:0",
|
||||
"-show_entries", "stream=width,height", "-of", "csv=p=0", filePath)
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("ffprobe error: %w", err)
|
||||
}
|
||||
parts := strings.Split(strings.TrimSpace(string(out)), ",")
|
||||
if len(parts) != 2 {
|
||||
return 0, 0, fmt.Errorf("invalid resolution format")
|
||||
}
|
||||
w, err1 := strconv.Atoi(parts[0])
|
||||
h, err2 := strconv.Atoi(parts[1])
|
||||
if err1 != nil || err2 != nil {
|
||||
return 0, 0, fmt.Errorf("invalid resolution numbers")
|
||||
}
|
||||
return w, h, nil
|
||||
}
|
||||
|
||||
func (m *FFmpegManager) ProbeDuration(filePath string) (float64, error) {
|
||||
cmd := exec.Command("ffprobe", "-v", "quiet", "-show_entries", "format=duration", "-of", "csv=p=0", filePath)
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("ffprobe error: %w", err)
|
||||
}
|
||||
return strconv.ParseFloat(strings.TrimSpace(string(out)), 64)
|
||||
}
|
||||
|
||||
func (m *FFmpegManager) ValidateFile(filePath string) error {
|
||||
cmd := exec.Command("ffprobe", "-v", "quiet", "-show_format", "-show_streams", filePath)
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("file validation failed: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ParseProgress is a utility to parse time=XX:XX:XX.XX from stderr line to calculate percentage if duration is known.
|
||||
// Will be used by the queue worker reading from a pipe.
|
||||
func ParseProgress(line string, durationSec float64) float64 {
|
||||
// Example line: frame= 123 fps= 30 q=22.0 size= 2048kB time=00:00:10.50 bitrate=1500.0kbits/s speed=1.5x
|
||||
idx := strings.Index(line, "time=")
|
||||
if idx == -1 {
|
||||
return -1
|
||||
}
|
||||
timeStr := line[idx+5:]
|
||||
spaceIdx := strings.Index(timeStr, " ")
|
||||
if spaceIdx != -1 {
|
||||
timeStr = timeStr[:spaceIdx]
|
||||
}
|
||||
|
||||
// timeStr is HH:MM:SS.ms
|
||||
parts := strings.Split(timeStr, ":")
|
||||
if len(parts) != 3 {
|
||||
return -1
|
||||
}
|
||||
|
||||
h, _ := strconv.ParseFloat(parts[0], 64)
|
||||
m, _ := strconv.ParseFloat(parts[1], 64)
|
||||
s, _ := strconv.ParseFloat(parts[2], 64)
|
||||
|
||||
currentSec := h*3600 + m*60 + s
|
||||
if durationSec > 0 {
|
||||
return (currentSec / durationSec) * 100.0
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// ShlexSplit splits a command string like a shell would.
|
||||
// A simple implementation since go doesn't have shlex.split built-in.
|
||||
func ShlexSplit(s string) []string {
|
||||
var args []string
|
||||
var buf bytes.Buffer
|
||||
inQuotes := false
|
||||
var quoteChar rune
|
||||
|
||||
for _, r := range s {
|
||||
if inQuotes {
|
||||
if r == quoteChar {
|
||||
inQuotes = false
|
||||
} else {
|
||||
buf.WriteRune(r)
|
||||
}
|
||||
} else {
|
||||
if r == '\'' || r == '"' {
|
||||
inQuotes = true
|
||||
quoteChar = r
|
||||
} else if r == ' ' {
|
||||
if buf.Len() > 0 {
|
||||
args = append(args, buf.String())
|
||||
buf.Reset()
|
||||
}
|
||||
} else {
|
||||
buf.WriteRune(r)
|
||||
}
|
||||
}
|
||||
}
|
||||
if buf.Len() > 0 {
|
||||
args = append(args, buf.String())
|
||||
}
|
||||
return args
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package encoder
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (m *FFmpegManager) BuildVideoCmd(inputPath, outputPath, targetResolution, ffmpegFlags string, originalWidth, originalHeight int) (*exec.Cmd, error) {
|
||||
args := []string{"-i", inputPath}
|
||||
|
||||
if targetResolution != "" {
|
||||
targetHeightStr := strings.TrimRight(strings.ToLower(targetResolution), "p")
|
||||
targetHeight, err := strconv.Atoi(targetHeightStr)
|
||||
if err == nil && targetHeight > 0 && originalHeight > targetHeight {
|
||||
scaleWidth := int((float64(originalWidth) / float64(originalHeight)) * float64(targetHeight))
|
||||
if scaleWidth%2 != 0 {
|
||||
scaleWidth--
|
||||
}
|
||||
args = append(args, "-vf", fmt.Sprintf("scale=%d:%d", scaleWidth, targetHeight))
|
||||
}
|
||||
}
|
||||
|
||||
args = append(args, "-c:v", "libx265", "-c:a", "copy", "-preset", "medium", "-crf", "22", "-f", "matroska")
|
||||
|
||||
if ffmpegFlags != "" {
|
||||
userArgs := ShlexSplit(ffmpegFlags)
|
||||
args = append(args, userArgs...)
|
||||
}
|
||||
|
||||
args = append(args, outputPath, "-y")
|
||||
|
||||
return exec.Command(m.BinaryPath, args...), nil
|
||||
}
|
||||
|
||||
func (m *FFmpegManager) ShouldSkipVideo(inputPath, targetResolution string) bool {
|
||||
// Probe the codec
|
||||
cmd := exec.Command("ffprobe", "-v", "quiet", "-select_streams", "v:0",
|
||||
"-show_entries", "stream=codec_name", "-of", "csv=p=0", inputPath)
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
codec := strings.TrimSpace(string(out))
|
||||
|
||||
// Probe the height
|
||||
cmdHeight := exec.Command("ffprobe", "-v", "quiet", "-select_streams", "v:0",
|
||||
"-show_entries", "stream=height", "-of", "csv=p=0", inputPath)
|
||||
outHeight, err := cmdHeight.Output()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// If it's not H.264 or H.265, do not skip (encode it)
|
||||
if codec != "h264" && codec != "hevc" {
|
||||
return false
|
||||
}
|
||||
|
||||
// If it's H.265 and no target resolution is set, it's already in the target codec
|
||||
if codec == "hevc" && targetResolution == "" {
|
||||
return true
|
||||
}
|
||||
|
||||
// If it's H.265 and target resolution is set, only skip if current height is <= target height
|
||||
if codec == "hevc" && targetResolution != "" {
|
||||
currentHeight, _ := strconv.Atoi(strings.TrimSpace(string(outHeight)))
|
||||
targetHeight, _ := strconv.Atoi(strings.TrimRight(strings.ToLower(targetResolution), "p"))
|
||||
if currentHeight > 0 && targetHeight > 0 && currentHeight <= targetHeight {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user