From 9677fda9bd66ba51a820f3a83df4aec2ea4f05f7 Mon Sep 17 00:00:00 2001 From: mowshon Date: Mon, 18 Apr 2022 17:53:36 +0300 Subject: [PATCH] Upload local files --- .gitignore | 9 ++ README.md | 5 + go.mod | 16 +++ go.sum | 68 ++++++++++++ helpers.go | 22 ++++ moviego.go | 311 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 431 insertions(+) create mode 100644 README.md create mode 100644 go.mod create mode 100644 go.sum create mode 100644 helpers.go create mode 100644 moviego.go diff --git a/.gitignore b/.gitignore index 66fd13c..1490c48 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,12 @@ # Dependency directories (remove the comment below to include it) # vendor/ + +.idea/ +*.mp4 +*.mp3 +*.png + +/*/*.mp4 +/*/*.mp3 +/*/*.png \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..8969866 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +## MovieGo 📽 + +--- + +MovieGo is a Golang library for video editing. The library is designed for fast processing of routine tasks related to video editing. \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..093f34a --- /dev/null +++ b/go.mod @@ -0,0 +1,16 @@ +module moviego + +go 1.18 + +require ( + github.com/tidwall/gjson v1.14.0 + github.com/u2takey/ffmpeg-go v0.4.1 +) + +require ( + github.com/aws/aws-sdk-go v1.38.20 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect + github.com/u2takey/go-utils v0.3.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..26efb08 --- /dev/null +++ b/go.sum @@ -0,0 +1,68 @@ +github.com/aws/aws-sdk-go v1.38.20 h1:QbzNx/tdfATbdKfubBpkt84OM6oBkxQZRw6+bW2GyeA= +github.com/aws/aws-sdk-go v1.38.20/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= +github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/panjf2000/ants/v2 v2.4.2/go.mod h1:f6F0NZVFsGCp5A7QW/Zj/m92atWwOkY0OIhFxRNFr4A= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/tidwall/gjson v1.14.0 h1:6aeJ0bzojgWLa82gDQHcx3S0Lr/O51I9bJ5nv6JFx5w= +github.com/tidwall/gjson v1.14.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/u2takey/ffmpeg-go v0.4.1 h1:l5ClIwL3N2LaH1zF3xivb3kP2HW95eyG5xhHE1JdZ9Y= +github.com/u2takey/ffmpeg-go v0.4.1/go.mod h1:ruZWkvC1FEiUNjmROowOAps3ZcWxEiOpFoHCvk97kGc= +github.com/u2takey/go-utils v0.3.1 h1:TaQTgmEZZeDHQFYfd+AdUT1cT4QJgJn/XVPELhHw4ys= +github.com/u2takey/go-utils v0.3.1/go.mod h1:6e+v5vEZ/6gu12w/DC2ixZdZtCrNokVxD0JUklcqdCs= +gocv.io/x/gocv v0.25.0/go.mod h1:Rar2PS6DV+T4FL+PM535EImD/h13hGVaHhnCu1xarBs= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b h1:uwuIcX0g4Yl1NC5XAz37xsr2lTtcqevgzYNVt49waME= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= diff --git a/helpers.go b/helpers.go new file mode 100644 index 0000000..e3cb94f --- /dev/null +++ b/helpers.go @@ -0,0 +1,22 @@ +package moviego + +type Ordered interface { + int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64 | uintptr | float32 | float64 | string +} + +func InArray[T Ordered](needle T, haystack []T) bool { + for _, val := range haystack { + if val == needle { + return true + } + } + return false +} + +func Keys[M ~map[K]V, K comparable, V any](m M) []K { + r := make([]K, 0, len(m)) + for k := range m { + r = append(r, k) + } + return r +} diff --git a/moviego.go b/moviego.go new file mode 100644 index 0000000..5479a3f --- /dev/null +++ b/moviego.go @@ -0,0 +1,311 @@ +package moviego + +import ( + "errors" + "fmt" + "github.com/tidwall/gjson" + ffmpeg "github.com/u2takey/ffmpeg-go" + "io/ioutil" + "log" + "os" + "path/filepath" + "strings" +) + +type Video struct { + filePath string + width int64 + height int64 + duration float64 + probe string + stream *ffmpeg.Stream + ffmpegArgs map[string][]string + isTemp bool + hasModified bool + extension string +} + +type Output struct { + video Video +} + +func (OutputProcess Output) Run() error { + return OutputProcess.video.render() +} + +func (V *Video) addKwArgs(key, value string) { + if V.ffmpegArgs == nil { + V.ffmpegArgs = make(map[string][]string) + V.addDefaultKwArgs() + } + + V.ffmpegArgs[key] = append(V.ffmpegArgs[key], value) +} + +func (V *Video) addDefaultKwArgs() { + //keys := Keys(V.ffmpegArgs) + // + //if InArray("c:v", keys) == false { + // V.addKwArgs("c:v", "copy") + //} + // + //if InArray("c:a", keys) == false { + // V.addKwArgs("c:a", "copy") + //} +} + +func (V Video) ResizeByWidth(requiredWidth int64) Video { + // New Video Height = (Current Height / Current Width ) * My Required Width + V.height = int64((float64(V.height) / float64(V.width)) * float64(requiredWidth)) + + if V.height%2 != 0 { + V.height += 1 + } + + V.width = requiredWidth + V.hasModified = true + + return V +} + +func (V Video) ResizeByHeight(requiredHeight int64) Video { + // New Width = (Current Width / Current Height) * My Required Height + V.width = int64((float64(V.width) / float64(V.height)) * float64(requiredHeight)) + + if V.width%2 != 0 { + V.width += 1 + } + + V.height = requiredHeight + V.hasModified = true + + return V +} + +func (V Video) Resize(width, height int64) Video { + V.width, V.height = width, height + V.hasModified = true + return V +} + +func (V Video) prepareKwArgs(ignoreThisKeywords []string) ffmpeg.KwArgs { + // Fix: ffmpeg width or height not divisible by 2 error + if V.width%2 != 0 || V.height%2 != 0 { + V.addKwArgs("vf", fmt.Sprintf("format=yuv444p,scale=%d:%d", V.width, V.height)) + V.hasModified = true + } else { + V.addKwArgs("vf", fmt.Sprintf("scale=%d:%d", V.width, V.height)) + } + + compileKwArgs := make(ffmpeg.KwArgs) + for Keyword, Args := range V.ffmpegArgs { + if InArray(Keyword, ignoreThisKeywords) { + continue + } + + compileKwArgs[Keyword] = strings.Join(Args, ",") + } + + return compileKwArgs +} + +func (V Video) Output(OutputFilename string) Output { + compileKwArgs := V.prepareKwArgs([]string{""}) + + V.stream = V.stream.Output(OutputFilename, compileKwArgs) + + return Output{video: V} +} + +func (V Video) render() error { + if V.isTemp { + defer os.Remove(V.filePath) + } + + return V.stream.OverWriteOutput().Run() +} + +func (V *Video) checkStartAndEnd(start, end float64) { + if start > end { + panic("The `start` of the clip can't be bigger than its `end`.") + } + + if start > V.duration { + panic("The `start` cannot be bigger than the length of the main video.") + } + + if end > V.duration { + panic("The `end` cannot be bigger than the length of the main video.") + } +} + +func (V Video) tempRender() Video { + tempFolder := "" + file, err := ioutil.TempFile(tempFolder, fmt.Sprintf("video-*.%s", V.extension)) + + if err != nil { + log.Fatal(err) + } + + renderError := V.Output(file.Name()).Run() + + if renderError != nil { + log.Fatal(renderError) + } + + tempVideo, loadError := Load(file.Name()) + + if loadError != nil { + log.Fatal(loadError) + } + + tempVideo.isTemp = true + + return tempVideo +} + +func (V Video) SubClip(start, end float64) Video { + V.checkStartAndEnd(start, end) + + V.addKwArgs("ss", fmt.Sprintf("%f", start)) + V.addKwArgs("to", fmt.Sprintf("%f", end)) + V.duration = end - start + V.hasModified = true + + return V.tempRender() +} + +func Concat(videos []Video) (Video, error) { + var videoParts string + tempFolder := "" + + concatStorage, _ := ioutil.TempFile(tempFolder, "list-*.txt") + + var lastExtension string + for num, video := range videos { + if video.hasModified == false { + videoParts += fmt.Sprintf("file '%s'\n", video.filePath) + } else { + file, tempFileErr := ioutil.TempFile(tempFolder, fmt.Sprintf("video-%d-*.%s", num, video.extension)) + lastExtension = video.extension + + if tempFileErr != nil { + return Video{}, tempFileErr + } + + videoParts += fmt.Sprintf("file '%s'\n", file.Name()) + + video.Output(file.Name()).Run() + fmt.Println("Done:", file.Name()) + + defer os.Remove(file.Name()) + } + } + + _, writingError := concatStorage.WriteString(videoParts) + if writingError != nil { + return Video{}, writingError + } + + finalFile, finalErr := ioutil.TempFile(tempFolder, fmt.Sprintf("final-*.%s", lastExtension)) + if finalErr != nil { + return Video{}, finalErr + } + + err := ffmpeg.Input(concatStorage.Name(), ffmpeg.KwArgs{"f": "concat", "safe": "0"}).Output( + finalFile.Name(), ffmpeg.KwArgs{"c": "copy"}, + ).OverWriteOutput().Run() + + if err != nil { + return Video{}, err + } + + defer os.Remove(concatStorage.Name()) + + loadedFinalVideo, finalLoadError := Load(finalFile.Name()) + + if finalLoadError != nil { + return loadedFinalVideo, finalErr + } + + loadedFinalVideo.isTemp = true + + return loadedFinalVideo, nil +} + +func (V Video) FadeIn(start, duration float64) Video { + V.addKwArgs("vf", fmt.Sprintf("fade=t=in:st=%.3f:d=%.3f", start, duration)) + V.hasModified = true + return V +} + +func (V Video) FadeOut(duration float64) Video { + end := V.duration - duration + V.addKwArgs("vf", fmt.Sprintf("fade=t=out:st=%.3f:d=%.3f", end, duration)) + V.hasModified = true + return V +} + +func (V Video) AudioFadeIn(start, duration float64) Video { + V.addKwArgs("af", fmt.Sprintf("afade=t=in:st=%.3f:d=%.3f", start, duration)) + V.hasModified = true + return V +} + +func (V Video) AudioFadeOut(duration float64) Video { + end := V.duration - duration + V.addKwArgs("af", fmt.Sprintf("afade=t=out:st=%.3f:d=%.3f", end, duration)) + V.hasModified = true + return V +} + +func (V Video) Screenshot(timeInSeconds float64, outputFilename string) (string, error) { + compileKwArgs := V.prepareKwArgs([]string{"c:a", "c:v", "af"}) + compileKwArgs["ss"] = timeInSeconds + compileKwArgs["vframes"] = "1" + + abs, absError := filepath.Abs(outputFilename) + if absError != nil { + return "", absError + } + + err := V.stream.Output(abs, compileKwArgs).OverWriteOutput().Run() + if err != nil { + return "", err + } + + return abs, nil +} + +func (V Video) GetFilename() string { + return V.filePath +} + +func Load(fileName string) (Video, error) { + file, err := os.Stat(fileName) + if errors.Is(err, os.ErrNotExist) && !file.IsDir() { + return Video{}, os.ErrNotExist + } + + extension := strings.Trim(filepath.Ext(fileName), ".") + if extension == "" { + panic(fmt.Sprintf("your file '%s' does not have an extension!", fileName)) + } + + abs, absError := filepath.Abs(fileName) + videoProbe, _ := ffmpeg.Probe(abs) + + if absError != nil { + return Video{}, absError + } + + return Video{ + filePath: abs, + width: gjson.Get(videoProbe, "streams.0.width").Int(), + height: gjson.Get(videoProbe, "streams.0.height").Int(), + duration: gjson.Get(videoProbe, "format.duration").Float(), + probe: videoProbe, + stream: ffmpeg.Input(fileName), + extension: extension, + }, nil +}