Files
SpotiFLAC-Mobile/go_backend/extension_runtime_ffmpeg.go

183 lines
4.3 KiB
Go

package gobackend
import (
"fmt"
"strings"
"sync"
"time"
"github.com/dop251/goja"
)
// FFmpegCommand holds a pending FFmpeg command for Flutter to execute.
type FFmpegCommand struct {
ExtensionID string
Command string
InputPath string
OutputPath string
Completed bool
Success bool
Error string
Output string
}
var (
ffmpegCommands = make(map[string]*FFmpegCommand)
ffmpegCommandsMu sync.RWMutex
ffmpegCommandID int64
)
func GetPendingFFmpegCommand(commandID string) *FFmpegCommand {
ffmpegCommandsMu.RLock()
defer ffmpegCommandsMu.RUnlock()
return ffmpegCommands[commandID]
}
func SetFFmpegCommandResult(commandID string, success bool, output, errorMsg string) {
ffmpegCommandsMu.Lock()
defer ffmpegCommandsMu.Unlock()
if cmd, exists := ffmpegCommands[commandID]; exists {
cmd.Completed = true
cmd.Success = success
cmd.Output = output
cmd.Error = errorMsg
}
}
func ClearFFmpegCommand(commandID string) {
ffmpegCommandsMu.Lock()
defer ffmpegCommandsMu.Unlock()
delete(ffmpegCommands, commandID)
}
func (r *ExtensionRuntime) ffmpegExecute(call goja.FunctionCall) goja.Value {
if len(call.Arguments) < 1 {
return r.vm.ToValue(map[string]interface{}{
"success": false,
"error": "command is required",
})
}
command := call.Arguments[0].String()
ffmpegCommandsMu.Lock()
ffmpegCommandID++
cmdID := fmt.Sprintf("%s_%d", r.extensionID, ffmpegCommandID)
ffmpegCommands[cmdID] = &FFmpegCommand{
ExtensionID: r.extensionID,
Command: command,
Completed: false,
}
ffmpegCommandsMu.Unlock()
GoLog("[Extension:%s] FFmpeg command queued: %s\n", r.extensionID, cmdID)
timeout := 5 * time.Minute
start := time.Now()
for {
ffmpegCommandsMu.RLock()
cmd := ffmpegCommands[cmdID]
completed := cmd != nil && cmd.Completed
ffmpegCommandsMu.RUnlock()
if completed {
ffmpegCommandsMu.RLock()
result := map[string]interface{}{
"success": cmd.Success,
"output": cmd.Output,
}
if cmd.Error != "" {
result["error"] = cmd.Error
}
ffmpegCommandsMu.RUnlock()
ClearFFmpegCommand(cmdID)
return r.vm.ToValue(result)
}
if time.Since(start) > timeout {
ClearFFmpegCommand(cmdID)
return r.vm.ToValue(map[string]interface{}{
"success": false,
"error": "FFmpeg command timed out",
})
}
time.Sleep(100 * time.Millisecond)
}
}
func (r *ExtensionRuntime) ffmpegGetInfo(call goja.FunctionCall) goja.Value {
if len(call.Arguments) < 1 {
return r.vm.ToValue(map[string]interface{}{
"success": false,
"error": "file path is required",
})
}
filePath := call.Arguments[0].String()
quality, err := GetAudioQuality(filePath)
if err != nil {
return r.vm.ToValue(map[string]interface{}{
"success": false,
"error": err.Error(),
})
}
return r.vm.ToValue(map[string]interface{}{
"success": true,
"bit_depth": quality.BitDepth,
"sample_rate": quality.SampleRate,
"total_samples": quality.TotalSamples,
"duration": float64(quality.TotalSamples) / float64(quality.SampleRate),
})
}
func (r *ExtensionRuntime) ffmpegConvert(call goja.FunctionCall) goja.Value {
if len(call.Arguments) < 2 {
return r.vm.ToValue(map[string]interface{}{
"success": false,
"error": "input and output paths are required",
})
}
inputPath := call.Arguments[0].String()
outputPath := call.Arguments[1].String()
options := map[string]interface{}{}
if len(call.Arguments) > 2 && !goja.IsUndefined(call.Arguments[2]) && !goja.IsNull(call.Arguments[2]) {
if opts, ok := call.Arguments[2].Export().(map[string]interface{}); ok {
options = opts
}
}
var cmdParts []string
cmdParts = append(cmdParts, "-i", fmt.Sprintf("%q", inputPath))
if codec, ok := options["codec"].(string); ok {
cmdParts = append(cmdParts, "-c:a", codec)
}
if bitrate, ok := options["bitrate"].(string); ok {
cmdParts = append(cmdParts, "-b:a", bitrate)
}
if sampleRate, ok := options["sample_rate"].(float64); ok {
cmdParts = append(cmdParts, "-ar", fmt.Sprintf("%d", int(sampleRate)))
}
if channels, ok := options["channels"].(float64); ok {
cmdParts = append(cmdParts, "-ac", fmt.Sprintf("%d", int(channels)))
}
cmdParts = append(cmdParts, "-y", fmt.Sprintf("%q", outputPath))
command := strings.Join(cmdParts, " ")
execCall := goja.FunctionCall{
Arguments: []goja.Value{r.vm.ToValue(command)},
}
return r.ffmpegExecute(execCall)
}