From 7b49753570d47755a0211ace61508a27776a0f24 Mon Sep 17 00:00:00 2001 From: Sergey Petrov Date: Wed, 9 Apr 2025 17:58:24 +0500 Subject: [PATCH] Add handlers. --- reader/cmd/main.go | 3 +- reader/internal/handlers/handlers.go | 340 +++++++++++++++++++++++++-- 2 files changed, 327 insertions(+), 16 deletions(-) diff --git a/reader/cmd/main.go b/reader/cmd/main.go index f9848b6..15f3ff6 100644 --- a/reader/cmd/main.go +++ b/reader/cmd/main.go @@ -19,7 +19,8 @@ func main() { //http.HandleFunc("GET /api/v1/vods/{id}/", handlers.ConfigVodsHandler) //http.HandleFunc("DELETE /api/v1/vods/{id}/", handlers.DelVodsHandler) //http.HandleFunc("GET /api/v1/vods/{id}/files/", handlers.ListFilesVodsHandler) - //http.HandleFunc("GET /api/v1/vods/{id}/{res}/{file}", handlers.FileVodsHandler) + //http.HandleFunc("GET /api/v1/vods/{id}/{res}/{file}", handlers.SingleVodsHandler) + //http.HandleFunc("DELETE /api/v1/vods/{id}/{res}/{file}", handlers.DelSingleVodsHandler) // //log.Println("Starting server on:") //log.Printf("Serving on HTTP port: %d\n", port) diff --git a/reader/internal/handlers/handlers.go b/reader/internal/handlers/handlers.go index 0ab85cb..f64b25e 100644 --- a/reader/internal/handlers/handlers.go +++ b/reader/internal/handlers/handlers.go @@ -2,8 +2,19 @@ package handlers import ( "encoding/json" + "errors" + "fmt" "log" "net/http" + "os" + "strconv" + "strings" + "time" + + "git.insit.tech/psa/rtsp_reader-writer/writer/pkg/storage" + "go.uber.org/zap" + "reader/internal/config" + logger "reader/internal/log" "reader/internal/processor" ) @@ -13,6 +24,87 @@ type VideoRequest struct { EndTime string `json:"end_time"` } +type ListVodsResponse struct { + EstimatedCount int `json:"estimated_count"` // Estimated total number of records for the query. + Vods []string `json:"vods"` // List of vods. + Files map[string][]string `json:"files"` // List files and folders of a specific VOD location. +} + +type ConfigVodsResponse struct { + Prefix string `json:"prefix"` // The unique name of VOD location. + AutoMbr bool `json:"auto_mbr"` // Turns on automatic creation of a multi-bitrate HLS playlist from several files with different bitrates. + Disabled bool `json:"disabled"` // Whether this VOD location is disabled. + Protocols struct { // Configuraton of play protocols. + Hls bool `json:"hls"` // Whether to allow or deny an HLS stream playback. + Cmaf bool `json:"cmaf"` // Whether to allow or deny an LL-HLS stream playback. + Dash bool `json:"dash"` // Whether to allow or deny a DASH stream playback. + Player bool `json:"player"` // Whether to allow or deny playback in embed.html. + Mss bool `json:"mss"` // Whether to allow or deny an MSS stream playback. + Rtmp bool `json:"rtmp"` // Whether to allow or deny an RTMP stream playback. + Rtsp bool `json:"rtsp"` // Whether to allow or deny an RTSP stream playback. + M4F bool `json:"m4f"` // Whether to allow or deny an M4F stream playback. + M4S bool `json:"m4s"` // Whether to allow or deny an M4S stream playback. + Mseld bool `json:"mseld"` // Whether to allow or deny an MSE-LD stream playback. + Tshttp bool `json:"tshttp"` // Whether to allow or deny an MPEG-TS stream playback over HTTP(S). + Webrtc bool `json:"webrtc"` // Whether to allow or deny an WebRTC stream playback. + Srt bool `json:"srt"` // Whether to allow or deny an SRT stream playback. + Shoutcast bool `json:"shoutcast"` // Whether to allow or deny a SHOUTcast/Icecast stream playback. + Mp4 bool `json:"mp4"` // Whether to allow or deny an MP4 file download over HTTP(S). + Jpeg bool `json:"jpeg"` // Whether to allow or deny delivering JPEG thumbnails over HTTP(S). + Api bool `json:"api"` // Whether to allow or deny API requests. + } `json:"protocols"` + SegmentDuration int `json:"segment_duration"` // The time, in seconds, of the segment duration. Used for the protocols like HLS or DASH. + AddAudioOnly bool `json:"add_audio_only"` // Whether to add an audio-only version of an HLS stream. Used to create App Store compliant HLS streams to deliver the content to Apple iOS devices. Add audio-only HLS playlist to variant MBR playlist for iOS compliant streaming. + Provider string `json:"provider"` // Human-readable name of the content provider. Applicable to MPEG-TS. +} + +// Inactive5Minutes checks if a folder has files that was not created or modified for last 5 minutes. +func Inactive5Minutes(entries []os.DirEntry) (bool, error) { + threshold := time.Now().Add(-5 * time.Minute) + + disabled := true + + for _, entry := range entries { + info, err := entry.Info() + if err != nil { + return true, errors.New("Info error: " + err.Error()) + } + + if info.ModTime().After(threshold) { + disabled = false + return disabled, nil + } + } + + return true, nil +} + +// recDurationMilliseconds calculates the difference between first existing rec file and the last one in milliseconds. +func recDurationMilliseconds(entries []os.DirEntry) (int, error) { + // Find last file and first file; get timestamps. + lastFile := entries[len(entries)-1].Name() + lastTime := strings.Split(lastFile, "_")[0] + + firstFile := entries[0].Name() + firstTime := strings.Split(firstFile, "_")[0] + + // Convert string timestamps to int. + lastTimeInt, err := strconv.Atoi(lastTime) + if err != nil { + return 0, errors.New("convert last time error: " + err.Error()) + } + + firstTimeInt, err := strconv.Atoi(firstTime) + if err != nil { + return 0, errors.New("convert first time error: " + err.Error()) + } + + // Calculate the difference. + difference := lastTimeInt - firstTimeInt + + return difference * 1000, nil +} + // Download processes Download request. func Download(w http.ResponseWriter, r *http.Request) { log.Printf("new download request: %+v\n", r) @@ -50,22 +142,240 @@ func HLS(w http.ResponseWriter, r *http.Request) { http.StripPrefix("/hls", http.FileServer(http.Dir(path))).ServeHTTP(w, r) } -// -// vod -// -// GET List VOD locations -// GET List files in VOD locations which are played by the clients -// GET Get VOD location -// PUT Save VOD location -// DEL Delete VOD location -// GET List files in a VOD location -// GET Get a single VOD file -// PUT Save a VOD file -// DEL Delete a VOD file -// - -// List VOD locations +// ListVodsHandler returns the list of VOD locations. // // This method allows to get the list of all VOD locations. VOD location is a virtual filepath used to place files for // VOD (Video on Demand) broadcasting. +func ListVodsHandler(w http.ResponseWriter, r *http.Request) { + // Read directory. + entries, err := os.ReadDir(config.DirData) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + logger.Log.Error("failed to read dir", zap.Error(err)) + return + } + + // Filter only folders. + var dirs []string + for _, entry := range entries { + if entry.IsDir() { + dirs = append(dirs, entry.Name()) + } + } + + // Prepare the Response. + VodsRes := ListVodsResponse{ + EstimatedCount: len(dirs), + Vods: dirs, + } + + // Write header and code response. + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(VodsRes); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + logger.Log.Error("failed to encode dir", zap.Error(err)) + return + } +} + +// ConfigVodsHandler returns configuration of the requested VOD location. // +// This method allows to get a single VOD location. +func ConfigVodsHandler(w http.ResponseWriter, r *http.Request) { + // Read camera id. + id := r.PathValue("id") + + // Get resolutions. + resolutions, err := storage.GetResolutions(id) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + logger.Log.Error("camera does not exist", zap.Error(err)) + return + } + + // Calculate response fields. + + // Create path to first resolution. + resDir := fmt.Sprintf("%s/%s/%s", config.DirData, id, resolutions[0]) + + // Read directory. + entries, err := os.ReadDir(resDir) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + logger.Log.Error("resolution does not exist", zap.Error(err)) + return + } + + // Check if a folder has files that was not created or modified for last 5 minutes. + disabled, err := Inactive5Minutes(entries) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + + segmentDuration, err := recDurationMilliseconds(entries) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + + // Prepare the Response. + VodsRes := ConfigVodsResponse{ + Prefix: id, + AutoMbr: false, // Always false. Temporary. + Disabled: disabled, + SegmentDuration: segmentDuration, + AddAudioOnly: false, // Always false. Temporary. + Provider: "Insit", + } + + // Write header and code response. + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(VodsRes); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + logger.Log.Error("failed to encode dir", zap.Error(err)) + return + } + w.Write([]byte("Whole VOD location configuration")) +} + +// DelVodsHandler delete archive of the requested VOD location. +// +// This method delete a single VOD location by its prefix. +func DelVodsHandler(w http.ResponseWriter, r *http.Request) { + // Read camera id. + id := r.PathValue("id") + + err := os.Remove(fmt.Sprintf("%s/%s", config.DirData, id)) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + logger.Log.Error("camera does not exist", zap.Error(err)) + return + } + + w.WriteHeader(http.StatusNoContent) + w.Write([]byte("Deleted")) +} + +// ListFilesVodsHandler returns the list of all files and folders in archive for a specific VOD location. +// +// This method allows to get the list of all files and folders for a specific VOD location. +func ListFilesVodsHandler(w http.ResponseWriter, r *http.Request) { + // Read camera id. + id := r.PathValue("id") + + // Create map for response. + files := make(map[string][]string) + + // Get resolutions. + resolutions, err := storage.GetResolutions(id) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + logger.Log.Error("camera does not exist", zap.Error(err)) + return + } + + for _, resolution := range resolutions { + // Create path to the resolutions. + resDir := fmt.Sprintf("%s/%s/%s", config.DirData, id, resolution) + + // Read directory. + entries, err := os.ReadDir(resDir) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + logger.Log.Error("resolution does not exist", zap.Error(err)) + return + } + + // Create slice for files in folders. + filesInFolder := make([]string, 0) + + // Add all files to the slice with files. + for _, entry := range entries { + filesInFolder = append(filesInFolder, entry.Name()) + } + + // Add resolution and all files to the response map. + files[resolution] = filesInFolder + } + + // Prepare the Response. + vodsRes := ListVodsResponse{ + EstimatedCount: len(files), + Files: files, + } + + // Write header and code response. + w.Header().Set("Content-Type", "application/json") + w.Write([]byte("List of files in the VOD storage")) + if err := json.NewEncoder(w).Encode(vodsRes); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + logger.Log.Error("failed to encode dir", zap.Error(err)) + return + } +} + +// SingleVodsHandler returns a specific file in archive for a specific resolution and VOD location. +// +// This method allows to get a single VOD file. +func SingleVodsHandler(w http.ResponseWriter, r *http.Request) { + // Read camera id, res, filename. + id := r.PathValue("id") + res := r.PathValue("res") + file := r.PathValue("file") + + // Calculate file size in bytes. + FileBytes, err := storage.FileBytes(id, res, file) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + logger.Log.Error("file not found", zap.Error(err)) + return + } + + dur, tracks, err := storage.GetDurAndTracks(id, res, file) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + logger.Log.Error("file not found", zap.Error(err)) + return + } + + media := MediaSingleVodsResponse{ + Tracks: tracks, + Duration: dur, + Provider: "Insit", + Title: id, + } + + // Prepare the Response. + vodsRes := SingleVodsResponse{ + Name: file, + Prefix: id, + Url: r.URL.String(), + Folder: res, + Bytes: FileBytes, + MediaInfo: media, + } + + // Write header and code response. + w.Header().Set("Content-Type", "application/json") + w.Write([]byte("Whole VOD file configuration")) + if err := json.NewEncoder(w).Encode(vodsRes); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + logger.Log.Error("failed to encode dir", zap.Error(err)) + return + } +} + +type SingleVodsResponse struct { + Name string `json:"name"` + Prefix string `json:"prefix"` + Url string `json:"url"` + Folder string `json:"folder"` + Bytes int64 `json:"bytes"` + MediaInfo MediaSingleVodsResponse `json:"media_info"` +} + +type MediaSingleVodsResponse struct { + Tracks map[string]string `json:"tracks"` + Duration int `json:"duration"` + Provider string `json:"provider"` + Title string `json:"title"` +}