Structured the project in different files
All checks were successful
gitea/video-server-backend/pipeline/head This commit looks good

This commit is contained in:
Jose134 2024-08-13 19:41:53 +02:00
parent 02accfa66b
commit d9fbf941bd
6 changed files with 245 additions and 170 deletions

View File

@ -0,0 +1,67 @@
package handler
import (
"encoding/json"
"log"
"net/http"
"os"
"path/filepath"
"strings"
"video_server_backend/internal/bufferedsocket"
"github.com/gorilla/websocket"
)
const WEBSOCKET_WRITE_BUFFER_SIZE = 4096
const VIDEOS_DIR = "./videos"
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: WEBSOCKET_WRITE_BUFFER_SIZE,
CheckOrigin: func(r *http.Request) bool {
return true
},
}
type Video struct {
Filename string `json:"filename"`
}
func SearchVideoHandler(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query().Get("q")
if query == "" {
http.Error(w, "Query is required.", http.StatusBadRequest)
return
}
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
http.Error(w, "Could not upgrade to websocket.", http.StatusInternalServerError)
return
}
socket := bufferedsocket.NewBufferedSocket(conn, WEBSOCKET_WRITE_BUFFER_SIZE)
defer socket.Close()
err = filepath.Walk(VIDEOS_DIR, func(path string, info os.FileInfo, err error) error {
if strings.HasSuffix(path, ".mp4") && strings.Contains(path, query) {
jsonMsg, err := json.Marshal(Video{Filename: path})
if err != nil {
log.Println(err)
}
jsonMsg = append(jsonMsg, '\n')
socket.WriteMessage(jsonMsg)
}
return nil
})
if err != nil {
log.Println(err)
}
err = socket.Flush()
if err != nil {
log.Println(err)
}
}

View File

@ -0,0 +1,74 @@
package handler
import (
"errors"
"fmt"
"net/http"
"os"
"strconv"
"strings"
)
func StreamVideoHandler(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, errors.New("invalid range header")
}
rangeSpec := strings.Split(rangeParts[1], "-")
start, err := strconv.ParseInt(rangeSpec[0], 10, 64)
if err != nil {
return 0, 0, errors.New("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, errors.New("invalid range end")
}
} else {
end = fileSize - 1
}
return start, end, nil
}

16
api/middleware/cors.go Normal file
View File

@ -0,0 +1,16 @@
package middleware
import "net/http"
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)
})
}

View File

@ -1,186 +1,30 @@
package main package main
import ( import (
"encoding/json"
"fmt"
"log" "log"
"net/http" "net/http"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/gorilla/websocket" "video_server_backend/api/handler"
"video_server_backend/api/middleware"
) )
var upgrader = websocket.Upgrader{ func notFoundHandler(w http.ResponseWriter, r *http.Request) {
ReadBufferSize: 1024,
WriteBufferSize: 4096,
CheckOrigin: func(r *http.Request) bool {
return true
},
}
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) http.Error(w, "Endpoint not found", http.StatusNotFound)
} }
func connectSearchSocket(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query().Get("q")
if query == "" {
http.Error(w, "Query is required.", http.StatusBadRequest)
return
}
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()
}()
var msg = []byte{}
err = filepath.Walk(VIDEOS_DIR, func(path string, info os.FileInfo, err error) error {
fmt.Println("checking " + path)
if strings.HasSuffix(path, ".mp4") && strings.Contains(path, query) {
fmt.Println("found video " + path)
jsonMsg, err := json.Marshal(Video{Filename: path})
if err != nil {
log.Println(err)
}
jsonMsg = append(jsonMsg, []byte("\n")...)
if len(msg)+len(jsonMsg) > 4096 {
fmt.Println("sending message")
err = conn.WriteMessage(websocket.TextMessage, msg)
if err != nil {
log.Println(err)
return err
}
msg = jsonMsg
} else {
msg = append(msg, jsonMsg...)
}
}
return nil
})
if err != nil {
log.Println(err)
}
// Send remaining messages
if len(msg) > 0 {
err = conn.WriteMessage(websocket.TextMessage, msg)
if err != nil {
log.Println(err)
}
}
fmt.Println("done")
}
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 { func createMux() *http.ServeMux {
var mux *http.ServeMux = http.NewServeMux() var mux *http.ServeMux = http.NewServeMux()
mux.HandleFunc("/video", streamVideo) mux.HandleFunc("/video", handler.StreamVideoHandler)
mux.HandleFunc("/search", connectSearchSocket) mux.HandleFunc("/search", handler.SearchVideoHandler)
mux.HandleFunc("/", notFound) mux.HandleFunc("/", notFoundHandler)
return mux return mux
} }
func main() { func createHandler() http.Handler {
log.Fatal(http.ListenAndServe(":8080", addCORSHeaders(createMux()))) return middleware.AddCORSHeaders(createMux())
}
func main() {
log.Fatal(http.ListenAndServe(":8080", createHandler()))
} }

3
go.mod
View File

@ -3,6 +3,5 @@ module video_server_backend
go 1.22.6 go 1.22.6
require ( require (
github.com/google/uuid v1.6.0 // indirect github.com/gorilla/websocket v1.5.3 // direct
github.com/gorilla/websocket v1.5.3 // indirect
) )

View File

@ -0,0 +1,75 @@
package bufferedsocket
import (
"errors"
"fmt"
"github.com/gorilla/websocket"
)
type BufferedSocket struct {
conn *websocket.Conn
bufferSize int
buffer []byte
bufferLen int
}
func NewBufferedSocket(conn *websocket.Conn, bufferSize int) *BufferedSocket {
return &BufferedSocket{
conn: conn,
bufferSize: bufferSize,
buffer: make([]byte, 0, bufferSize),
}
}
func (bs *BufferedSocket) writeBuffer() error {
fmt.Println("writing buffer")
fmt.Println(bs.bufferLen)
fmt.Printf("%s\n", string(bs.buffer[:bs.bufferLen]))
err := bs.conn.WriteMessage(websocket.TextMessage, bs.buffer[:bs.bufferLen])
if err != nil {
return err
}
return nil
}
func (bs *BufferedSocket) WriteMessage(message []byte) error {
if len(message) > bs.bufferSize {
return errors.New("message larger than buffer size")
}
if bs.bufferLen+len(message) > bs.bufferSize {
fmt.Println("flushing from write message")
err := bs.writeBuffer()
if err != nil {
return err
}
bs.buffer = make([]byte, bs.bufferSize)
bs.bufferLen = 0
}
bs.buffer = append(bs.buffer, message...)
bs.bufferLen += len(message)
return nil
}
func (bs *BufferedSocket) Flush() error {
if bs.bufferLen > 0 {
fmt.Println("flushing from flush")
err := bs.writeBuffer()
if err != nil {
return err
}
bs.buffer = make([]byte, 0, bs.bufferSize)
bs.bufferLen = 0
}
return nil
}
func (bs *BufferedSocket) Close() error {
err := bs.conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
if err != nil {
return err
}
bs.conn.Close()
return nil
}