Add handlers.

This commit is contained in:
Сергей Петров 2025-04-09 17:58:24 +05:00
parent 523d313642
commit 7b49753570
2 changed files with 327 additions and 16 deletions

View File

@ -19,7 +19,8 @@ func main() {
//http.HandleFunc("GET /api/v1/vods/{id}/", handlers.ConfigVodsHandler) //http.HandleFunc("GET /api/v1/vods/{id}/", handlers.ConfigVodsHandler)
//http.HandleFunc("DELETE /api/v1/vods/{id}/", handlers.DelVodsHandler) //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}/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.Println("Starting server on:")
//log.Printf("Serving on HTTP port: %d\n", port) //log.Printf("Serving on HTTP port: %d\n", port)

View File

@ -2,8 +2,19 @@ package handlers
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt"
"log" "log"
"net/http" "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" "reader/internal/processor"
) )
@ -13,6 +24,87 @@ type VideoRequest struct {
EndTime string `json:"end_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.
}
// 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. // Download processes Download request.
func Download(w http.ResponseWriter, r *http.Request) { func Download(w http.ResponseWriter, r *http.Request) {
log.Printf("new download request: %+v\n", r) 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) http.StripPrefix("/hls", http.FileServer(http.Dir(path))).ServeHTTP(w, r)
} }
// // ListVodsHandler returns the list of VOD locations.
// 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
// //
// This method allows to get the list of all VOD locations. VOD location is a virtual filepath used to place files for // 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. // 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"`
}