first commit
This commit is contained in:
81
handlers/analyze.go
Normal file
81
handlers/analyze.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"aroll/store"
|
||||
"aroll/transcode"
|
||||
"aroll/ws"
|
||||
)
|
||||
|
||||
type analyzeRequest struct {
|
||||
Filename string `json:"filename"`
|
||||
NoiseDb float64 `json:"noiseDb"`
|
||||
MinSilence float64 `json:"minSilence"`
|
||||
Padding float64 `json:"padding"`
|
||||
}
|
||||
|
||||
// AnalyzeHandler fetches the video bytes from Redis, writes them to a short-lived
|
||||
// temp file for FFmpeg, runs silence detection, then deletes the temp file.
|
||||
func AnalyzeHandler(st *store.Store, hub *ws.Hub) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
var req analyzeRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "bad JSON: "+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
data, err := st.Get(context.Background(), req.Filename)
|
||||
if err != nil {
|
||||
http.Error(w, "file not found in store (may have expired)", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
ext := filepath.Ext(req.Filename)
|
||||
|
||||
go func() {
|
||||
// Write to a temp file — FFmpeg needs a file path, not a byte slice.
|
||||
// This file exists only for the duration of the FFmpeg scan.
|
||||
tmp, err := os.CreateTemp("", "aroll-analyze-*"+ext)
|
||||
if err != nil {
|
||||
broadcastError(hub, "create temp: "+err.Error())
|
||||
return
|
||||
}
|
||||
defer os.Remove(tmp.Name())
|
||||
|
||||
if _, err := tmp.Write(data); err != nil {
|
||||
tmp.Close()
|
||||
broadcastError(hub, "write temp: "+err.Error())
|
||||
return
|
||||
}
|
||||
tmp.Close()
|
||||
|
||||
_, err = transcode.DetectSpeechSegments(
|
||||
tmp.Name(),
|
||||
req.NoiseDb,
|
||||
req.MinSilence,
|
||||
req.Padding,
|
||||
hub.Broadcast,
|
||||
)
|
||||
if err != nil {
|
||||
broadcastError(hub, err.Error())
|
||||
}
|
||||
}()
|
||||
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
}
|
||||
}
|
||||
|
||||
func broadcastError(hub *ws.Hub, msg string) {
|
||||
data, _ := json.Marshal(map[string]string{"type": "error", "message": msg})
|
||||
hub.Broadcast(data)
|
||||
}
|
||||
40
handlers/download.go
Normal file
40
handlers/download.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"aroll/store"
|
||||
)
|
||||
|
||||
// DownloadHandler fetches the exported video from Redis, streams it to the
|
||||
// browser, then immediately deletes the key. One download per export.
|
||||
func DownloadHandler(st *store.Store) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
key := strings.TrimPrefix(r.URL.Path, "/")
|
||||
|
||||
// Only serve keys we created in ExportHandler
|
||||
if !strings.HasPrefix(key, "output-") {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
data, err := st.Get(ctx, key)
|
||||
if err != nil {
|
||||
http.Error(w, "file not found or already downloaded", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// Delete from Redis now — one download only
|
||||
defer st.Delete(ctx, key)
|
||||
|
||||
w.Header().Set("Content-Type", "video/mp4")
|
||||
w.Header().Set("Content-Disposition", `attachment; filename="aroll-cut.mp4"`)
|
||||
w.Header().Set("Content-Length", strconv.Itoa(len(data)))
|
||||
w.Write(data)
|
||||
}
|
||||
}
|
||||
104
handlers/export.go
Normal file
104
handlers/export.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"aroll/store"
|
||||
"aroll/transcode"
|
||||
"aroll/ws"
|
||||
)
|
||||
|
||||
type exportRequest struct {
|
||||
Filename string `json:"filename"`
|
||||
Segments []transcode.Segment `json:"segments"`
|
||||
}
|
||||
|
||||
// ExportHandler fetches the input video from Redis, runs FFmpeg to cut the
|
||||
// silence, stores the output back in Redis, then cleans up the temp files.
|
||||
// The output key is sent to the frontend via a "done" WebSocket message.
|
||||
func ExportHandler(st *store.Store, hub *ws.Hub) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
var req exportRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "bad JSON: "+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if len(req.Segments) == 0 {
|
||||
http.Error(w, "no segments", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
data, err := st.Get(context.Background(), req.Filename)
|
||||
if err != nil {
|
||||
http.Error(w, "file not found in store (may have expired)", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
ext := filepath.Ext(req.Filename)
|
||||
|
||||
go func() {
|
||||
// Write input bytes to a temp file for FFmpeg
|
||||
inTmp, err := os.CreateTemp("", "aroll-in-*"+ext)
|
||||
if err != nil {
|
||||
broadcastError(hub, "create input temp: "+err.Error())
|
||||
return
|
||||
}
|
||||
defer os.Remove(inTmp.Name())
|
||||
|
||||
if _, err := inTmp.Write(data); err != nil {
|
||||
inTmp.Close()
|
||||
broadcastError(hub, "write input temp: "+err.Error())
|
||||
return
|
||||
}
|
||||
inTmp.Close()
|
||||
|
||||
// Create an output temp file for FFmpeg to write into
|
||||
outTmp, err := os.CreateTemp("", "aroll-out-*.mp4")
|
||||
if err != nil {
|
||||
broadcastError(hub, "create output temp: "+err.Error())
|
||||
return
|
||||
}
|
||||
outTmp.Close()
|
||||
defer os.Remove(outTmp.Name())
|
||||
|
||||
// Run FFmpeg — progress streamed via WebSocket
|
||||
if err := transcode.ExportSegments(
|
||||
inTmp.Name(), outTmp.Name(), req.Segments, hub.Broadcast,
|
||||
); err != nil {
|
||||
broadcastError(hub, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Read the output bytes and store them in Redis
|
||||
outData, err := os.ReadFile(outTmp.Name())
|
||||
if err != nil {
|
||||
broadcastError(hub, "read output: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
outputKey := "output-" + store.NewID()
|
||||
if err := st.Set(context.Background(), outputKey, outData); err != nil {
|
||||
broadcastError(hub, "store output: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Tell the frontend where to download from
|
||||
msg, _ := json.Marshal(map[string]string{
|
||||
"type": "done",
|
||||
"message": outputKey,
|
||||
})
|
||||
hub.Broadcast(msg)
|
||||
}()
|
||||
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
}
|
||||
}
|
||||
58
handlers/upload.go
Normal file
58
handlers/upload.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
|
||||
"aroll/store"
|
||||
)
|
||||
|
||||
// UploadHandler reads the video into memory and stores it in Redis under a
|
||||
// random key. The key is returned to the frontend as "filename".
|
||||
// Nothing is written to disk — the file lives only in Redis with a TTL.
|
||||
func UploadHandler(st *store.Store) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
r.Body = http.MaxBytesReader(w, r.Body, 4<<30)
|
||||
if err := r.ParseMultipartForm(64 << 20); err != nil {
|
||||
http.Error(w, "parse form: "+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
file, header, err := r.FormFile("video")
|
||||
if err != nil {
|
||||
http.Error(w, "missing 'video' field", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
data, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
http.Error(w, "read file: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
ext := filepath.Ext(header.Filename)
|
||||
if ext == "" {
|
||||
ext = ".mp4"
|
||||
}
|
||||
|
||||
// Key format: "video-<id><ext>" e.g. "video-a3f9...2b.mp4"
|
||||
key := "video-" + store.NewID() + ext
|
||||
|
||||
if err := st.Set(context.Background(), key, data); err != nil {
|
||||
http.Error(w, "store: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]string{"filename": key})
|
||||
}
|
||||
}
|
||||
47
handlers/ws.go
Normal file
47
handlers/ws.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"aroll/ws"
|
||||
|
||||
gws "github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
var upgrader = gws.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 4096,
|
||||
CheckOrigin: func(r *http.Request) bool { return true },
|
||||
}
|
||||
|
||||
func WSHandler(hub *ws.Hub) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
log.Printf("ws upgrade: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
client := &ws.Client{Send: make(chan []byte, 256)}
|
||||
hub.Register <- client
|
||||
|
||||
// Writer goroutine: pumps messages from hub → WebSocket
|
||||
go func() {
|
||||
defer conn.Close()
|
||||
for msg := range client.Send {
|
||||
if err := conn.WriteMessage(gws.TextMessage, msg); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Reader loop: keeps the connection alive and detects close frames
|
||||
for {
|
||||
if _, _, err := conn.ReadMessage(); err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
hub.Unregister <- client
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user