commit 203d43051da33425a0dba2995176bfc400f7417a Author: Jose134 Date: Sat Aug 10 00:40:12 2024 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6330093 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +videos/ diff --git a/cmd/video_server_backend/main.go b/cmd/video_server_backend/main.go new file mode 100644 index 0000000..7970316 --- /dev/null +++ b/cmd/video_server_backend/main.go @@ -0,0 +1,200 @@ +package main + +import ( + "encoding/json" + "fmt" + "log" + "net/http" + "os" + "path/filepath" + "slices" + "strconv" + "strings" + + "github.com/google/uuid" + "github.com/gorilla/websocket" +) + +var upgrader = websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, + CheckOrigin: func(r *http.Request) bool { + return true + }, +} + +var tasks = []string{} + +const VIDEOS_DIR = "./videos" + +type Video struct { + Filename string `json:"filename"` +} + +func notFound(w http.ResponseWriter, r *http.Request) { + http.Error(w, "Endpoint not found", http.StatusNotFound) +} + +func searchVideo(w http.ResponseWriter, r *http.Request) { + query := r.URL.Query().Get("q") + if query == "" { + http.Error(w, "Query is required.", http.StatusBadRequest) + return + } + + task := uuid.New().String() + tasks = append(tasks, task) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write([]byte(task)) + + fmt.Printf("Task %s created\n", task) +} + +func connectSearchSocket(w http.ResponseWriter, r *http.Request) { + taskId := r.URL.Query().Get("task") + if !slices.Contains(tasks, taskId) { + http.Error(w, "Task not found.", http.StatusNotFound) + return + } + + fmt.Printf("Task %s started\n", taskId) + + conn, err := upgrader.Upgrade(w, r, nil) + if err != nil { + fmt.Println(err) + http.Error(w, "Could not upgrade to websocket.", http.StatusInternalServerError) + return + } + + defer func() { + err := conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) + if err != nil { + log.Println("Error during websocket close: ", err) + } + conn.Close() + }() + + files := []string{} + err = filepath.Walk(VIDEOS_DIR, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + files = append(files, path) + return nil + }) + + if err != nil { + // http.Error(w, "Could not read video directory.", http.StatusInternalServerError) + return + } + + for _, file := range files { + if strings.Contains(file, ".mp4") { + jsonMsg, err := json.Marshal(Video{Filename: file}) + if err != nil { + log.Println(err) + } + + err = conn.WriteMessage(websocket.TextMessage, jsonMsg) + if err != nil { + log.Println(err) + } + } + } + + tasks = slices.DeleteFunc(tasks, func(task string) bool { return task == taskId }) + fmt.Printf("Task %s finished\n", taskId) +} + +func streamVideo(w http.ResponseWriter, r *http.Request) { + videoPath := r.URL.Query().Get("v") + videoFile, err := os.Open(videoPath) + if err != nil { + http.Error(w, "Video file not found.", http.StatusNotFound) + return + } + defer videoFile.Close() + + fileInfo, err := videoFile.Stat() + if err != nil { + http.Error(w, "Could not obtain file info.", http.StatusInternalServerError) + return + } + + fileSize := fileInfo.Size() + rangeHeader := r.Header.Get("Range") + if rangeHeader == "" { + http.ServeFile(w, r, videoPath) + return + } + + start, end, err := getStartEndRange(rangeHeader, fileSize) + if err != nil || start > end || end >= fileSize { + http.Error(w, "Invalid range.", http.StatusBadRequest) + return + } + + w.Header().Set("Content-Type", "video/mp4") + w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, fileSize)) + w.Header().Set("Accept-Ranges", "bytes") + w.WriteHeader(http.StatusPartialContent) + + videoFile.Seek(start, 0) + buf := make([]byte, end-start+1) + videoFile.Read(buf) + w.Write(buf) +} + +func getStartEndRange(rangeHeader string, fileSize int64) (int64, int64, error) { + rangeParts := strings.Split(rangeHeader, "=") + if len(rangeParts) != 2 || rangeParts[0] != "bytes" { + return 0, 0, fmt.Errorf("invalid range header") + } + + rangeSpec := strings.Split(rangeParts[1], "-") + start, err := strconv.ParseInt(rangeSpec[0], 10, 64) + if err != nil { + return 0, 0, fmt.Errorf("invalid range start") + } + + var end int64 + if len(rangeSpec) == 2 && rangeSpec[1] != "" { + end, err = strconv.ParseInt(rangeSpec[1], 10, 64) + if err != nil { + return 0, 0, fmt.Errorf("invalid range end") + } + } else { + end = fileSize - 1 + } + + return start, end, nil +} + +func addCORSHeaders(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") + if r.Method == "OPTIONS" { + w.WriteHeader(http.StatusOK) + return + } + next.ServeHTTP(w, r) + }) +} + +func createMux() *http.ServeMux { + var mux *http.ServeMux = http.NewServeMux() + mux.HandleFunc("/video", streamVideo) + mux.HandleFunc("/search", searchVideo) + mux.HandleFunc("/search_socket", connectSearchSocket) + mux.HandleFunc("/", notFound) + + return mux +} + +func main() { + log.Fatal(http.ListenAndServe(":8080", addCORSHeaders(createMux()))) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..4cef24d --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module video_server_backend + +go 1.22.6 + +require ( + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..73bbf57 --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=