401 lines
13 KiB
Go

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"))
}