382 lines
12 KiB
Go
382 lines
12 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.
|
|
}
|
|
|
|
// 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
|
|
}
|
|
}
|
|
|
|
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"`
|
|
}
|