Video editing from Go, without FFmpeg on your machine.
Trim, resize, compress, convert, caption and transcribe by calling one HTTP endpoint per operation. Parameters are validated before anything renders, so a bad call fails as a typed error rather than a broken encode.
What running FFmpeg from Go usually costs
- A single static binary is often why Go was chosen, and shelling out to FFmpeg puts a system dependency back into the deployment.
- os/exec gives you an exit code and stderr, so a bad parameter is a string to parse rather than a typed error.
- The encode competes with the service for CPU on the same box, which is felt first by whatever else that box was doing.
Trim a clip and wait for the file
Go
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
"time"
)
const base = "https://kinopipe.com/api/v1"
type job struct {
JobID string `json:"jobId"`
Status string `json:"status"`
ErrorMessage string `json:"errorMessage"`
Result struct {
Output struct {
DownloadURL string `json:"downloadUrl"`
} `json:"output"`
} `json:"result"`
}
func call(method, url string, body any) (job, error) {
var payload *bytes.Reader
if body != nil {
encoded, _ := json.Marshal(body)
payload = bytes.NewReader(encoded)
} else {
payload = bytes.NewReader(nil)
}
request, err := http.NewRequest(method, url, payload)
if err != nil {
return job{}, err
}
request.Header.Set("Authorization", "Bearer "+os.Getenv("KINOPIPE_API_KEY"))
request.Header.Set("Content-Type", "application/json")
response, err := http.DefaultClient.Do(request)
if err != nil {
return job{}, err
}
defer response.Body.Close()
var decoded job
return decoded, json.NewDecoder(response.Body).Decode(&decoded)
}
func main() {
started, err := call(http.MethodPost, base+"/tools/trim-video", map[string]any{
"inputs": []map[string]string{{"id": "main", "url": "https://example.com/interview.mp4"}},
"options": map[string]int{"start": 12, "end": 48},
})
if err != nil {
panic(err)
}
for {
current, err := call(http.MethodGet, base+"/jobs/"+started.JobID, nil)
if err != nil {
panic(err)
}
if current.Status == "failed" {
panic(current.ErrorMessage)
}
if current.Status == "succeeded" {
fmt.Println(current.Result.Output.DownloadURL)
return
}
time.Sleep(2 * time.Second)
}
}- Standard library only, so the binary stays single and static.
- A split job returns every segment under outputs rather than output, which is worth handling if you call split-video or split-video-by-scenes.
- Send an Idempotency-Key header when you retry, and the retry returns the original job instead of paying for a second render.
FAQ
Why not just call os/exec?
It works when FFmpeg is on the machine and you are happy to own that. The trade is a system dependency in the image and errors that arrive as text on stderr instead of as validation before the render.
Is there a Go SDK?
Not yet, and the API is small enough that the code above is most of one. Everything is JSON over HTTP with a bearer token.
Can I run several edits at once?
Yes. Jobs are independent, so start them in goroutines and poll each. Credits are reserved per job and refunded when a render fails.