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" ) type VideoRequest struct { Date string `json:"date"` StartTime string `json:"start_time"` 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. } 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"` } // 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) downloadRequest := VideoRequest{} err := json.NewDecoder(r.Body).Decode(&downloadRequest) if err != nil { log.Printf("json decode error: %v\n", err) w.WriteHeader(http.StatusBadRequest) return } pathFileNameRes, err := processor.Process(downloadRequest.Date, downloadRequest.StartTime, downloadRequest.EndTime) if err != nil { log.Printf("process error: %v\n", err) w.WriteHeader(http.StatusBadRequest) return } w.Header().Set("Content-Type", "video/mp4") // Разрешаем частичную загрузку (поддержка перемотки) w.Header().Set("Accept-Ranges", "bytes") http.ServeFile(w, r, pathFileNameRes) } // HLS processes Download request. func HLS(w http.ResponseWriter, r *http.Request) { log.Printf("new hls request: %+v\n", r) path := "/home/psa/GoRepository/data/1280x720/" w.Header().Set("Access-Control-Allow-Origin", "*") http.StripPrefix("/hls", http.FileServer(http.Dir(path))).ServeHTTP(w, r) } // 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 } } // DelSingleVodsHandler deletes a VOD file by its name. // // This method deletes a VOD file by its name. func DelSingleVodsHandler(w http.ResponseWriter, r *http.Request) { // Read camera id, res, filename. id := r.PathValue("id") res := r.PathValue("res") file := r.PathValue("file") if err := os.Remove(fmt.Sprintf("%s/%s/%s/%s", config.DirData, id, res, file)); err != nil { http.Error(w, err.Error(), http.StatusNotFound) logger.Log.Error("file does not exist", zap.Error(err)) return } w.WriteHeader(http.StatusNoContent) w.Write([]byte("Deleted")) }