Skip to content

Commit

Permalink
feat: add ffmpeg to build
Browse files Browse the repository at this point in the history
  • Loading branch information
k1nho committed Jun 20, 2024
1 parent a371eac commit c485554
Show file tree
Hide file tree
Showing 13 changed files with 139 additions and 40 deletions.
13 changes: 9 additions & 4 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ on:
# Match any new tag
- "*"

permissions:
contents: write

env:
# Necessary for most environments as build failure can occur due to OOM issues
NODE_OPTIONS: "--max-old-space-size=4096"
Expand All @@ -17,9 +20,6 @@ jobs:
fail-fast: false
matrix:
build:
- name: "Gahara"
platform: "linux/amd64"
os: "ubuntu-latest"
- name: "Gahara"
platform: "darwin/universal"
os: "macos-latest"
Expand All @@ -31,11 +31,16 @@ jobs:
with:
submodules: recursive

- name: Download FFmpeg for macOS
if: matrix.build.platform == 'darwin/universal'
run: |
chmod +x hack/setup.sh
./hack/setup.sh ${{matrix.build.platform}}
- name: Build wails
uses: dAppServer/[email protected]
id: build
with:
build-name: ${{ matrix.build.name }}
build-platform: ${{ matrix.build.platform }}
package: false
go-version: "1.22"
11 changes: 11 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.PHONY: ffmpeg
ffmpeg:
@echo "Setting up FFmpeg..."
@chmod +x ./hack/setup.sh
@./hack/setup.sh darwin/universal


.PHONY: cleanup
cleanup:
@echo "Teardown..."
@rm -rf ./resources/
24 changes: 22 additions & 2 deletions app.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"path"
"path/filepath"
"strings"
"time"

"runtime"

Expand Down Expand Up @@ -38,6 +39,8 @@ type App struct {
config Config
// Timeline: the project timeline
Timeline video.Timeline `json:"timeline"`
// FFmpegPath: the configured ffmpeg on build
FFmpegPath string
}

// NewApp creates a new App application struct
Expand All @@ -51,8 +54,25 @@ func (a *App) startup(ctx context.Context) {
a.ctx = ctx
err := a.gaharaSetup()
if err != nil {
wruntime.LogFatal(ctx, err.Error())
wruntime.LogFatal(a.ctx, err.Error())
}
FFmpegPath, err := ExtractFFmpeg()
if err != nil {
wruntime.LogFatal(a.ctx, fmt.Sprintf("could not initialize FFmpeg: %s", err.Error()))
}
a.FFmpegPath = FFmpegPath
wruntime.LogInfo(a.ctx, fmt.Sprintf("initialized FFmpeg at %s", a.FFmpegPath))
}

func (a *App) cleanup(ctx context.Context) {
_, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()

err := os.RemoveAll(filepath.Dir(a.FFmpegPath))
if err != nil {
wruntime.LogError(a.ctx, "could not cleanup FFmpeg")
}
wruntime.LogInfo(a.ctx, "FFmpeg was cleaned")
}

// FilePicker: opens the native file picker for the user
Expand Down Expand Up @@ -208,7 +228,7 @@ func (a *App) ReadProjectWorkspace() ([]Video, error) {
continue
}

duration, err := getVideoDuration(video.ProcessingOpts{
duration, err := getVideoDuration(a.FFmpegPath, video.ProcessingOpts{
Filename: strings.Split(project.Name(), ".")[0],
VideoFormat: filepath.Ext(project.Name()),
InputPath: a.config.ProjectDir,
Expand Down
31 changes: 31 additions & 0 deletions darwin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//go:build darwin

package main

import (
"embed"
"os"
"path/filepath"
)

//go:embed resources/darwin/ffmpeg
var FFmpegBinary embed.FS

func ExtractFFmpeg() (string, error) {
data, err := FFmpegBinary.ReadFile("resources/darwin/ffmpeg")
if err != nil {
return "", err
}

tempDir, err := os.MkdirTemp("", "ffmpeg")
if err != nil {
return "", err
}

ffmpegPath := filepath.Join(tempDir, "ffmpeg")
err = os.WriteFile(ffmpegPath, data, 0755)
if err != nil {
return "", err
}
return ffmpegPath, nil
}
6 changes: 4 additions & 2 deletions ffmpegbuilder/ffmpegbuilder.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
)

type FFmpegBuilder struct {
FFmpegPath string
PreInputParams PreInputParams
Inputs []string
FilterGraphParams FilterGraphParams
Expand Down Expand Up @@ -67,8 +68,9 @@ type OutputParams struct {
NullOutput string
}

func NewDefaultFFmpegBuilder() *FFmpegBuilder {
func NewDefaultFFmpegBuilder(FFmpegPath string) *FFmpegBuilder {
return &FFmpegBuilder{
FFmpegPath: FFmpegPath,
PreInputParams: NewDefaultPreInputParams(),
Inputs: []string{},
ComplexFilterGraph: []string{},
Expand Down Expand Up @@ -223,7 +225,7 @@ func (f *FFmpegBuilder) ConcatFilter(videoNodes []video.VideoNode) (string, erro
// BuildQuery: returns the ffmpeg query with all the parameters given
func (f *FFmpegBuilder) BuildQuery() (string, error) {
var cmd strings.Builder
cmd.WriteString("ffmpeg ")
cmd.WriteString(fmt.Sprintf("%s ", f.FFmpegPath))

if f.PreInputParams.HideBanner {
cmd.WriteString("-hide_banner ")
Expand Down
10 changes: 5 additions & 5 deletions ffmpegbuilder/ffmpegbuilder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ func TestFFmpegBuilder(t *testing.T) {

t.Run("format conversion query", func(t *testing.T) {
expectedQuery := "ffmpeg -hide_banner -v quiet -stats_period 5s -progress pipe:2 -i \"myinput.mp4\" \"myoutput.mov\" "
query, err := NewDefaultFFmpegBuilder().WithInputs("myinput.mp4").WithOutputs("myoutput.mov").BuildQuery()
query, err := NewDefaultFFmpegBuilder("ffmpeg").WithInputs("myinput.mp4").WithOutputs("myoutput.mov").BuildQuery()
if err != nil {
t.Fatal(err)
}
Expand All @@ -33,7 +33,7 @@ func TestFFmpegBuilder(t *testing.T) {

t.Run("generate proxy file query", func(t *testing.T) {
expectedQuery := "ffmpeg -hide_banner -v quiet -stats_period 5s -progress pipe:2 -i \"inputpath/input.mp4\" -c copy \"outputpath/input.mov\" "
query, err := CreateProxyFileQuery(video.ProcessingOpts{
query, err := CreateProxyFileQuery("ffmpeg", video.ProcessingOpts{
Filename: "input",
InputPath: "inputpath",
OutputPath: "outputpath",
Expand All @@ -49,7 +49,7 @@ func TestFFmpegBuilder(t *testing.T) {

t.Run("generate thumbnail query", func(t *testing.T) {
expectedQuery := "ffmpeg -hide_banner -v quiet -stats_period 5s -progress pipe:2 -i \"inputpath/input.mp4\" -frames:v 1 \"outputpath/input.png\" "
query, err := CreateThumbnailQuery(video.ProcessingOpts{
query, err := CreateThumbnailQuery("ffmpeg", video.ProcessingOpts{
Filename: "input",
InputPath: "inputpath",
OutputPath: "outputpath",
Expand All @@ -66,7 +66,7 @@ func TestFFmpegBuilder(t *testing.T) {
t.Run("concat filter query", func(t *testing.T) {
expectedQuery := "ffmpeg -hide_banner -v quiet -stats_period 5s -progress pipe:2 -i \"root1\" -i \"root2\" -i \"root3\" -filter_complex \"[0:v]trim=start=20.1000:end=25.2000,setpts=PTS-STARTPTS,scale=1920x1080[v0];[0:v]trim=start=1.1200:end=10.2000,setpts=PTS-STARTPTS,scale=1920x1080[v1];[1:v]trim=start=12.2000:end=21.2000,setpts=PTS-STARTPTS,scale=1920x1080[v2];[2:v]trim=start=69.1120:end=80.2300,setpts=PTS-STARTPTS,scale=1920x1080[v3];[v0][v1][v2][v3]concat=n=4:v=1:a=0[out]\" -map \"[out]\" -c:v libx264 -crf 18 -preset medium \"outputpath/myvideo.mp4\" "

query, err := MergeClipsQuery(mockTl().VideoNodes, video.ProcessingOpts{
query, err := MergeClipsQuery("ffmpeg", mockTl().VideoNodes, video.ProcessingOpts{
Resolution: "1920x1080",
Codec: "libx264",
CRF: "18",
Expand All @@ -90,7 +90,7 @@ func TestFFmpegBuilder(t *testing.T) {

expectedQuery := fmt.Sprintf("ffmpeg -hide_banner -v quiet -stats_period 5s -progress pipe:2 -ss 22.2300 -i \"root1\" -t %.4f -avoid_negative_ts make_zero -c copy -movflags '+faststart' \"outputpath/myvideo.mp4\" ", duration)

query, err := LosslessCutQuery(videoNode, video.ProcessingOpts{
query, err := LosslessCutQuery("ffmpeg", videoNode, video.ProcessingOpts{
OutputPath: "outputpath",
Filename: videoNode.Name,
VideoFormat: ".mp4",
Expand Down
20 changes: 10 additions & 10 deletions ffmpegbuilder/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import (
"github.com/k1nho/gahara/internal/video"
)

func CheckVideoDuration(userOpts video.ProcessingOpts) (string, error) {
func CheckVideoDuration(FFmpegPath string, userOpts video.ProcessingOpts) (string, error) {
input := GetFullInputPath(userOpts)

query, err := NewDefaultFFmpegBuilder().WithInputs(input).
query, err := NewDefaultFFmpegBuilder(FFmpegPath).WithInputs(input).
WithNullOutput().WithVerbose("").BuildQuery()
if err != nil {
return "", err
Expand All @@ -17,12 +17,12 @@ func CheckVideoDuration(userOpts video.ProcessingOpts) (string, error) {
}

// CreateProxyFileQuery: creates a proxy file for a video
func CreateProxyFileQuery(userOpts video.ProcessingOpts, format string) (string, error) {
func CreateProxyFileQuery(FFmpegPath string, userOpts video.ProcessingOpts, format string) (string, error) {
input := GetFullInputPath(userOpts)
userOpts.VideoFormat = format
output := GetFullOutputPath(userOpts)

querybuilder := NewDefaultFFmpegBuilder().WithInputs(input).WithCodec("copy").
querybuilder := NewDefaultFFmpegBuilder(FFmpegPath).WithInputs(input).WithCodec("copy").
WithOutputs(output)

if err := querybuilder.validateProxyFileCreationQuery(); err != nil {
Expand All @@ -38,12 +38,12 @@ func CreateProxyFileQuery(userOpts video.ProcessingOpts, format string) (string,
}

// CreateThumbnailQuery: generates a thumbnail taking the 1 frame of a video
func CreateThumbnailQuery(userOpts video.ProcessingOpts, format string) (string, error) {
func CreateThumbnailQuery(FFmpegPath string, userOpts video.ProcessingOpts, format string) (string, error) {
input := GetFullInputPath(userOpts)
userOpts.VideoFormat = format
output := GetFullOutputPath(userOpts)

query, err := NewDefaultFFmpegBuilder().WithInputs(input).WithScale(userOpts.Resolution).
query, err := NewDefaultFFmpegBuilder(FFmpegPath).WithInputs(input).WithScale(userOpts.Resolution).
WithVideoFrames("1").WithOutputs(output).BuildQuery()
if err != nil {
return "", err
Expand All @@ -53,8 +53,8 @@ func CreateThumbnailQuery(userOpts video.ProcessingOpts, format string) (string,
}

// MergeClipsQuery: returns the query to concatenate a series of video nodes
func MergeClipsQuery(videoNodes []video.VideoNode, userOpts video.ProcessingOpts) (string, error) {
querybuilder := NewDefaultFFmpegBuilder().WithInputs(ExtractInputs(videoNodes)...).
func MergeClipsQuery(FFmpegPath string, videoNodes []video.VideoNode, userOpts video.ProcessingOpts) (string, error) {
querybuilder := NewDefaultFFmpegBuilder(FFmpegPath).WithInputs(ExtractInputs(videoNodes)...).
WithPreset(userOpts.Preset).WithCRF(userOpts.CRF).WithVideoCodec(userOpts.Codec).
WithFScale(userOpts.Resolution).WithOutputs(GetFullOutputPath(userOpts))

Expand All @@ -76,11 +76,11 @@ func MergeClipsQuery(videoNodes []video.VideoNode, userOpts video.ProcessingOpts
}

// LosslessCutQuery: returns the query string to make a lossless cut of a video node
func LosslessCutQuery(videoNode video.VideoNode, userOpts video.ProcessingOpts) (string, error) {
func LosslessCutQuery(FFmpegPath string, videoNode video.VideoNode, userOpts video.ProcessingOpts) (string, error) {
// overwrite filename, if it was passed by default lossy opts
userOpts.Filename = videoNode.Name

querybuilder := NewDefaultFFmpegBuilder().WithInputs(videoNode.RID).WithInputStartTime(videoNode.Start).
querybuilder := NewDefaultFFmpegBuilder(FFmpegPath).WithInputs(videoNode.RID).WithInputStartTime(videoNode.Start).
WithOutputDuration(videoNode.End - videoNode.Start).WithCodec("copy").WithAvoidNegativeTS("make_zero").
WithMovFlags("+faststart").WithOutputs(GetFullOutputPath(userOpts))

Expand Down
20 changes: 20 additions & 0 deletions hack/setup.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#!/bin/bash

PLATFORM=$1

if [[ "$PLATFORM" == "darwin/universal" ]]; then
echo "Setting up FFmpeg for macOS"
curl -O https://evermeet.cx/ffmpeg/ffmpeg-115960-g3a5202d026.zip
unzip ffmpeg-115960-g3a5202d026.zip
mkdir -p resources/darwin/
mv ffmpeg resources/darwin/ffmpeg
chmod +x resources/darwin/ffmpeg
rm -rf ffmpeg-115960-g3a5202d026.zip
elif [[ "$PLATFORM" == "linux/amd64" ]]; then
echo "Setting up FFmpeg for Linux"
elif [[ "$PLATFORM" == "windows" ]]; then
echo "Setting up FFmpeg for Windows"
else
echo "Unsupported platform: $PLATFORM"
exit 1
fi
10 changes: 0 additions & 10 deletions internal/video/video.go
Original file line number Diff line number Diff line change
Expand Up @@ -310,16 +310,6 @@ func (tl *Timeline) DeleteRIDReferences(rid string) error {
return nil
}

// CreateProxyFile: creates a copy file from the original to preserve original and work with the given video clip
func CreateProxyFileCMD(inputFilePath, outputFilePath string) *exec.Cmd {
return exec.Command("ffmpeg",
"-i", inputFilePath, // input
"-codec", "copy",
"-strict", "experimental",
outputFilePath)

}

// GenerateEditThumbnail: generate a thumbnail from a video
func GenerateEditThumb(inputFilePath string, outputFilePath string, opts ThumbnailOpts) *exec.Cmd {
return exec.Command("ffmpeg",
Expand Down
9 changes: 9 additions & 0 deletions linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
//go:build linux

package main

import "fmt"

func ExtractFFmpeg() (string, error) {
return "", fmt.Errorf("unsupported platform")
}
1 change: 1 addition & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ func main() {
},
BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
OnStartup: app.startup,
OnShutdown: app.cleanup,
Menu: app.AppMenu(),
Bind: []interface{}{
app,
Expand Down
15 changes: 8 additions & 7 deletions video.go
Original file line number Diff line number Diff line change
Expand Up @@ -428,7 +428,7 @@ func (a *App) FFmpegQuery(queryType string, userOpts video.ProcessingOpts) error

// queryFiltergraph: executes a filtergraph query, currently merge clips
func (a *App) queryFiltergraph(userOpts video.ProcessingOpts) error {
query, err := ffmpegbuilder.MergeClipsQuery(a.Timeline.VideoNodes, userOpts)
query, err := ffmpegbuilder.MergeClipsQuery(a.FFmpegPath, a.Timeline.VideoNodes, userOpts)
if err != nil {
return err
}
Expand Down Expand Up @@ -464,7 +464,7 @@ func (a *App) queryLosslessCut(userOpts video.ProcessingOpts) error {
go func(vNode video.VideoNode) {
defer wg.Done()
userOpts.Filename = vNode.Name
query, err := ffmpegbuilder.LosslessCutQuery(vNode, userOpts)
query, err := ffmpegbuilder.LosslessCutQuery(a.FFmpegPath, vNode, userOpts)
if err != nil {
msgChannel <- VideoProcessingResult{ID: vNode.ID, Status: Failed, Message: err.Error()}
return
Expand All @@ -483,11 +483,11 @@ func (a *App) queryLosslessCut(userOpts video.ProcessingOpts) error {

// queryCreateProxyFile: executes a conversion query for the given video
func (a *App) queryCreateProxyFile(userOpts video.ProcessingOpts) error {
query, err := ffmpegbuilder.CreateProxyFileQuery(userOpts, ".mov")
query, err := ffmpegbuilder.CreateProxyFileQuery(a.FFmpegPath, userOpts, ".mov")
if err != nil {
return err
}

fmt.Println("query is: ", query)
err = a.executeFFmpegQuery(query, NewMonitoringOpts(video.OBV_OUT_TIME))
if err != nil {
return err
Expand All @@ -498,7 +498,7 @@ func (a *App) queryCreateProxyFile(userOpts video.ProcessingOpts) error {

// queryCreateThumbnail: executes a query to generate a thumbnail for a video (picks 1st frame)
func (a *App) queryCreateThumbnail(userOpts video.ProcessingOpts) error {
query, err := ffmpegbuilder.CreateThumbnailQuery(userOpts, ".png")
query, err := ffmpegbuilder.CreateThumbnailQuery(a.FFmpegPath, userOpts, ".png")
if err != nil {
return err
}
Expand Down Expand Up @@ -563,6 +563,7 @@ func (a *App) monitorFFmpegOuput(FFmpegOut io.ReadCloser, monitoringOpts *Monito
if args[0] != video.OBV_OUT_TIME {
continue
}
fmt.Println(args, "/n the other ", args[1])
duration, err := convertHMStoSeconds(args[1])
if err != nil {
// TODO: handle conversion error
Expand Down Expand Up @@ -608,8 +609,8 @@ func convertHMStoSeconds(hms string) (float64, error) {
return totalSeconds, nil
}

func getVideoDuration(userOpts video.ProcessingOpts) (float64, error) {
query, err := ffmpegbuilder.CheckVideoDuration(userOpts)
func getVideoDuration(FFmpegPath string, userOpts video.ProcessingOpts) (float64, error) {
query, err := ffmpegbuilder.CheckVideoDuration(FFmpegPath, userOpts)
if err != nil {
return 0, err
}
Expand Down
Loading

0 comments on commit c485554

Please sign in to comment.