Skip to content

Instantly share code, notes, and snippets.

@sonemaro
Created June 6, 2024 13:43
Show Gist options
  • Save sonemaro/0cc48d3775fbd3fbb5027e3c42ab26fa to your computer and use it in GitHub Desktop.
Save sonemaro/0cc48d3775fbd3fbb5027e3c42ab26fa to your computer and use it in GitHub Desktop.
FFMPEG + CUDA -> SUPER FAST VIDEO CONVERSION
package main
import (
"bytes"
"fmt"
"log"
"math"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"sync"
"time"
)
const (
defaultChunkFormat = "output_%03d"
)
func runCmd(cmdStr string) (string, error) {
cmd := exec.Command(strings.Split(cmdStr, " ")[0], strings.Split(cmdStr, " ")[1:]...)
var out bytes.Buffer
// Set the Stdout and Stderr of the command to the buffer
cmd.Stdout = &out
cmd.Stderr = &out
// Run the command
err := cmd.Run()
if err != nil {
return "", err
}
return out.String(), nil
}
// getDuration calculates the duration of a video using the ffprobe command.
//
// It takes a string parameter `video` which represents the path to the video file.
// The function returns a time.Duration representing the duration of the video.
func getDuration(video string) time.Duration {
c := fmt.Sprintf("ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 %s", video)
out, err := runCmd(c)
if err != nil {
return 0
}
secs := strings.Split(out, ".")
secsStr := secs[0]
duration, err := time.ParseDuration(fmt.Sprintf("%ss", secsStr))
if err != nil {
return 0
}
return duration
}
func calcChunkDuration(n int, d time.Duration) string {
res := "00:10:00"
if d.Seconds() < 60 {
res = fmt.Sprintf("00:00:%02d", int(d.Seconds()))
} else {
mins := math.Round(d.Minutes() / float64(n))
res = fmt.Sprintf("00:%02d:00", int(mins))
}
return res
}
// splitVideo splits a video into chunks of a given duration.
// command: ffmpeg -i input.mp4 -c copy -f segment -segment_time 00:10:00 -reset_timestamps 1 -force_key_frames "expr:gte(t,n_forced*10)" output_%03d.mp4
// ffmpeg will generate contents like this: [segment @ 0x5602ea2bd9c0] Opening 'output_001.mp4' for writingA speed=N/A
// later we will capture file names with this pattern: Opening '(.+?)' for writing
func splitVideo(input string, chuckDurationStr, outDir string, chuckNameFormat string) (parts []string, err error) {
outFile := fmt.Sprintf("%s/%s", outDir, chuckNameFormat)
c := fmt.Sprintf("ffmpeg -i %s -c copy -f segment -segment_time %s -reset_timestamps 1 -force_key_frames \"expr:gte(t,n_forced*10)\" %s.mp4", input, chuckDurationStr, outFile)
out, err := runCmd(c)
if err != nil {
return nil, err
}
re := regexp.MustCompile(`Opening '(.+?)' for writing`)
matches := re.FindAllStringSubmatch(out, -1)
var files []string
for _, match := range matches {
// the first submatch is the entire match, the second one is the captured group[Copilot]
if len(match) > 1 {
files = append(files, match[1])
}
}
if len(files) == 0 {
return nil, fmt.Errorf("no files created")
}
for i, f := range files {
fileName := filepath.Base(f)
files[i] = fmt.Sprintf("%s/%s", outDir, fileName)
}
return files, nil
}
// convert converts a video to h265
// command: ffmpeg -hwaccel cuda -i input.mp4 -c:v hevc_nvenc -preset p7 -tune hq -cq 28 -r 30 -s 1920x1080 -c:a copy output.mp4
func convert(videos []string, fps, w, h int, outDir string) (converted []string, err error) {
var wg sync.WaitGroup
for _, v := range videos {
wg.Add(1)
go func(v string) {
defer wg.Done()
fileName := filepath.Base(v)
o := fmt.Sprintf("%s/%s", outDir, strings.Replace(fileName, ".mp4", "_converted.mp4", -1))
c := fmt.Sprintf("ffmpeg -hwaccel cuda -i %s -c:v hevc_nvenc -preset p7 -tune hq -cq 28 -r %d -s %dx%d -c:a copy %s", v, fps, w, h, o)
_, err = runCmd(c)
if err != nil {
log.Println(err)
return
}
converted = append(converted, o)
}(v)
}
wg.Wait()
return
}
func merge(videos []string, outDir string, outName string) (final string, err error) {
fName := fmt.Sprintf("%s/%s", outDir, "filelist.txt")
err = os.Truncate(fName, 0)
if err != nil {
return "", err
}
fl, err := os.OpenFile(fName, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
defer fl.Close()
if err != nil {
return "", err
}
for _, v := range videos {
fl.WriteString(fmt.Sprintf("file %s\n", v))
}
c := fmt.Sprintf("ffmpeg -f concat -safe 0 -i %s/%s -c copy %s/%s", outDir, "filelist.txt", outDir, outName)
_, err = runCmd(c)
if err != nil {
return "", err
}
return fmt.Sprintf("%s/%s", outDir, outName), nil
}
func main() {
f := "/home/soroush/videos/dota2_lycan_gameplay.mp4"
d := getDuration(f)
dur := calcChunkDuration(4, d)
videos, err := splitVideo(f, dur, "/home/soroush/projects/video", defaultChunkFormat)
fmt.Println(videos, err)
converted, err := convert(videos, 30, 1920, 1080, "/home/soroush/projects/video")
if err != nil {
fmt.Println(converted, err)
panic(err)
}
fmt.Println(converted)
final, err := merge(converted, "/home/soroush/projects/video", "merged_output.mp4")
if err != nil {
panic(err)
}
fmt.Println(final)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment