Files
ffmpegd/cmd/ffmpegd.go
2022-07-26 20:17:25 -07:00

354 lines
8.5 KiB
Go

package cmd
import (
"encoding/json"
"fmt"
"math"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/alfg/ffmpegd/ffmpeg"
"github.com/gorilla/websocket"
)
const (
logo = `
███████╗███████╗███╗ ███╗██████╗ ███████╗ ██████╗ ██████╗
██╔════╝██╔════╝████╗ ████║██╔══██╗██╔════╝██╔════╝ ██╔══██╗
█████╗ █████╗ ██╔████╔██║██████╔╝█████╗ ██║ ███╗██║ ██║
██╔══╝ ██╔══╝ ██║╚██╔╝██║██╔═══╝ ██╔══╝ ██║ ██║██║ ██║
██║ ██║ ██║ ╚═╝ ██║██║ ███████╗╚██████╔╝██████╔╝
╚═╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚══════╝ ╚═════╝ ╚═════╝
v0.1.0
`
version = "ffmpegd version 0.1.0"
description = "[\u001b[32mffmpegd\u001b[0m] - websocket server for \u001b[33mffmpeg-commander\u001b[0m.\n"
usage = `
Usage:
ffmpegd Run server.
ffmpegd [port] Run server on port.
ffmpegd version Print version.
ffmpegd help This help text.
`
progressInterval = time.Second * 1
)
var (
port = "8080"
allowedOrigins = []string{
"http://localhost:" + port,
"https://alfg.github.io",
"https://alfg.dev",
}
clients = make(map[*websocket.Conn]bool)
broadcast = make(chan Message)
upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
for _, origin := range allowedOrigins {
if r.Header.Get("Origin") == origin {
return true
}
}
return false
},
}
progressCh chan struct{}
)
// Message payload from client.
type Message struct {
Type string `json:"type"`
Input string `json:"input"`
Output string `json:"output"`
Payload string `json:"payload"`
}
// Status response to client.
type Status struct {
Percent float64 `json:"percent"`
Speed string `json:"speed"`
FPS float64 `json:"fps"`
Err string `json:"err,omitempty"`
}
// FilesResponse http response for files endpoint.
type FilesResponse struct {
Cwd string `json:"cwd"`
Folders []string `json:"folders"`
Files []file `json:"files"`
}
type file struct {
Name string `json:"name"`
Size int64 `json:"size"`
}
func Run() {
parseArgs()
// CLI Banner.
printBanner()
// Check if FFmpeg/FFprobe are available.
err := verifyFFmpeg()
if err != nil {
fmt.Println("\u001b[31m" + err.Error() + "\u001b[0m")
fmt.Println("\u001b[31mPlease ensure FFmpeg and FFprobe are installed and available on $PATH.\u001b[0m")
return
}
// HTTP/WS Server.
startServer()
}
func parseArgs() {
args := os.Args
// Use defaults if no args are set.
if len(args) == 1 {
return
}
// Print version, help or set port.
if args[1] == "version" || args[1] == "-v" {
fmt.Println(version)
os.Exit(1)
} else if args[1] == "help" || args[1] == "-h" {
fmt.Println(usage)
os.Exit(1)
} else if _, err := strconv.Atoi(args[1]); err == nil {
port = args[1]
}
}
func printBanner() {
fmt.Println(logo)
fmt.Println(description)
}
func startServer() {
http.HandleFunc("/ws", handleConnections)
http.HandleFunc("/files", handleFiles)
http.Handle("/", http.FileServer(http.Dir("./")))
// Handles incoming WS messages from client.
go handleMessages()
fmt.Println(" Server started on port \u001b[33m:" + port + "\u001b[0m.")
fmt.Println(" - Go to \u001b[33mhttps://alfg.github.io/ffmpeg-commander\u001b[0m to connect!")
fmt.Println(" - \u001b[33mffmpegd\u001b[0m must be enabled in ffmpeg-commander options.")
fmt.Println("")
fmt.Printf("Waiting for connection...")
err := http.ListenAndServe(":"+port, nil)
if err != nil {
fmt.Println("ListenAndServe: ", err)
}
}
func handleConnections(w http.ResponseWriter, r *http.Request) {
ws, err := upgrader.Upgrade(w, r, nil)
if err != nil {
fmt.Printf("\rWaiting for connection...\u001b[31m websocket connection failed!\u001b[0m")
return
}
defer ws.Close()
// Register client.
clients[ws] = true
for {
fmt.Printf("\rWaiting for connection......\u001b[32mconnected!\u001b[0m")
var msg Message
// Read in a new message as JSON and map it to a Message object.
err := ws.ReadJSON(&msg)
if err != nil {
fmt.Printf("\rWaiting for connection...\u001b[31mdisconnected!\u001b[0m")
delete(clients, ws)
break
}
// Send the newly received message to the broadcast channel.
broadcast <- msg
}
}
func handleFiles(w http.ResponseWriter, r *http.Request) {
prefix := r.URL.Query().Get("prefix")
if prefix == "" {
prefix = "."
}
prefix = strings.TrimSuffix(prefix, "/")
wd, _ := os.Getwd()
resp := &FilesResponse{
Cwd: wd,
Folders: []string{},
Files: []file{},
}
files, _ := os.ReadDir(cleanPath(prefix))
for _, f := range files {
if f.IsDir() {
if prefix == "." {
resp.Folders = append(resp.Folders, f.Name()+"/")
} else {
resp.Folders = append(resp.Folders, prefix+"/"+f.Name()+"/")
}
} else {
var obj file
if prefix == "./" {
obj.Name = prefix + f.Name()
} else {
obj.Name = prefix + "/" + f.Name()
}
info, _ := f.Info()
obj.Size = info.Size()
resp.Files = append(resp.Files, obj)
}
}
w.Header().Set("Content-Type", "application/json")
cors(&w, r)
json.NewEncoder(w).Encode(resp)
}
func cleanPath(path string) string {
// Clean path to make safe for use with filepath.Join.
if path == "" {
return ""
}
path = filepath.Clean(path)
if !filepath.IsAbs(path) {
path = filepath.Clean(string(os.PathSeparator) + path)
path, _ = filepath.Rel(string(os.PathSeparator), path)
}
return filepath.Clean(path)
}
func cors(w *http.ResponseWriter, r *http.Request) {
for _, origin := range allowedOrigins {
if r.Header.Get("Origin") == origin {
(*w).Header().Set("Access-Control-Allow-Origin", origin)
}
}
}
func handleMessages() {
for {
msg := <-broadcast
if msg.Type == "encode" {
runEncode(msg.Input, msg.Output, msg.Payload)
}
}
}
func verifyFFmpeg() error {
f := &ffmpeg.FFmpeg{}
version, err := f.Version()
if err != nil {
return err
}
fmt.Println(" Checking FFmpeg version....\u001b[32m" + version + "\u001b[0m")
probe := &ffmpeg.FFProbe{}
version, err = probe.Version()
if err != nil {
return err
}
fmt.Println(" Checking FFprobe version...\u001b[32m" + version + "\u001b[0m\n")
return nil
}
func runEncode(input, output, payload string) {
probe := ffmpeg.FFProbe{}
probeData, err := probe.Run(input)
if err != nil {
sendError(err)
return
}
ffmpeg := &ffmpeg.FFmpeg{}
go trackEncodeProgress(probeData, ffmpeg)
err = ffmpeg.Run(input, output, payload)
// If we get an error back from ffmpeg, send an error ws message to clients.
if err != nil {
close(progressCh)
sendError(err)
return
}
close(progressCh)
for client := range clients {
p := &Status{
Percent: 100,
}
err := client.WriteJSON(p)
if err != nil {
fmt.Println("error: %w", err)
client.Close()
delete(clients, client)
}
}
}
func sendError(err error) {
for client := range clients {
p := &Status{
Err: err.Error(),
}
err := client.WriteJSON(p)
if err != nil {
fmt.Println("error: %w", err)
client.Close()
delete(clients, client)
}
}
}
func trackEncodeProgress(p *ffmpeg.FFProbeResponse, f *ffmpeg.FFmpeg) {
progressCh = make(chan struct{})
ticker := time.NewTicker(progressInterval)
for {
select {
case <-progressCh:
ticker.Stop()
fmt.Printf("\rWaiting for next job... ")
return
case <-ticker.C:
currentFrame := f.Progress.Frame
totalFrames, _ := strconv.Atoi(p.Streams[0].NbFrames)
speed := f.Progress.Speed
fps := f.Progress.FPS
// Only track progress if we know the total frames.
if totalFrames != 0 {
pct := (float64(currentFrame) / float64(totalFrames)) * 100
pct = math.Round(pct*100) / 100
fmt.Printf("\rEncoding... %d / %d (%0.2f%%) %s @ %0.2f fps", currentFrame, totalFrames, pct, speed, fps)
for client := range clients {
p := &Status{
Percent: pct,
Speed: speed,
FPS: fps,
}
err := client.WriteJSON(p)
if err != nil {
fmt.Println("error: %w", err)
client.Close()
delete(clients, client)
}
}
}
}
}
}