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 //}