621 lines
17 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package storage
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"io"
"log"
"os"
"os/exec"
"strconv"
"strings"
"time"
"git.insit.tech/psa/rtsp_reader-writer/writer/internal/config"
)
// Constants for detection video and audio types.
const (
PacketTypeAV1 = 1
PacketTypeH264 = 2
PacketTypeH265 = 3
PacketTypeMJPEG = 4
PacketTypeVP8 = 5
PacketTypeVP9 = 6
PacketTypeG711 = 21
PacketTypeAAC = 22
PacketTypeLPCM = 23
PacketTypeOPUS = 24
)
// InterleavedPacket is the structure of each inner packet.
type InterleavedPacket struct {
Type byte
Pts int64
H264AUs [][]byte
LPCMSamples []byte
}
// Segment is overall structure according to each period.
type Segment struct {
Date string
Duration string
Packet InterleavedPacket
Packets []InterleavedPacket
}
// writeString записывает строку: сначала int32 длина, затем данные.
func writeString(w io.Writer, s string) error {
if err := binary.Write(w, binary.LittleEndian, int32(len(s))); err != nil {
return err
}
_, err := w.Write([]byte(s))
return err
}
// WritePacket записывает один interleaved пакет.
func WritePacket(w io.Writer, pkt InterleavedPacket) error {
// Записываем тип пакета (1 байт)
if err := binary.Write(w, binary.LittleEndian, pkt.Type); err != nil {
return err
}
// Записываем pts (int64)
if err := binary.Write(w, binary.LittleEndian, pkt.Pts); err != nil {
return err
}
if pkt.Type == PacketTypeH264 {
// Для H264 AU как [][]byte.
numAUs := int32(len(pkt.H264AUs))
if err := binary.Write(w, binary.LittleEndian, numAUs); err != nil {
return err
}
for _, au := range pkt.H264AUs {
if err := binary.Write(w, binary.LittleEndian, int32(len(au))); err != nil {
return err
}
if _, err := w.Write(au); err != nil {
return err
}
}
} else if pkt.Type == PacketTypeLPCM {
// Для LPCM просто длина и данные.
if err := binary.Write(w, binary.LittleEndian, int32(len(pkt.LPCMSamples))); err != nil {
return err
}
if _, err := w.Write(pkt.LPCMSamples); err != nil {
return err
}
} else {
return fmt.Errorf("неизвестный тип пакета: %d", pkt.Type)
}
return nil
}
// WriteHeader записывает заголовок сегмента (Start и Duration).
func WriteHeader(w io.Writer, seg Segment) error {
var buf bytes.Buffer
if err := writeString(&buf, seg.Date); err != nil {
return err
}
if err := writeString(&buf, seg.Duration); err != nil {
return err
}
segData := buf.Bytes()
if err := binary.Write(w, binary.LittleEndian, int32(len(segData))); err != nil {
return err
}
_, err := w.Write(segData)
return err
}
// WriteInterleavedPacket записывает сегмент с пакетами.
// Теперь количество пакетов определяется динамически.
func WriteInterleavedPacket(w io.Writer, seg Segment) error {
var buf bytes.Buffer
if err := binary.Write(&buf, binary.LittleEndian, int32(1)); err != nil {
return err
}
// Записываем каждый пакет.
if err := WritePacket(&buf, seg.Packet); err != nil {
return err
}
segData := buf.Bytes()
if err := binary.Write(w, binary.LittleEndian, int32(len(segData))); err != nil {
return err
}
_, err := w.Write(segData)
return err
}
// partitionTime convert numbers from type STRING to type INT.
func partitionTime(time string) (startHour int, startMinute int, err error) {
// Part hours and minutes.
s := []byte(time)
h := []byte{s[0], s[1]}
m := []byte{s[3], s[4]}
// Transforms hours and minutes into INTs.
startHour, err = strconv.Atoi(string(h))
if err != nil {
return 0, 0, err
}
startMinute, err = strconv.Atoi(string(m))
if err != nil {
return 0, 0, err
}
return startHour, startMinute, nil
}
// CreateTXT collects filenames into TXT file.
func CreateTXT(path, date, startTime, endTime string) (string, error) {
//fileNames := make([]string, 0)
fileNameTXT := startTime + "_" + endTime + ".txt"
f, err := os.Create(path + fileNameTXT)
if err != nil {
return "", fmt.Errorf("create file error: %s", err.Error())
}
defer f.Close()
// Read the directory.
dirEntry, err := os.ReadDir(path)
if err != nil {
return "", fmt.Errorf("read directory error: %s", err.Error())
}
// Calculate start time and end time of the required fragment.
startHour, startMinute, endHour, endMinute := calcNeededTime(startTime, endTime)
for i := startHour; i <= endHour; i++ {
for j := startMinute; j < endMinute; j++ { // exclude the last minute as endMinute is the final time
for _, entry := range dirEntry {
if strings.Contains(entry.Name(), strconv.Itoa(i)+"-"+strconv.Itoa(j)+"-00"+"_"+date) {
_, err = f.WriteString("file '" + entry.Name() + "'\n")
if err != nil {
return "", fmt.Errorf("write file error: %s", err.Error())
}
}
}
}
}
return fileNameTXT, nil
}
// MergeFiles merges the collected files into one MP4 file.
func MergeFiles(path, fileNamesTXT string) (string, error) {
fileNameRes := time.Now().Format("15-04-05") + "_video.mp4"
cmd := exec.Command("ffmpeg",
"-f", "concat",
"-safe", "0",
"-fflags", "+genpts",
"-i", path+fileNamesTXT,
"-c", "copy",
path+fileNameRes)
err := cmd.Run()
if err != nil {
return "", fmt.Errorf("merge files error: %s", err.Error())
}
return fileNameRes, nil
}
/////////////////////////////////////////////////////////////////////////////////////
//// Функция copyFile находит файл в репозитории и создает его копию.
//func copyFiles(path string, filename []string) (filenameCopied []string, err error) {
// for i := 0; i < len(filename); i++ {
// input, err := os.Open(path + filename[i])
// if err != nil {
// log.Println("Ошибка открытия input файла: ", err)
// }
// defer func() {
// err = input.Close()
// if err != nil {
// log.Println("Ошибка закрытия input файла.")
// }
// }()
//
// output, err := os.Create(time.Now().Format("15-04-05") + "video.mkv")
// if err != nil {
// log.Println("Ошибка создания output файла: ", err)
// }
// defer func() {
// err = output.Close()
// if err != nil {
// log.Println("Ошибка закрытия output файла.")
// }
//
// }()
//
// _, err = io.Copy(output, input)
// if err != nil {
// log.Println("Ошибка копирования файла")
// }
// }
//
// log.Println("Файлы скопированы.")
// return filename, err
//}
//
//// MergeMKV принимает названия видеофайлов для объединения.
//func MergeMKV(filenames ...string) {
// // Создание файла со списком видеофайлов для объединения
// f, err := os.Create("videoList.txt")
// if err != nil {
// log.Fatalln("Ошибка создания файла со списком видеофайлов для объединения", err)
// }
// defer func() {
// err = f.Close()
// if err != nil {
// log.Fatalln("Ошибка закрытия файла: ", err.Error())
// }
// }()
//
// // Запись видеофайлов для объединения
// n := len(filenames)
//
// for i := 0; i < n; i++ {
// _, err = f.WriteString("file '" + filenames[i] + "'\n")
// if err != nil {
// log.Fatalln(err)
// }
// }
//
// err = mergeFfmpeg(f)
// if err != nil {
// log.Fatalln("Ошибка объединения видеофайлов с помощью ffmpeg: ", err)
// }
//
// err = os.Remove("videoList.txt")
// if err != nil {
// log.Println("Ошибка удаления временного файла videoList.txt: ", err)
// }
// log.Println("Временный файл videoList.txt успешно удален")
//}
//
//// DeleteTrimmedFragments удаляет временно созданные файлы.
//func DeleteTrimmedFragments(filenames ...string) {
// n := len(filenames)
//
// for i := 0; i < n; i++ {
// err := os.Remove(filenames[i])
// if err != nil {
// log.Printf("Ошибка удаления временного файла %s: %s", filenames[i], err.Error())
// }
// log.Printf("Временный файл %s успешно удален", filenames[i])
// }
//}
//
//
//// createFilename forms first part of the filename which contains time and date.
//func createFilename(time, date string) (fileName string) {
// s := []byte(time)
// s[3], s[4] = '0', '0'
// fileName = string(s) + "_" + date
// return fileName
//}
//
//// Добавление одного часа к строке в формате ЧЧ-ММ
//func addHour(startTime string) (fileName string) {
// s := []byte(startTime)
// if s[0] == '0' && s[1] == '9' {
// s[0] = '1'
// s[1] = '0'
// } else if s[0] == '1' && s[1] == '9' {
// s[0] = '2'
// s[1] = '0'
// } else {
// s[1]++
// }
// fileName = string(s)
// return fileName
//}
//
//// Проверка наличия файла
//func checkFile(path, fileName string) (string, error) {
// found := false
// filenameIn := fileName
//
// dirEntry, err := os.ReadDir(path)
// if err != nil {
// return "", err
// }
// for _, entry := range dirEntry {
// if strings.Contains(entry.Name(), filenameIn) {
// filenameIn = entry.Name()
// break
// }
// return "", fmt.Errorf("file %s not found", filenameIn)
// }
//
// err = filepath.Walk(path, func(filePath string, info os.FileInfo, err error) error {
// if err != nil {
// return err // Возвращаем ошибку, если возникла
// }
// if !info.IsDir() && info.Name() == filenameIn {
// log.Println("Файл обнаружен:", filePath)
// found = true
// return filepath.SkipDir // Останавливаем обход после нахождения
// }
// return nil
// })
//
// if err != nil {
// return "", err
// }
//
// if !found {
// return "", errors.New("file not found")
// }
//
// return filenameIn, nil
//}
// GetResolutions parses all resolutions of a camera.
func GetResolutions(file string) ([]string, error) {
res, err := ReadDir(fmt.Sprintf("%s/%s", config.DirData, file))
if err != nil {
return nil, err
}
resolutions := make([]string, 0)
for _, r := range res {
if r.IsDir() && r.Name() != "log" {
resolutions = append(resolutions, r.Name())
}
}
return resolutions, nil
}
// UnixToTime transfers Unix time to the formatted time.
func UnixToTime(unixTimeInt int) (string, error) {
t := time.Unix(int64(unixTimeInt), 0)
return t.Format("15-04-05_02-01-2006"), nil
}
// FileBytes returns file size in bytes.
func FileBytes(id, res, file string) (int64, error) {
filePath := fmt.Sprintf("%s/%s/%s/%s", config.DirData, id, res, file)
info, err := os.Stat(filePath)
if err != nil {
return 0, err
}
size := info.Size()
return size, nil
}
// GetDurAndTracks reads .insit file and returns duration and tracks of the file.
func GetDurAndTracks(id, res, file string) (int, map[string]string, error) {
// Open file for reading.
f, err := os.Open(fmt.Sprintf("%s/%s/%s/%s", config.DirData, id, res, file))
if err != nil {
return 0, nil, errors.New("opening file error for file: " + err.Error())
}
defer f.Close()
// Read StreamID.
var streamIDLen int32
if err := binary.Read(f, binary.LittleEndian, &streamIDLen); err != nil {
return 0, nil, errors.New("reading StreamID length error: " + err.Error())
}
streamIDBytes := make([]byte, streamIDLen)
if _, err := io.ReadFull(f, streamIDBytes); err != nil {
return 0, nil, errors.New("reading StreamID error: " + err.Error())
}
// Read header of the file.
var segLen int32
if err := binary.Read(f, binary.LittleEndian, &segLen); err != nil {
return 0, nil, errors.New("reading header length error: " + err.Error())
}
segData := make([]byte, segLen)
if _, err := io.ReadFull(f, segData); err != nil {
return 0, nil, errors.New("reading header error: " + err.Error())
}
headerReader := bytes.NewReader(segData)
headerSeg, err := ReadHeaderSegment(headerReader)
if err != nil {
return 0, nil, errors.New("reading header error: " + err.Error())
}
// Parse duration of a segment.
per, err := strconv.Atoi(headerSeg.Duration)
if err != nil {
return 0, nil, errors.New("parsing duration error: " + err.Error())
}
tracks := make(map[string]string, 2)
for {
// Read segments length.
var segLen int32
err := binary.Read(f, binary.LittleEndian, &segLen)
if err != nil {
if err == io.EOF {
break
}
return 0, nil, errors.New("read segments length error: " + err.Error())
}
// Read segments data.
segData = make([]byte, segLen)
if _, err := io.ReadFull(f, segData); err != nil {
return 0, nil, errors.New("read segments error: " + err.Error())
}
packetReader := bytes.NewReader(segData)
packets, err := ReadPacketSegment(packetReader)
if err != nil {
return 0, nil, errors.New("func readPacketSegment error: " + err.Error())
}
for _, pkt := range packets {
switch pkt.Type {
case PacketTypeH264:
tracks["video"] = "H264"
case PacketTypeLPCM:
tracks["audio"] = "LPCM"
}
}
}
return per, tracks, nil
}
// readString reads string length and then reads string data.
func readString(r io.Reader) (string, error) {
var length int32
if err := binary.Read(r, binary.LittleEndian, &length); err != nil {
return "", err
}
buf := make([]byte, length)
if _, err := io.ReadFull(r, buf); err != nil {
return "", err
}
return string(buf), nil
}
// ReadHeaderSegment reads header of the segment.
func ReadHeaderSegment(r io.Reader) (Segment, error) {
var seg Segment
date, err := readString(r)
if err != nil {
return seg, err
}
seg.Date = date
duration, err := readString(r)
if err != nil {
return seg, err
}
seg.Duration = duration
return seg, nil
}
// readPacket reads one interleaved packet.
func readPacket(r io.Reader) (InterleavedPacket, error) {
var pkt InterleavedPacket
// Read type of the packet.
typeByte := make([]byte, 1)
if _, err := io.ReadFull(r, typeByte); err != nil {
return pkt, err
}
pkt.Type = typeByte[0]
// Read PTS (int64).
if err := binary.Read(r, binary.LittleEndian, &pkt.Pts); err != nil {
return pkt, err
}
// Read data of the segment.
if pkt.Type == PacketTypeH264 {
var numAUs int32
if err := binary.Read(r, binary.LittleEndian, &numAUs); err != nil {
return pkt, err
}
var auList [][]byte
for i := 0; i < int(numAUs); i++ {
var auLen int32
if err := binary.Read(r, binary.LittleEndian, &auLen); err != nil {
return pkt, err
}
auData := make([]byte, auLen)
if _, err := io.ReadFull(r, auData); err != nil {
return pkt, err
}
auList = append(auList, auData)
}
pkt.H264AUs = auList
} else if pkt.Type == PacketTypeLPCM {
var auLen int32
if err := binary.Read(r, binary.LittleEndian, &auLen); err != nil {
return pkt, err
}
auData := make([]byte, auLen)
if _, err := io.ReadFull(r, auData); err != nil {
return pkt, err
}
pkt.LPCMSamples = auData
} else {
return pkt, fmt.Errorf("unknown type of the packet: %d", pkt.Type)
}
return pkt, nil
}
// ReadPacketSegment reads segment packets.
func ReadPacketSegment(r io.Reader) ([]InterleavedPacket, error) {
var numPackets int32
if err := binary.Read(r, binary.LittleEndian, &numPackets); err != nil {
return nil, err
}
var packets []InterleavedPacket
for i := 0; i < int(numPackets); i++ {
pkt, err := readPacket(r)
if err != nil {
return nil, err
}
packets = append(packets, pkt)
}
return packets, nil
}
// calcNeededTime accepts the start and the end of the recording time, converts the time from the format STRING to the
// format INT and returns the hour and the minute of the start recording time, the hour and the minute of the end
// recording time.
func calcNeededTime(startTime, endTime string) (startHour, startMinute, endHour, endMinute int) {
// Calc needed time.
startHour, startMinute, err := partitionTime(startTime)
if err != nil {
log.Fatal("Ошибка конвертации: ", err)
}
endHour, endMinute, err = partitionTime(endTime)
if err != nil {
log.Fatal("Ошибка конвертации: ", err)
}
return startHour, startMinute, endHour, endMinute
}
/////////////////////////////////////////////////////////////////////////////////////
//// CalcEndMinuteFirstVideo calculates the need to change the hour (switching one fragment of video recording (which
//// lasts 1 hour) to another fragment of video recording) and returns the number of hours (required number of video
//// fragments), the duration of minutes (the object responsible for the indicator of minutes) of each fragment for the
//// formation of the final video (except for the last video fragment, provided DurationHour > 0 (the third object
//// returned by the CalcEndMinuteFirstVideo function)).
////
//// In the case that you need to take a full fragment of the video file, for example, from 00-00 to 03-00, the
//// DurationHour value is conciliated by 1 and returned.
//func CalcEndMinuteFirstVideo(durationHour, endMinuteFirstVideo, startHour, endHour, endMinute int) (
// durationHourCalc int, endMinuteFirstVideoCalc int) {
// durationHour = endHour - startHour
//
// if durationHour > 0 {
// endMinuteFirstVideo = 60
// } else {
// endMinuteFirstVideo = endMinute
// }
//
// if endMinute == 0 && durationHour > 0 {
// durationHour -= 1
// }
//
// return durationHour, endMinuteFirstVideo
//}