enki 76979d055b
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
Transcoding and Nip71 update
2025-08-21 19:32:26 -07:00

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)
}
}