Some checks are pending
CI Pipeline / Run Tests (push) Waiting to run
CI Pipeline / Lint Code (push) Waiting to run
CI Pipeline / Security Scan (push) Waiting to run
CI Pipeline / Build Docker Images (push) Blocked by required conditions
CI Pipeline / E2E Tests (push) Blocked by required conditions
236 lines
6.1 KiB
Go
236 lines
6.1 KiB
Go
package transcoding
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Quality represents a transcoding quality profile
|
|
type Quality struct {
|
|
Name string `json:"name"`
|
|
Width int `json:"width"`
|
|
Height int `json:"height"`
|
|
Bitrate string `json:"bitrate"`
|
|
Preset string `json:"preset"`
|
|
}
|
|
|
|
// Job represents a transcoding job
|
|
type Job struct {
|
|
ID string `json:"id"`
|
|
InputPath string `json:"input_path"`
|
|
OutputDir string `json:"output_dir"`
|
|
FileHash string `json:"file_hash"`
|
|
Qualities []Quality `json:"qualities"`
|
|
Priority int `json:"priority"`
|
|
Status string `json:"status"` // "queued", "processing", "completed", "failed"
|
|
Progress float64 `json:"progress"`
|
|
Error string `json:"error,omitempty"`
|
|
StartTime time.Time `json:"start_time"`
|
|
CompletedAt time.Time `json:"completed_at,omitempty"`
|
|
Callback func(error) `json:"-"`
|
|
}
|
|
|
|
// Transcoder handles video transcoding operations
|
|
type Transcoder struct {
|
|
workDir string
|
|
ffmpegPath string
|
|
concurrent int
|
|
queue chan Job
|
|
jobs map[string]*Job // Track job status
|
|
enabled bool
|
|
}
|
|
|
|
// DefaultQualities provides standard quality profiles
|
|
var DefaultQualities = []Quality{
|
|
{Name: "1080p", Width: 1920, Height: 1080, Bitrate: "5000k", Preset: "fast"},
|
|
{Name: "720p", Width: 1280, Height: 720, Bitrate: "2500k", Preset: "fast"},
|
|
{Name: "480p", Width: 854, Height: 480, Bitrate: "1000k", Preset: "fast"},
|
|
}
|
|
|
|
// NewTranscoder creates a new transcoder instance
|
|
func NewTranscoder(workDir string, concurrent int, enabled bool) (*Transcoder, error) {
|
|
if !enabled {
|
|
return &Transcoder{
|
|
enabled: false,
|
|
jobs: make(map[string]*Job),
|
|
}, nil
|
|
}
|
|
|
|
// Verify FFmpeg is available
|
|
if _, err := exec.LookPath("ffmpeg"); err != nil {
|
|
return nil, fmt.Errorf("ffmpeg not found in PATH: %w", err)
|
|
}
|
|
|
|
// Create work directory if it doesn't exist
|
|
if err := os.MkdirAll(workDir, 0755); err != nil {
|
|
return nil, fmt.Errorf("failed to create work directory: %w", err)
|
|
}
|
|
|
|
t := &Transcoder{
|
|
workDir: workDir,
|
|
ffmpegPath: "ffmpeg",
|
|
concurrent: concurrent,
|
|
queue: make(chan Job, 100),
|
|
jobs: make(map[string]*Job),
|
|
enabled: true,
|
|
}
|
|
|
|
// Start worker goroutines
|
|
for i := 0; i < concurrent; i++ {
|
|
go t.worker()
|
|
}
|
|
|
|
return t, nil
|
|
}
|
|
|
|
// IsEnabled returns whether transcoding is enabled
|
|
func (t *Transcoder) IsEnabled() bool {
|
|
return t.enabled
|
|
}
|
|
|
|
// worker processes jobs from the queue
|
|
func (t *Transcoder) worker() {
|
|
for job := range t.queue {
|
|
t.processJob(job)
|
|
}
|
|
}
|
|
|
|
// SubmitJob adds a job to the transcoding queue
|
|
func (t *Transcoder) SubmitJob(job Job) {
|
|
if !t.enabled {
|
|
if job.Callback != nil {
|
|
job.Callback(fmt.Errorf("transcoding is disabled"))
|
|
}
|
|
return
|
|
}
|
|
|
|
job.Status = "queued"
|
|
job.StartTime = time.Now()
|
|
t.jobs[job.ID] = &job
|
|
t.queue <- job
|
|
}
|
|
|
|
// GetJobStatus returns the current status of a job
|
|
func (t *Transcoder) GetJobStatus(jobID string) (*Job, bool) {
|
|
job, exists := t.jobs[jobID]
|
|
return job, exists
|
|
}
|
|
|
|
// NeedsTranscoding checks if a file needs transcoding for web compatibility
|
|
func (t *Transcoder) NeedsTranscoding(filePath string) (bool, error) {
|
|
if !t.enabled {
|
|
return false, nil
|
|
}
|
|
|
|
// Use ffprobe to analyze the file
|
|
cmd := exec.Command("ffprobe", "-v", "quiet", "-print_format", "json", "-show_format", "-show_streams", filePath)
|
|
output, err := cmd.Output()
|
|
if err != nil {
|
|
return true, err // Assume needs transcoding if we can't analyze
|
|
}
|
|
|
|
outputStr := string(output)
|
|
|
|
// Check if already in web-friendly format
|
|
hasH264 := strings.Contains(outputStr, "\"codec_name\": \"h264\"")
|
|
hasAAC := strings.Contains(outputStr, "\"codec_name\": \"aac\"")
|
|
isMP4 := strings.HasSuffix(strings.ToLower(filePath), ".mp4")
|
|
|
|
// If it's H.264/AAC in MP4 container, probably doesn't need transcoding
|
|
if hasH264 && hasAAC && isMP4 {
|
|
return false, nil
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
// CreateStreamingVersion creates a single web-compatible MP4 version
|
|
func (t *Transcoder) CreateStreamingVersion(inputPath, outputPath string) error {
|
|
if !t.enabled {
|
|
return fmt.Errorf("transcoding is disabled")
|
|
}
|
|
|
|
// Ensure output directory exists
|
|
if err := os.MkdirAll(filepath.Dir(outputPath), 0755); err != nil {
|
|
return fmt.Errorf("failed to create output directory: %w", err)
|
|
}
|
|
|
|
args := []string{
|
|
"-i", inputPath,
|
|
"-c:v", "libx264", // H.264 video codec
|
|
"-c:a", "aac", // AAC audio codec
|
|
"-preset", "fast", // Balance speed/quality
|
|
"-crf", "23", // Quality (23 is default, lower = better)
|
|
"-maxrate", "2M", // Max bitrate for streaming
|
|
"-bufsize", "4M", // Buffer size
|
|
"-movflags", "+faststart", // Enable fast start for web
|
|
"-y", // Overwrite output file
|
|
outputPath,
|
|
}
|
|
|
|
cmd := exec.Command(t.ffmpegPath, args...)
|
|
return cmd.Run()
|
|
}
|
|
|
|
// processJob handles the actual transcoding work
|
|
func (t *Transcoder) processJob(job Job) {
|
|
job.Status = "processing"
|
|
t.jobs[job.ID] = &job
|
|
|
|
var err error
|
|
defer func() {
|
|
if err != nil {
|
|
job.Status = "failed"
|
|
job.Error = err.Error()
|
|
} else {
|
|
job.Status = "completed"
|
|
job.Progress = 100.0
|
|
}
|
|
job.CompletedAt = time.Now()
|
|
t.jobs[job.ID] = &job
|
|
|
|
if job.Callback != nil {
|
|
job.Callback(err)
|
|
}
|
|
}()
|
|
|
|
// Create output directory
|
|
if err = os.MkdirAll(job.OutputDir, 0755); err != nil {
|
|
err = fmt.Errorf("failed to create output directory: %w", err)
|
|
return
|
|
}
|
|
|
|
// Create streaming MP4 version (most important for web compatibility)
|
|
outputPath := filepath.Join(job.OutputDir, "stream.mp4")
|
|
err = t.CreateStreamingVersion(job.InputPath, outputPath)
|
|
if err != nil {
|
|
err = fmt.Errorf("transcoding failed: %w", err)
|
|
return
|
|
}
|
|
|
|
job.Progress = 100.0
|
|
}
|
|
|
|
// GetTranscodedPath returns the path to a transcoded file if it exists
|
|
func (t *Transcoder) GetTranscodedPath(fileHash string) string {
|
|
if !t.enabled {
|
|
return ""
|
|
}
|
|
|
|
streamPath := filepath.Join(t.workDir, fileHash, "stream.mp4")
|
|
if _, err := os.Stat(streamPath); err == nil {
|
|
return streamPath
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// Close shuts down the transcoder
|
|
func (t *Transcoder) Close() {
|
|
if t.enabled && t.queue != nil {
|
|
close(t.queue)
|
|
}
|
|
} |