Created
June 6, 2024 13:43
-
-
Save sonemaro/0cc48d3775fbd3fbb5027e3c42ab26fa to your computer and use it in GitHub Desktop.
FFMPEG + CUDA -> SUPER FAST VIDEO CONVERSION
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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