168 lines
4.3 KiB
Go
168 lines
4.3 KiB
Go
package encoder
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"videnc-vibe/pkg/types"
|
|
)
|
|
|
|
type Encoder struct {
|
|
ffmpegPath string
|
|
opusencPath string
|
|
}
|
|
|
|
func New() *Encoder {
|
|
return &Encoder{
|
|
ffmpegPath: "ffmpeg",
|
|
opusencPath: "opusenc",
|
|
}
|
|
}
|
|
|
|
func (e *Encoder) CheckDeps() error {
|
|
if _, err := exec.LookPath(e.ffmpegPath); err != nil {
|
|
return fmt.Errorf("ffmpeg not found")
|
|
}
|
|
if _, err := exec.LookPath(e.opusencPath); err != nil {
|
|
return fmt.Errorf("opusenc not found")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (e *Encoder) GetMediaInfo(path string) (width, height int, interlaced bool, err error) {
|
|
cmd := exec.Command(e.ffmpegPath, "-i", path, "-hide_banner", "-vf", "idet")
|
|
output, _ := cmd.StderrPipe()
|
|
cmd.Start()
|
|
|
|
buf := make([]byte, 16384)
|
|
n, _ := output.Read(buf)
|
|
output.Close()
|
|
|
|
var w, h int
|
|
fmt.Sscanf(string(buf[:n]), "%dx%d", &w, &h)
|
|
|
|
interlaced = strings.Contains(string(buf[:n]), "TFF") || strings.Contains(string(buf[:n]), "BFF")
|
|
|
|
return w, h, interlaced, nil
|
|
}
|
|
|
|
func (e *Encoder) Transcode(input string, job *types.Job, metadata *types.Metadata, interlaced bool) error {
|
|
dir := filepath.Dir(input)
|
|
audioWavs, err := e.extractAudio(input, dir)
|
|
if err != nil {
|
|
return fmt.Errorf("extracting audio: %w", err)
|
|
}
|
|
defer e.cleanupWavs(audioWavs)
|
|
|
|
opusFiles, err := e.encodeOpus(audioWavs, dir)
|
|
if err != nil {
|
|
return fmt.Errorf("encoding opus: %w", err)
|
|
}
|
|
defer e.cleanupOpus(opusFiles)
|
|
|
|
if err := e.encodeVideo(input, opusFiles, job, metadata, interlaced); err != nil {
|
|
return fmt.Errorf("encoding video: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (e *Encoder) extractAudio(input, dir string) ([]string, error) {
|
|
cmd := exec.Command(e.ffmpegPath, "-i", input,
|
|
"-vn", "-c:a", "pcm_s16le", "-ar", "48000",
|
|
"-f", "wav", filepath.Join(dir, "audio_%d.wav"))
|
|
if out, err := cmd.CombinedOutput(); err != nil {
|
|
return nil, fmt.Errorf("ffmpeg extract: %s %w", out, err)
|
|
}
|
|
|
|
matches, _ := filepath.Glob(filepath.Join(dir, "audio_*.wav"))
|
|
return matches, nil
|
|
}
|
|
|
|
func (e *Encoder) encodeOpus(wavs []string, dir string) ([]string, error) {
|
|
var opusFiles []string
|
|
for _, wav := range wavs {
|
|
out := strings.TrimSuffix(wav, ".wav") + ".opus"
|
|
cmd := exec.Command(e.opusencPath, "--bitrate", "128k", wav, out)
|
|
if out, err := cmd.CombinedOutput(); err != nil {
|
|
return nil, fmt.Errorf("opusenc: %s %w", out, err)
|
|
}
|
|
opusFiles = append(opusFiles, out)
|
|
}
|
|
return opusFiles, nil
|
|
}
|
|
|
|
func (e *Encoder) encodeVideo(input string, opusFiles []string, job *types.Job, metadata *types.Metadata, interlaced bool) error {
|
|
dir := filepath.Dir(input)
|
|
outFile := filepath.Join(dir, "output.mkv")
|
|
|
|
svtParams := fmt.Sprintf("film-grain=10:film-grain-denoise=1:scd=1:qm-min=4:qm-max=15:preset=%d", job.Preset)
|
|
|
|
vf := "zscale=filter=spline36:mode=16:9"
|
|
if interlaced {
|
|
vf = "bwdif=mode=0:par=-1:-1," + vf
|
|
}
|
|
|
|
args := []string{
|
|
"-y",
|
|
"-i", input,
|
|
"-c:v", "libsvtav1",
|
|
"-crf", strconv.Itoa(job.CRF),
|
|
"-svtav1-params", svtParams,
|
|
"-pix_fmt", "yuv420p10le",
|
|
"-vf", vf,
|
|
"-c:a", "copy",
|
|
"-c:s", "copy",
|
|
}
|
|
|
|
for _, opus := range opusFiles {
|
|
args = append(args, "-i", opus)
|
|
}
|
|
|
|
args = append(args, "-map", "0:v")
|
|
args = append(args, "-map", "0:s?")
|
|
|
|
for i := range opusFiles {
|
|
args = append(args, "-map", fmt.Sprintf("%d:a", i+1))
|
|
}
|
|
|
|
args = append(args, "-metadata", fmt.Sprintf("TITLE=%s", metadata.Title))
|
|
args = append(args, "-metadata", fmt.Sprintf("DATE_RELEASED=%s", metadata.DateReleased))
|
|
args = append(args, "-metadata", fmt.Sprintf("IMDBID=%s", metadata.IMDBID))
|
|
args = append(args, "-metadata", fmt.Sprintf("ORIGINAL_MEDIA_TYPE=%s", metadata.OriginalMedia))
|
|
|
|
if metadata.IsSeries {
|
|
args = append(args, "-metadata", fmt.Sprintf("COLLECTION=%s", metadata.Collection))
|
|
args = append(args, "-metadata", fmt.Sprintf("SEASON=%s", metadata.Season))
|
|
args = append(args, "-metadata", fmt.Sprintf("EPISODE=%s", metadata.Episode))
|
|
if metadata.TVMazeID != "" {
|
|
args = append(args, "-metadata", fmt.Sprintf("TVMAZE_ID=%s", metadata.TVMazeID))
|
|
}
|
|
}
|
|
|
|
args = append(args, outFile)
|
|
|
|
cmd := exec.Command(e.ffmpegPath, args...)
|
|
if out, err := cmd.CombinedOutput(); err != nil {
|
|
return fmt.Errorf("ffmpeg encode: %s %w", out, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (e *Encoder) cleanupWavs(files []string) {
|
|
for _, f := range files {
|
|
os.Remove(f)
|
|
}
|
|
}
|
|
|
|
func (e *Encoder) cleanupOpus(files []string) {
|
|
for _, f := range files {
|
|
os.Remove(f)
|
|
}
|
|
}
|