mirror of
https://github.com/mickael-kerjean/filestash.git
synced 2026-10-20 08:16:40 +00:00
134 lines
3.5 KiB
Go
134 lines
3.5 KiB
Go
package ffmpeg
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os/exec"
|
|
"strconv"
|
|
|
|
. "github.com/mickael-kerjean/filestash/server/common"
|
|
)
|
|
|
|
func transcodeVideoSegment(ctx context.Context, cachePath string, segmentNumber int, w io.Writer) error {
|
|
start := segmentNumber * HLS_VIDEO_SEGMENT_LENGTH
|
|
args := []string{
|
|
"-hide_banner", "-loglevel", "error",
|
|
"-timelimit", "30",
|
|
"-ss", fmt.Sprintf("%d.00", start),
|
|
"-i", cachePath,
|
|
"-t", fmt.Sprintf("%d.00", HLS_VIDEO_SEGMENT_LENGTH),
|
|
"-an", "-sn",
|
|
"-force_key_frames", fmt.Sprintf("expr:gte(t,n_forced*%d.000)", HLS_VIDEO_SEGMENT_LENGTH),
|
|
"-fps_mode", "cfr",
|
|
"-output_ts_offset", fmt.Sprintf("%d.00", start),
|
|
}
|
|
|
|
switch ENCODER {
|
|
case "h264_vaapi":
|
|
args = append([]string{
|
|
"-init_hw_device", "vaapi=va:/dev/dri/renderD128",
|
|
"-filter_hw_device", "va",
|
|
}, args...)
|
|
args = append(args,
|
|
"-vf", fmt.Sprintf("format=nv12,hwupload,scale_vaapi=w=-2:h=%d", VIDEO_MAX_HEIGHT),
|
|
"-c:v", "h264_vaapi",
|
|
)
|
|
case "h264_nvenc":
|
|
args = append([]string{
|
|
"-init_hw_device", "cuda=hw",
|
|
"-filter_hw_device", "hw",
|
|
}, args...)
|
|
args = append(args,
|
|
"-vf", fmt.Sprintf("format=nv12,hwupload_cuda,scale_cuda=w=-2:h=%d", VIDEO_MAX_HEIGHT),
|
|
"-c:v", "h264_nvenc",
|
|
)
|
|
case "h264_v4l2m2m":
|
|
args = append(args,
|
|
"-vf", fmt.Sprintf("scale=-2:%d,format=yuv420p", VIDEO_MAX_HEIGHT),
|
|
"-c:v", "h264_v4l2m2m",
|
|
"-b:v", "2500k",
|
|
"-num_output_buffers", "32",
|
|
"-num_capture_buffers", "32",
|
|
)
|
|
case "libx264":
|
|
args = append(args,
|
|
"-vf", fmt.Sprintf("scale=-2:%d,format=yuv420p", VIDEO_MAX_HEIGHT),
|
|
"-c:v", "libx264", "-preset", "veryfast",
|
|
"-x264opts", "subme=0:me_range=4:rc_lookahead=10:me=dia:no_chroma_me:8x8dct=0:partitions=none",
|
|
)
|
|
default:
|
|
return ErrNotImplemented
|
|
}
|
|
|
|
args = append(args, "-f", "mpegts", "pipe:1")
|
|
return runFFmpeg(ctx, args, w)
|
|
}
|
|
|
|
func transcodeAudioSegment(ctx context.Context, cachePath string, segmentNumber int, w io.Writer) error {
|
|
start := segmentNumber * HLS_AUDIO_SEGMENT_LENGTH
|
|
args := []string{
|
|
"-hide_banner", "-loglevel", "error",
|
|
"-timelimit", "30",
|
|
"-ss", fmt.Sprintf("%d.00", start),
|
|
"-i", cachePath,
|
|
"-t", fmt.Sprintf("%d.00", HLS_AUDIO_SEGMENT_LENGTH),
|
|
"-vn", "-sn",
|
|
"-c:a", "aac", "-b:a", strconv.Itoa(AUDIO_BITRATE),
|
|
"-output_ts_offset", fmt.Sprintf("%d.00", start),
|
|
"-f", "mpegts", "pipe:1",
|
|
}
|
|
return runFFmpeg(ctx, args, w)
|
|
}
|
|
|
|
func runFFmpeg(ctx context.Context, args []string, w io.Writer) error {
|
|
cmd := exec.CommandContext(ctx, "ffmpeg", args...)
|
|
cmd.Stdout = w
|
|
stderr, err := cmd.StderrPipe()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := cmd.Start(); err != nil {
|
|
return fmt.Errorf("ffmpeg start: %w", err)
|
|
}
|
|
msg, _ := io.ReadAll(stderr)
|
|
if err := cmd.Wait(); err != nil {
|
|
if ctx.Err() != nil {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("ffmpeg: %w: %s", err, msg)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func probeDuration(path string) (float64, error) {
|
|
out, err := exec.Command(
|
|
"ffprobe",
|
|
"-v", "error",
|
|
"-show_entries", "format=duration",
|
|
"-of", "json",
|
|
path,
|
|
).Output()
|
|
if err != nil {
|
|
return 0, fmt.Errorf("ffprobe: %w", err)
|
|
}
|
|
var parsed struct {
|
|
Format struct {
|
|
Duration string `json:"duration"`
|
|
} `json:"format"`
|
|
}
|
|
if err := json.Unmarshal(out, &parsed); err != nil {
|
|
return 0, fmt.Errorf("ffprobe parse: %w", err)
|
|
}
|
|
if parsed.Format.Duration == "" {
|
|
return 0, errors.New("ffprobe: no duration")
|
|
}
|
|
d, err := strconv.ParseFloat(parsed.Format.Duration, 64)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("ffprobe duration: %w", err)
|
|
}
|
|
return d, nil
|
|
}
|