From dc09904c1200360384ed02471a93c7acc8c27bff Mon Sep 17 00:00:00 2001 From: Sergey Petrov Date: Fri, 7 Mar 2025 14:29:49 +0500 Subject: [PATCH] create TS files instead of mp4; using another libraries for processing RTP packets; changed project architecture; added dependence to teh private library. --- writer/cmd/main.go | 637 +-------------------------- writer/go.mod | 11 +- writer/go.sum | 30 +- writer/internal/config/config.go | 31 ++ writer/internal/media/g711.go | 42 ++ writer/internal/media/h264.go | 43 ++ writer/internal/procRTSP/client.go | 180 ++++++++ writer/pkg/converter/mpegts_muxer.go | 143 ++++++ writer/pkg/converter/pcm_to_aac.go | 37 ++ writer/pkg/packer/mp4.go | 364 +++++++++++++++ 10 files changed, 875 insertions(+), 643 deletions(-) create mode 100644 writer/internal/config/config.go create mode 100644 writer/internal/media/g711.go create mode 100644 writer/internal/media/h264.go create mode 100644 writer/internal/procRTSP/client.go create mode 100644 writer/pkg/converter/mpegts_muxer.go create mode 100644 writer/pkg/converter/pcm_to_aac.go create mode 100644 writer/pkg/packer/mp4.go diff --git a/writer/cmd/main.go b/writer/cmd/main.go index bfefd62..616b8ce 100644 --- a/writer/cmd/main.go +++ b/writer/cmd/main.go @@ -1,641 +1,10 @@ package main -import ( - "bytes" - "errors" - "log" - "os" - "strings" - "sync" - "time" - - "github.com/Eyevinn/mp4ff/mp4" - "github.com/bluenviron/gortsplib/v4" - "github.com/bluenviron/gortsplib/v4/pkg/base" - "github.com/bluenviron/gortsplib/v4/pkg/description" - "github.com/bluenviron/gortsplib/v4/pkg/format" - "github.com/bluenviron/gortsplib/v4/pkg/format/rtph264" - "github.com/gen2brain/aac-go" - "github.com/pion/rtp" - "github.com/zaf/g711" -) - -var ( - currentVideoFile *os.File - currentAudioFile *os.File - currentMP4File *os.File - wg sync.WaitGroup - fileMu sync.Mutex - formaH264Mu sync.Mutex - formaH264 *format.H264 - currentSegmentStarted bool // Флаг указывает записан ли уже IDR (с SPS/PPS) в текущий сегмент -) +import "writer/internal/procRTSP" func main() { - // Парсинг URL RTSP - u, err := base.ParseURL("rtsp://intercom-video-1.insit.ru/dp-gfahybswjoendkcpbaqvgkeizruudbhsfdr") + err := procRTSP.ProcRTSP(1, "rtsp://intercom-video-2.insit.ru/dp-ohusuxzcvzsnpzzvkpyhddnwxuyeyc") if err != nil { - log.Fatalln("Ошибка парсинга URL:", err) - } - - // Инициализация клиента и подключение к камере - client := gortsplib.Client{} - err = client.Start(u.Scheme, u.Host) - if err != nil { - log.Fatalln("Ошибка соединения:", err) - } - defer client.Close() - - // Получение описания - desc, _, err := client.Describe(u) - if err != nil { - log.Fatalln("Ошибка получения описания:", err) - } - - // Установка настроек - err = client.SetupAll(desc.BaseURL, desc.Medias) - if err != nil { - log.Fatalln("Ошибка настройки:", err) - } - - // Проверка форматов для видео- и аудио-потоков - var h264Format *format.H264 - var g711Format *format.G711 - for _, media := range desc.Medias { - switch media.Formats[0].(type) { - case *format.H264: - h264Format = media.Formats[0].(*format.H264) - case *format.G711: - g711Format = media.Formats[0].(*format.G711) - } - } - if h264Format == nil || g711Format == nil { - log.Fatalln("Форматы не найдены") - } - - // Создание декодера для видео-потока H264 - decoderH264, err := h264Format.CreateDecoder() - if err != nil { - log.Fatalln("Ошибка создания декодера H264:", err) - } - - // Обработка RTP-пакетов: для видео – сбор NAL-ов и запись в файл с префиксом, для аудио – запись PCM-данных. - client.OnPacketRTPAny(func(medi *description.Media, forma format.Format, pkt *rtp.Packet) { - switch f := forma.(type) { - case *format.H264: - formaH264Mu.Lock() - formaH264 = f - formaH264Mu.Unlock() - - nalus, err := decoderH264.Decode(pkt) - if err != nil && !errors.Is(err, rtph264.ErrMorePacketsNeeded) { - log.Printf("Ошибка декодирования H264: %v", err) - return - } - - fileMu.Lock() - if currentVideoFile != nil { - // Если сегмент ещё не начат, ожидается появление ключевого кадра (IDR). - if !currentSegmentStarted { - var isIDR bool - for _, nalu := range nalus { - if len(nalu) > 0 && (nalu[0]&0x1F) == 5 { - isIDR = true - break - } - } - if !isIDR { - // Пакет не записывается, если в нём нет ключевого кадра. - fileMu.Unlock() - return - } - // При получении IDR происходит вставка SPS и PPS (с префиксами) в начало сегмента. - if len(f.SPS) > 0 { - if _, err := currentVideoFile.Write([]byte{0x00, 0x00, 0x00, 0x01}); err != nil { - log.Printf("Ошибка записи стартового кода SPS: %v", err) - } - if _, err := currentVideoFile.Write(f.SPS); err != nil { - log.Printf("Ошибка записи SPS: %v", err) - } - } - if len(f.PPS) > 0 { - if _, err := currentVideoFile.Write([]byte{0x00, 0x00, 0x00, 0x01}); err != nil { - log.Printf("Ошибка записи стартового кода PPS: %v", err) - } - if _, err := currentVideoFile.Write(f.PPS); err != nil { - log.Printf("Ошибка записи PPS: %v", err) - } - } - currentSegmentStarted = true - } - // Запись каждой NAL-единицы с префиксом - for _, nalu := range nalus { - if _, err := currentVideoFile.Write([]byte{0x00, 0x00, 0x00, 0x01}); err != nil { - log.Printf("Ошибка записи стартового кода NALU: %v", err) - } - if _, err := currentVideoFile.Write(nalu); err != nil { - log.Printf("Ошибка записи NALU: %v", err) - } - } - } - fileMu.Unlock() - - case *format.G711: - if strings.Contains(forma.RTPMap(), "PCMA") { - sampleG711a := g711.DecodeAlaw(pkt.Payload) - fileMu.Lock() - if currentAudioFile != nil { - if _, err := currentAudioFile.Write(sampleG711a); err != nil { - log.Printf("Ошибка записи аудио: %v", err) - } - } - fileMu.Unlock() - } else if strings.Contains(forma.RTPMap(), "PCMU") { - sampleG711u := g711.DecodeUlaw(pkt.Payload) - fileMu.Lock() - if currentAudioFile != nil { - if _, err := currentAudioFile.Write(sampleG711u); err != nil { - log.Printf("Ошибка записи аудио: %v", err) - } - } - fileMu.Unlock() - } else { - log.Println("Аудиокодек не идентифицирован") - } - } - }) - - // Запуск воспроизведения - _, err = client.Play(nil) - if err != nil { - log.Fatalln("Ошибка запуска воспроизведения:", err) - } - - // Параметр записи - period := time.Hour - - // Ожидание начала следующего часа - now := time.Now() - nextSegment := now.Truncate(time.Hour).Add(time.Hour) - waitDuration := nextSegment.Sub(now) - log.Printf("Ожидание до начала записи: %v\n", waitDuration) - time.Sleep(waitDuration) - log.Println("Начало записи фрагмента") - - // Создание начальных файлов - initialTimestamp := time.Now().Format("15-04_02-01-2006") - videoFilename := initialTimestamp + ".h264" - audioFilename := initialTimestamp + ".pcm" - mp4Filename := initialTimestamp + ".mp4" - - fileVideo, err := os.Create(videoFilename) - if err != nil { - log.Fatalln("Ошибка создания видеофайла:", err) - } - fileAudio, err := os.Create(audioFilename) - if err != nil { - log.Fatalln("Ошибка создания аудиофайла:", err) - } - fileMP4, err := os.Create(mp4Filename) - if err != nil { - log.Fatalln("Ошибка создания mp4 файла:", err) - } - - fileMu.Lock() - currentVideoFile = fileVideo - currentAudioFile = fileAudio - currentMP4File = fileMP4 - - // Начало нового сегмента – флаг сбрасывается и ожидается первый IDR - currentSegmentStarted = false - fileMu.Unlock() - - // Тикер для периодической ротации файлов - ticker := time.NewTicker(period) - defer ticker.Stop() - - // Горутина для смены файлов - go func() { - for range ticker.C { - newTimestamp := time.Now().Format("15-04_02-01-2006") - newVideoFilename := newTimestamp + ".h264" - newAudioFilename := newTimestamp + ".pcm" - newMP4Filename := newTimestamp + ".mp4" - - newVideoFile, err := os.Create(newVideoFilename) - if err != nil { - log.Printf("Ошибка создания видеофайла для фрагмента %s: %v", newTimestamp, err) - continue - } - newAudioFile, err := os.Create(newAudioFilename) - if err != nil { - log.Printf("Ошибка создания аудиофайла для фрагмента %s: %v", newTimestamp, err) - continue - } - newMP4File, err := os.Create(newMP4Filename) - if err != nil { - log.Printf("Ошибка создания mp4 файла для фрагмента %s: %v", newTimestamp, err) - } - - // Переключение на новые файлы - fileMu.Lock() - oldVideoFile := currentVideoFile - oldAudioFile := currentAudioFile - oldMP4File := currentMP4File - - oldVideoFilename := videoFilename - oldAudioFilename := audioFilename - oldMP4Filename := mp4Filename - - currentVideoFile = newVideoFile - currentAudioFile = newAudioFile - currentMP4File = newMP4File - - // Новый сегмент ещё не начат – флаг сбрасывается и ожидается первый IDR - currentSegmentStarted = false - - // Обновление имен для следующей итерации - videoFilename = newVideoFilename - audioFilename = newAudioFilename - mp4Filename = newMP4Filename - fileMu.Unlock() - - // Закрытие старых файлов - oldVideoFile.Close() - oldAudioFile.Close() - oldMP4File.Close() - - log.Println("Созданы новые файлы для записи:", videoFilename, audioFilename, mp4Filename) - - // Горутина для объединения видео и аудио в контейнер MP4 - go func(oldVideoFilename, oldAudioFilename, oldMP4Filename string) { - formaH264Mu.Lock() - h264Params := formaH264 - formaH264Mu.Unlock() - - // Конвертация PCM в AAC - aacFilename := strings.Replace(oldAudioFilename, ".pcm", ".aac", 1) - if err := convertPCMtoAAC(oldAudioFilename, aacFilename); err != nil { - log.Printf("Ошибка конвертации аудио: %v", err) - return - } - - log.Printf("Файл %s успешно конвертирован в %s\n", oldAudioFilename, aacFilename) - - // Создание MP4 файл - if err := createMP4File(oldVideoFilename, aacFilename, oldMP4Filename, h264Params); err != nil { - log.Printf("Ошибка создания MP4: %v", err) - } else { - log.Printf("Файлы %s, %s успешно объединены в контейнер MP4: %s\n", - oldVideoFilename, aacFilename, oldMP4Filename) - } - - // Удаление временных файлов - os.Remove(oldVideoFilename) - os.Remove(aacFilename) - os.Remove(oldAudioFilename) - - log.Printf("Файлы %s, %s, %s успешно удалены\n", oldVideoFilename, aacFilename, oldAudioFilename) - }(oldVideoFilename, oldAudioFilename, oldMP4Filename) - } - }() - - wg.Add(1) - wg.Wait() -} - -// Sample – единый тип для описания сэмпла (для видео и аудио) -type Sample struct { - Data []byte - Offset uint64 - Size uint32 - Dur uint32 - IsSync bool -} - -// convertPCMtoAAC конвертирует PCM-файл в AAC-файл. -func convertPCMtoAAC(aFilename, outputFile string) error { - if outputFile == "" { - outputFile = strings.Replace(aFilename, ".pcm", ".aac", 1) - } - - audioFileAAC, err := os.Create(outputFile) - if err != nil { - log.Println("Ошибка создания файла AAC:", err) - return err - } - defer audioFileAAC.Close() - - audioFilePCM, err := os.Open(aFilename) - if err != nil { - log.Println("Ошибка открытия PCM файла:", err) - return err - } - defer audioFilePCM.Close() - - opts := &aac.Options{ - SampleRate: 8000, - NumChannels: 1, - } - - encoderAAC, err := aac.NewEncoder(audioFileAAC, opts) - if err != nil { - log.Println("Ошибка создания инкодера AAC:", err) - return err - } - defer encoderAAC.Close() - - if err = encoderAAC.Encode(audioFilePCM); err != nil { - log.Println("Ошибка инкодирования в AAC:", err) - return err - } - - if err = os.Remove(aFilename); err != nil { - log.Println("Ошибка удаления PCM файла:", err) - } else { - log.Printf("PCM файл %s успешно удалён.", aFilename) - } - return nil -} - -// createMP4File упаковывает видео (H.264) и аудио (AAC) файлы в MP4. -func createMP4File(videoFile, audioFile, outputFile string, formaH264 *format.H264) error { - videoData, err := os.ReadFile(videoFile) - if err != nil { - return err - } - videoSamples := parseH264Samples(videoData) - - audioData, err := os.ReadFile(audioFile) - if err != nil { - return err - } - audioSamples := parseAACSamples(audioData) - - f := mp4.NewFile() - - videoTrack := createVideoTrack(formaH264.SPS, formaH264.PPS) - audioTrack := createAudioTrack(8000, 1) - - addSamplesToVideoTrack(videoTrack, videoSamples) - addSamplesToAudioTrack(audioTrack, audioSamples, 8000) - - moov := mp4.NewMoovBox() - moov.AddChild(videoTrack) - moov.AddChild(audioTrack) - f.AddChild(moov, 0) - - mdat := mp4.MdatBox{} - mdat.Data = append(videoData, audioData...) - f.AddChild(&mdat, 0) - - outFile, err := os.Create(outputFile) - if err != nil { - return err - } - defer outFile.Close() - return f.Encode(outFile) -} - -// createVideoTrack создаёт видео-трек на основе SPS и PPS. -func createVideoTrack(sps, pps []byte) *mp4.TrakBox { - sps = stripAnnexB(sps) - pps = stripAnnexB(pps) - - if len(sps) < 4 { - log.Fatalln("SPS слишком короткий или пустой") - } - if len(pps) < 1 { - log.Fatalln("PPS слишком короткий или пустой") - } - - trak := mp4.NewTrakBox() - timescale := uint32(90000) - - mdhd := &mp4.MdhdBox{ - Timescale: timescale, - Language: convertLanguage("und"), - } - - hdlr := &mp4.HdlrBox{ - HandlerType: "vide", - Name: "VideoHandler", - } - - avc1 := mp4.NewVisualSampleEntryBox("avc1") - avc1.Width = 1280 - avc1.Height = 720 - - avcC, err := mp4.CreateAvcC([][]byte{sps}, [][]byte{pps}, true) - if err != nil { - log.Fatalf("ошибка создания avcC: %v", err) - } - avcC.AVCLevelIndication = 0x1f - avc1.AddChild(avcC) - - stbl := mp4.NewStblBox() - stsd := mp4.NewStsdBox() - stsd.AddChild(avc1) - stbl.AddChild(stsd) - stbl.AddChild(&mp4.SttsBox{}) - stbl.AddChild(&mp4.StscBox{}) - stbl.AddChild(&mp4.StszBox{}) - stbl.AddChild(&mp4.StcoBox{}) - - trak.Mdia = &mp4.MdiaBox{ - Mdhd: mdhd, - Hdlr: hdlr, - Minf: &mp4.MinfBox{ - Stbl: stbl, - }, - } - - return trak -} - -// stripAnnexB удаляет стартовый код Annex-B из NAL-единицы. -func stripAnnexB(nalu []byte) []byte { - if len(nalu) >= 4 && bytes.Equal(nalu[:4], []byte{0x00, 0x00, 0x00, 0x01}) { - return nalu[4:] - } - if len(nalu) >= 3 && bytes.Equal(nalu[:3], []byte{0x00, 0x00, 0x01}) { - return nalu[3:] - } - return nalu -} - -// createAudioTrack создаёт аудио-трек. -func createAudioTrack(sampleRate, channels int) *mp4.TrakBox { - trak := mp4.NewTrakBox() - timescale := uint32(sampleRate) - - mdhd := &mp4.MdhdBox{ - Timescale: timescale, - Language: convertLanguage("und"), - } - - hdlr := &mp4.HdlrBox{ - HandlerType: "soun", - Name: "SoundHandler", - } - - mp4a := mp4.NewAudioSampleEntryBox("mp4a") - mp4a.ChannelCount = uint16(channels) - mp4a.SampleRate = uint16(sampleRate) - mp4a.SampleSize = 16 - - asc := getAACConfig(sampleRate, channels) - esds := createESDSBox(asc) - mp4a.AddChild(esds) - - stbl := mp4.NewStblBox() - stsd := mp4.NewStsdBox() - stsd.AddChild(mp4a) - stbl.AddChild(stsd) - stbl.AddChild(&mp4.SttsBox{}) - stbl.AddChild(&mp4.StscBox{}) - stbl.AddChild(&mp4.StszBox{}) - stbl.AddChild(&mp4.StcoBox{}) - - trak.Mdia = &mp4.MdiaBox{ - Mdhd: mdhd, - Hdlr: hdlr, - Minf: &mp4.MinfBox{ - Stbl: stbl, - }, - } - - return trak -} - -// createESDSBox создаёт ESDS-бокс для AAC. -func createESDSBox(asc []byte) *mp4.EsdsBox { - return &mp4.EsdsBox{ - ESDescriptor: mp4.ESDescriptor{ - DecConfigDescriptor: &mp4.DecoderConfigDescriptor{ - ObjectType: 0x40, - StreamType: 0x05, - DecSpecificInfo: &mp4.DecSpecificInfoDescriptor{ - DecConfig: asc, - }, - }, - }, + panic(err) } } - -func getAACConfig(sampleRate, channels int) []byte { - // Пример конфигурации для 8000 Гц, моно - return []byte{0x12, 0x10} -} - -// addSamplesToVideoTrack заполняет таблицы сэмплов видео-трека. -func addSamplesToVideoTrack(trak *mp4.TrakBox, samples []Sample) { - stbl := trak.Mdia.Minf.Stbl - - stts := mp4.SttsBox{} - for _, s := range samples { - stts.SampleCount = []uint32{1} - stts.SampleTimeDelta = []uint32{s.Dur} - } - - stsz := mp4.StszBox{} - for _, s := range samples { - stsz.SampleSize = []uint32{s.Size} - } - - stss := mp4.StssBox{} - for i, s := range samples { - if s.IsSync { - stss.SampleNumber = []uint32{uint32(i + 1)} - } - } - - stbl.AddChild(&stts) - stbl.AddChild(&stsz) - stbl.AddChild(&stss) -} - -// addSamplesToAudioTrack заполняет таблицы сэмплов аудио-трека. -func addSamplesToAudioTrack(trak *mp4.TrakBox, samples []Sample, sampleRate int) { - stbl := trak.Mdia.Minf.Stbl - - stts := mp4.SttsBox{} - for _, s := range samples { - stts.SampleCount = []uint32{1} - stts.SampleTimeDelta = []uint32{s.Dur} - } - - stsz := mp4.StszBox{} - for _, s := range samples { - stsz.SampleSize = []uint32{uint32(len(s.Data))} - } - - stbl.AddChild(&stts) - stbl.AddChild(&stsz) -} - -// parseH264Samples парсит H.264 поток на сэмплы. -func parseH264Samples(data []byte) []Sample { - var samples []Sample - start := 0 - for i := 0; i < len(data)-3; i++ { - if bytes.Equal(data[i:i+4], []byte{0x00, 0x00, 0x00, 0x01}) { - if start != 0 { - sampleData := data[start:i] - samples = append(samples, Sample{ - Size: uint32(len(sampleData)), - Dur: 3600, - IsSync: isKeyFrame(sampleData), - }) - } - start = i + 4 - } - } - if start < len(data) { - sampleData := data[start:] - samples = append(samples, Sample{ - Size: uint32(len(sampleData)), - Dur: 3600, - IsSync: isKeyFrame(sampleData), - }) - } - return samples -} - -// isKeyFrame определяет, является ли NALU ключевым (IDR). -func isKeyFrame(nalu []byte) bool { - if len(nalu) == 0 { - return false - } - nalType := nalu[0] & 0x1F - return nalType == 5 -} - -// parseAACSamples парсит AAC данные и возвращает сэмплы. -func parseAACSamples(data []byte) []Sample { - var samples []Sample - frameSize := 1024 // примерный размер AAC-фрейма - for i := 0; i < len(data); i += frameSize { - end := i + frameSize - if end > len(data) { - end = len(data) - } - samples = append(samples, Sample{ - Data: data[i:end], - Dur: 1024, - Size: uint32(end - i), - }) - } - return samples -} - -// convertLanguage преобразует строку языка в 16-битное значение. -func convertLanguage(lang string) uint16 { - if len(lang) != 3 { - return 0x55C4 - } - b1 := lang[0] - 0x60 - b2 := lang[1] - 0x60 - b3 := lang[2] - 0x60 - return uint16((b1 << 10) | (b2 << 5) | b3) -} diff --git a/writer/go.mod b/writer/go.mod index 4fb34f9..ef2ef7e 100644 --- a/writer/go.mod +++ b/writer/go.mod @@ -3,19 +3,24 @@ module writer go 1.23.6 require ( + git.insit.tech/sas/rtsp_proxy v0.0.0-20250307041058-f3c4351edaa7 github.com/Eyevinn/mp4ff v0.47.0 github.com/bluenviron/gortsplib/v4 v4.12.3 + github.com/bluenviron/mediacommon/v2 v2.0.0 github.com/gen2brain/aac-go v0.0.0-20230119102159-ef1e76509d21 - github.com/pion/rtp v1.8.11 + github.com/pion/rtp v1.8.12 github.com/zaf/g711 v1.4.0 ) require ( + github.com/asticode/go-astikit v0.52.0 // indirect + github.com/asticode/go-astits v1.13.0 // indirect github.com/bluenviron/mediacommon v1.14.0 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/grafov/m3u8 v0.12.1 // indirect github.com/pion/randutil v0.1.0 // indirect github.com/pion/rtcp v1.2.15 // indirect github.com/pion/sdp/v3 v3.0.10 // indirect - golang.org/x/net v0.34.0 // indirect - golang.org/x/sys v0.29.0 // indirect + golang.org/x/net v0.37.0 // indirect + golang.org/x/sys v0.31.0 // indirect ) diff --git a/writer/go.sum b/writer/go.sum index a84b75b..9fefb48 100644 --- a/writer/go.sum +++ b/writer/go.sum @@ -1,26 +1,42 @@ +git.insit.tech/sas/rtsp_proxy v0.0.0-20250307041058-f3c4351edaa7 h1:XW0pmUdrZjRR785QQPjeoRDzTdqdxn4JVdVXSa5dJAI= +git.insit.tech/sas/rtsp_proxy v0.0.0-20250307041058-f3c4351edaa7/go.mod h1:ARyDR0b8DXexdcoWH1N8Qs6EFGa8aF86qCqsrc3LX6w= github.com/Eyevinn/mp4ff v0.47.0 h1:XSSHYt5+I0fyOnHWoNwM72DtivlmHFR0V9azgIi+ZVU= github.com/Eyevinn/mp4ff v0.47.0/go.mod h1:hJNUUqOBryLAzUW9wpCJyw2HaI+TCd2rUPhafoS5lgg= +github.com/asticode/go-astikit v0.30.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0= +github.com/asticode/go-astikit v0.52.0 h1:kTl2XjgiVQhUl1H7kim7NhmTtCMwVBbPrXKqhQhbk8Y= +github.com/asticode/go-astikit v0.52.0/go.mod h1:fV43j20UZYfXzP9oBn33udkvCvDvCDhzjVqoLFuuYZE= +github.com/asticode/go-astits v1.13.0 h1:XOgkaadfZODnyZRR5Y0/DWkA9vrkLLPLeeOvDwfKZ1c= +github.com/asticode/go-astits v1.13.0/go.mod h1:QSHmknZ51pf6KJdHKZHJTLlMegIrhega3LPWz3ND/iI= github.com/bluenviron/gortsplib/v4 v4.12.3 h1:3EzbyGb5+MIOJQYiWytRegFEP4EW5paiyTrscQj63WE= github.com/bluenviron/gortsplib/v4 v4.12.3/go.mod h1:SkZPdaMNr+IvHt2PKRjUXxZN6FDutmSZn4eT0GmF0sk= github.com/bluenviron/mediacommon v1.14.0 h1:lWCwOBKNKgqmspRpwpvvg3CidYm+XOc2+z/Jw7LM5dQ= github.com/bluenviron/mediacommon v1.14.0/go.mod h1:z5LP9Tm1ZNfQV5Co54PyOzaIhGMusDfRKmh42nQSnyo= +github.com/bluenviron/mediacommon/v2 v2.0.0 h1:JinZ9v2x6QeAOzA0cDA6aFe8vQuCrU8OyWEhG2iNzwY= +github.com/bluenviron/mediacommon/v2 v2.0.0/go.mod h1:iHEz1SFIet6zBwAQoh1a92vTQ3dV3LpVFbom6/SLz3k= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gen2brain/aac-go v0.0.0-20230119102159-ef1e76509d21 h1:yfrARW/aVlqKORCdKrYdU0PZUKPqQvYEUQBKfVlNa9Q= github.com/gen2brain/aac-go v0.0.0-20230119102159-ef1e76509d21/go.mod h1:HZqGD/LXHB1VCGUGNzuyxSsD12f3KjbJbvImAmoK/mM= +github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg= github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grafov/m3u8 v0.12.1 h1:DuP1uA1kvRRmGNAZ0m+ObLv1dvrfNO0TPx0c/enNk0s= +github.com/grafov/m3u8 v0.12.1/go.mod h1:nqzOkfBiZJENr52zTVd/Dcl03yzphIMbJqkXGu+u080= github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo= github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0= -github.com/pion/rtp v1.8.11 h1:17xjnY5WO5hgO6SD3/NTIUPvSFw/PbLsIJyz1r1yNIk= -github.com/pion/rtp v1.8.11/go.mod h1:8uMBJj32Pa1wwx8Fuv/AsFhn8jsgw+3rUC2PfoBZ8p4= +github.com/pion/rtp v1.8.12 h1:nsKs8Wi0jQyBFHU3qmn/OvtZrhktVfJY0vRxwACsL5U= +github.com/pion/rtp v1.8.12/go.mod h1:8uMBJj32Pa1wwx8Fuv/AsFhn8jsgw+3rUC2PfoBZ8p4= github.com/pion/sdp/v3 v3.0.10 h1:6MChLE/1xYB+CjumMw+gZ9ufp2DPApuVSnDT8t5MIgA= github.com/pion/sdp/v3 v3.0.10/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E= +github.com/pkg/profile v1.4.0/go.mod h1:NWz/XGvpEW1FyYQ7fCx4dqYBLlfTcE+A9FLAkNKqjFE= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/youpy/go-riff v0.1.0 h1:vZO/37nI4tIET8tQI0Qn0Y79qQh99aEpponTPiPut7k= @@ -29,9 +45,11 @@ github.com/youpy/go-wav v0.3.2 h1:NLM8L/7yZ0Bntadw/0h95OyUsen+DQIVf9gay+SUsMU= github.com/youpy/go-wav v0.3.2/go.mod h1:0FCieAXAeSdcxFfwLpRuEo0PFmAoc+8NU34h7TUvk50= github.com/zaf/g711 v1.4.0 h1:XZYkjjiAg9QTBnHqEg37m2I9q3IIDv5JRYXs2N8ma7c= github.com/zaf/g711 v1.4.0/go.mod h1:eCDXt3dSp/kYYAoooba7ukD/Q75jvAaS4WOMr0l1Roo= -golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= -golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= -golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= -golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= +golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/writer/internal/config/config.go b/writer/internal/config/config.go new file mode 100644 index 0000000..e4ea6a1 --- /dev/null +++ b/writer/internal/config/config.go @@ -0,0 +1,31 @@ +package config + +import ( + "git.insit.tech/sas/rtsp_proxy/protos" + "log" + "time" +) + +// CreateFileName creates FileName structure. +func CreateFileName(resolutions []string, period int) *protos.FileName { + fn := protos.FileName{ + Path: "../../data/" + resolutions[0] + "/", + TimeNow: time.Now().Format("15-04-05_02-01-2006"), + Name: "videoFragment", + Number: -1, + Duration: float64(period * 60), + } + + return &fn +} + +// Waiter waits for the next period. +func Waiter(period int) { + periodTD := time.Duration(period) * time.Minute + + now := time.Now() + nextSegment := now.Truncate(periodTD).Add(periodTD) + waitDuration := nextSegment.Sub(now) + log.Printf("waiting for start recording: %v\n", waitDuration) + time.Sleep(waitDuration) +} diff --git a/writer/internal/media/g711.go b/writer/internal/media/g711.go new file mode 100644 index 0000000..0927e5c --- /dev/null +++ b/writer/internal/media/g711.go @@ -0,0 +1,42 @@ +package media + +import ( + "errors" + "fmt" + "github.com/bluenviron/gortsplib/v4" + "github.com/bluenviron/gortsplib/v4/pkg/description" + "github.com/bluenviron/gortsplib/v4/pkg/format" + "github.com/bluenviron/gortsplib/v4/pkg/format/rtplpcm" + "github.com/pion/rtp" + "log" +) + +// CheckG711Format finds the H264 media and format. +func CheckG711Format(desc *description.Session) (*format.G711, *description.Media, error) { + var g711Format *format.G711 + g711Media := desc.FindFormat(&g711Format) + if g711Media == nil { + return nil, nil, errors.New("media G711 not found") + } + + return g711Format, g711Media, nil +} + +// ProcG711 processes G711 flow and returns PTS and AU. +func ProcG711(c *gortsplib.Client, g711Media *description.Media, g711RTPDec *rtplpcm.Decoder, pkt *rtp.Packet) ( + int64, []byte, error) { + // Decode timestamp. + pts, ok := c.PacketPTS2(g711Media, pkt) + if !ok { + log.Printf("waiting for timestamp\n") + return 0, nil, nil + } + + // Extract access unit from RTP packets. + au, err := g711RTPDec.Decode(pkt) + if err != nil { + return 0, nil, fmt.Errorf("decoding G711 RTP packet: %w", err) + } + + return pts, au, nil +} diff --git a/writer/internal/media/h264.go b/writer/internal/media/h264.go new file mode 100644 index 0000000..bf9a6e7 --- /dev/null +++ b/writer/internal/media/h264.go @@ -0,0 +1,43 @@ +package media + +import ( + "errors" + "fmt" + "github.com/bluenviron/gortsplib/v4" + "github.com/bluenviron/gortsplib/v4/pkg/description" + "github.com/bluenviron/gortsplib/v4/pkg/format" + "github.com/bluenviron/gortsplib/v4/pkg/format/rtph264" + "github.com/pion/rtp" + "log" +) + +// CheckH264Format finds the H264 media and format. +func CheckH264Format(desc *description.Session) (*format.H264, *description.Media, error) { + var h264Format *format.H264 + h264Media := desc.FindFormat(&h264Format) + if h264Media == nil { + return nil, nil, errors.New("media H264 not found") + } + return h264Format, h264Media, nil +} + +// ProcH264 processes H264 flow and returns PTS and AU. +func ProcH264(c *gortsplib.Client, h264Media *description.Media, h264RTPDec *rtph264.Decoder, pkt *rtp.Packet) ( + int64, [][]byte, error) { + // Decode timestamp. + pts, ok := c.PacketPTS2(h264Media, pkt) + if !ok { + log.Printf("waiting for timestamp\n") + return 0, nil, nil + } + + // Extract access unit from RTP packets. + au, err := h264RTPDec.Decode(pkt) + if err != nil { + if err != rtph264.ErrNonStartingPacketAndNoPrevious && err != rtph264.ErrMorePacketsNeeded { + return 0, nil, fmt.Errorf("decoding H264 RTP packet: %w", err) + } + } + + return pts, au, nil +} diff --git a/writer/internal/procRTSP/client.go b/writer/internal/procRTSP/client.go new file mode 100644 index 0000000..9cd8191 --- /dev/null +++ b/writer/internal/procRTSP/client.go @@ -0,0 +1,180 @@ +package procRTSP + +import ( + "fmt" + "git.insit.tech/sas/rtsp_proxy/gens" + "github.com/bluenviron/gortsplib/v4" + "github.com/bluenviron/gortsplib/v4/pkg/base" + "github.com/bluenviron/gortsplib/v4/pkg/description" + "github.com/bluenviron/gortsplib/v4/pkg/format" + "github.com/pion/rtp" + _ "github.com/zaf/g711" + "log" + "time" + "writer/internal/config" + "writer/internal/media" + "writer/pkg/converter" +) + +var ( + resolutions = []string{"1280x720"} +) + +// ProcRTSP process RTSP protocol and writes H264 and PCM flows into TS container. +func ProcRTSP(period int, URI string) error { + // Create FileName structure + fn := config.CreateFileName(resolutions, period) + + //////////////////////////////////////////////////////////////////////////////////////// + + // Create M3U8 playlist. + go gens.MediaPlaylistGenerator("/home/psa/GoRepository/data", "", fn.Duration, resolutions) + + //////////////////////////////////////////////////////////////////////////////////////// + + // Initialise client. + c := gortsplib.Client{} + + // Parse URL. + u, err := base.ParseURL(URI) + if err != nil { + return fmt.Errorf("parse URL error: %w", err) + } + + // Connect to the server. + err = c.Start(u.Scheme, u.Host) + if err != nil { + return fmt.Errorf("connect to the server error: %w", err) + } + defer c.Close() + + // Find available medias. + desc, _, err := c.Describe(u) + if err != nil || desc == nil { + return fmt.Errorf("medias not found: %w", err) + } + + //////////////////////////////////////////////////////////////////////////////////////// + + // Find the H264 media and format. + h264Format, h264Media, err := media.CheckH264Format(desc) + + // Find the G711 media and format. + g711Format, g711Media, err := media.CheckG711Format(desc) + + // Initialising variable for AAC. + var mpeg4AudioFormat *format.MPEG4Audio + + //////////////////////////////////////////////////////////////////////////////////////// + + // Create RTP -> H264 decoder. + h264RTPDec, err := h264Format.CreateDecoder() + if err != nil { + return fmt.Errorf("create H264 decoder error: %w", err) + } + + // Create RTP -> H264 decoder. + g711RTPDec, err := g711Format.CreateDecoder() + if err != nil { + return fmt.Errorf("create G711 decoder error: %w", err) + } + + //////////////////////////////////////////////////////////////////////////////////////// + + // Wait for the next period. + config.Waiter(period) + log.Println("Start recording") + + //////////////////////////////////////////////////////////////////////////////////////// + + // Setup MPEG-TS muxer. + currentMpegtsMuxer := &converter.MpegtsMuxer{ + FileName: fn.SetNumNTime(), + H264Format: h264Format, + Mpeg4AudioFormat: mpeg4AudioFormat, + } + err = currentMpegtsMuxer.Initialize(resolutions) + if err != nil { + panic(err) + } + defer currentMpegtsMuxer.Close() + + //////////////////////////////////////////////////////////////////////////////////////// + + // Setup all medias. + err = c.SetupAll(desc.BaseURL, desc.Medias) + if err != nil { + panic(err) + } + + //////////////////////////////////////////////////////////////////////////////////////// + + // Called when a H264/RTP or G711/RTP packet arrives. + c.OnPacketRTPAny(func(medi *description.Media, forma format.Format, pkt *rtp.Packet) { + switch f := forma.(type) { + case *format.H264: + // Process H264 flow and return PTS and AU. + pts, au, err := media.ProcH264(&c, h264Media, h264RTPDec, pkt) + if err != nil { + fmt.Printf("process G711 error: %s\n", err) + } + + // Encode the access unit into MPEG-TS. + err = currentMpegtsMuxer.WriteH264(au, pts) + if err != nil { + log.Printf("writing H264 packet: %s\n", err) + } + + case *format.G711: + // Process G711 flow and returns PTS and AU. + _, au, err := media.ProcG711(&c, g711Media, g711RTPDec, pkt) + if err != nil { + fmt.Printf("process G711 error: %s\n", err) + } + + // Convert G711 to AAC. + _, err = converter.ConvertG711ToAAC(au, f.MULaw) // take aacAu + if err != nil { + log.Printf("converting G711 to AAC frame: %s\n", err) + } + + /* + // Encode the access unit into MPEG-TS. + err = MpegtsMuxer.writeMPEG4Audio([][]byte{aacAu}, pts) + if err != nil { + log.Printf("MPEG-TS write error: %v", err) + return + } + */ + } + }) + + //////////////////////////////////////////////////////////////////////////////////////// + + // Send PLAY request. + _, err = c.Play(nil) + if err != nil { + log.Fatalln("Ошибка запуска воспроизведения:", err) + } + + // Create ticker for rotation files. + ticker := time.NewTicker(time.Duration(period) * time.Minute) + defer ticker.Stop() + + // Rotate files. + go func() { + for range ticker.C { + currentMpegtsMuxer.Close() + currentMpegtsMuxer.FileName = fn.SetNumNTime() + + err = currentMpegtsMuxer.Initialize(resolutions) + if err != nil { + panic(err) + } + + log.Println("New file for recording created:", currentMpegtsMuxer.FileName) + } + }() + + panic(c.Wait()) +} diff --git a/writer/pkg/converter/mpegts_muxer.go b/writer/pkg/converter/mpegts_muxer.go new file mode 100644 index 0000000..e161cd9 --- /dev/null +++ b/writer/pkg/converter/mpegts_muxer.go @@ -0,0 +1,143 @@ +package converter + +import ( + "bufio" + "os" + "sync" + + "github.com/bluenviron/gortsplib/v4/pkg/format" + "github.com/bluenviron/mediacommon/v2/pkg/codecs/h264" + "github.com/bluenviron/mediacommon/v2/pkg/formats/mpegts" +) + +func multiplyAndDivide(v, m, d int64) int64 { + secs := v / d + dec := v % d + return (secs*m + dec*m/d) +} + +// MpegtsMuxer allows to save a H264 / MPEG-4 audio stream into a MPEG-TS file. +type MpegtsMuxer struct { + FileName string + H264Format *format.H264 + Mpeg4AudioFormat *format.MPEG4Audio + + f *os.File + b *bufio.Writer + w *mpegts.Writer + h264Track *mpegts.Track + mpeg4AudioTrack *mpegts.Track + dtsExtractor *h264.DTSExtractor + mutex sync.Mutex +} + +// Initialize initializes a MpegtsMuxer. +func (e *MpegtsMuxer) Initialize(resolutions []string) error { + var err error + if err = os.MkdirAll("../../data/"+resolutions[0]+"/", 0755); err != nil { + return err + } + + e.f, err = os.Create("../../data/" + resolutions[0] + "/" + e.FileName) + if err != nil { + return err + } + e.b = bufio.NewWriter(e.f) + + e.h264Track = &mpegts.Track{ + Codec: &mpegts.CodecH264{}, + } + + e.mpeg4AudioTrack = &mpegts.Track{ + Codec: &mpegts.CodecMPEG4Audio{ + // Config: *e.mpeg4AudioFormat.Config, + }, + } + + e.w = mpegts.NewWriter(e.b, []*mpegts.Track{e.h264Track, e.mpeg4AudioTrack}) + + return nil +} + +// Close closes all the MpegtsMuxer resources. +func (e *MpegtsMuxer) Close() { + err := e.b.Flush() + if err != nil { + panic(err) + } + err = e.f.Close() + if err != nil { + panic(err) + } +} + +// WriteH264 writes a H264 access unit into MPEG-TS. +func (e *MpegtsMuxer) WriteH264(au [][]byte, pts int64) error { + e.mutex.Lock() + defer e.mutex.Unlock() + + var filteredAU [][]byte + + nonIDRPresent := false + idrPresent := false + + for _, nalu := range au { + typ := h264.NALUType(nalu[0] & 0x1F) + switch typ { + case h264.NALUTypeSPS: + e.H264Format.SPS = nalu + continue + + case h264.NALUTypePPS: + e.H264Format.PPS = nalu + continue + + case h264.NALUTypeAccessUnitDelimiter: + continue + + case h264.NALUTypeIDR: + idrPresent = true + + case h264.NALUTypeNonIDR: + nonIDRPresent = true + } + + filteredAU = append(filteredAU, nalu) + } + + au = filteredAU + + if au == nil || (!nonIDRPresent && !idrPresent) { + return nil + } + + // Add SPS and PPS before access unit that contains an IDR. + if idrPresent { + au = append([][]byte{e.H264Format.SPS, e.H264Format.PPS}, au...) + } + + if e.dtsExtractor == nil { + // Skip samples silently until we find one with an IDR. + if !idrPresent { + return nil + } + e.dtsExtractor = h264.NewDTSExtractor() + } + + dts, err := e.dtsExtractor.Extract(au, pts) + if err != nil { + return err + } + + // Encode into MPEG-TS. + return e.w.WriteH264(e.h264Track, pts, dts, au) +} + +// writeMPEG4Audio writes MPEG-4 audio access units into MPEG-TS. +func (e *MpegtsMuxer) writeMPEG4Audio(aus [][]byte, pts int64) error { + e.mutex.Lock() + defer e.mutex.Unlock() + + return e.w.WriteMPEG4Audio( + e.mpeg4AudioTrack, multiplyAndDivide(pts, 90000, int64(e.Mpeg4AudioFormat.ClockRate())), aus) +} diff --git a/writer/pkg/converter/pcm_to_aac.go b/writer/pkg/converter/pcm_to_aac.go new file mode 100644 index 0000000..15b195e --- /dev/null +++ b/writer/pkg/converter/pcm_to_aac.go @@ -0,0 +1,37 @@ +package converter + +import ( + "bytes" + "fmt" + "github.com/bluenviron/mediacommon/v2/pkg/codecs/g711" + "github.com/gen2brain/aac-go" +) + +// ConvertG711ToAAC converts PCM to AAC. +func ConvertG711ToAAC(g711Samples []byte, mulaw bool) ([]byte, error) { + var pcmSamples []byte + if mulaw { + pcmSamples = g711.DecodeMulaw(g711Samples) + } else { + pcmSamples = g711.DecodeAlaw(g711Samples) + } + + var buf bytes.Buffer + opts := &aac.Options{ + SampleRate: 8000, // Исходная частота G711 + NumChannels: 1, + } + + enc, err := aac.NewEncoder(&buf, opts) + if err != nil { + return nil, fmt.Errorf("error creating encoder: %v", err) + } + defer enc.Close() + + r := bytes.NewReader(pcmSamples) + if err := enc.Encode(r); err != nil { + return nil, fmt.Errorf("error encoding: %v", err) + } + + return buf.Bytes(), nil +} diff --git a/writer/pkg/packer/mp4.go b/writer/pkg/packer/mp4.go new file mode 100644 index 0000000..7c09814 --- /dev/null +++ b/writer/pkg/packer/mp4.go @@ -0,0 +1,364 @@ +package packer + +import ( + "bytes" + "fmt" + "github.com/Eyevinn/mp4ff/mp4" + "github.com/bluenviron/gortsplib/v4/pkg/format" + "log" + "os" + "os/exec" + "strings" +) + +/* + // Горутина для объединения видео и аудио в контейнер MP4 + go func(oldH264Filename, oldPCMFilename, oldMP4Filename string) { + // Создание MP4 файл + //if err := createMP4File(oldH264Filename, aacFilename, oldMP4Filename, h264Params); err != nil { + // log.Printf("Ошибка создания MP4: %v", err) + //} else { + // log.Printf("Файлы %s, %s успешно объединены в контейнер MP4: %s\n", + // oldH264Filename, aacFilename, oldMP4Filename) + //} + + // Часть программы, вызывающая ffmpeg для объединения видеофайла .h264 аудиофайла .pcm в контейнер .mp4 - + // остается в программе до реализации аналогичного функционала без использования ffmpeg. + // Передаем значения переменных в замыкание, чтобы избежать их изменения + muxedFilename := strings.Replace(oldH264Filename, ".h264", ".mp4", 1) + cmd := exec.Command("ffmpeg", + "-y", + "-fflags", "+genpts", + "-r", "25", + "-i", oldH264Filename, + "-i", aacFilename, + "-c:v", "copy", + muxedFilename, + ) + + if err := cmd.Run(); err != nil { + log.Printf("Ошибка при объединении фрагментов: %v", err) + } else { + log.Printf("Фрагменты объединены в файл %s", muxedFilename) + } + + // Удаление временных файлов + //os.Remove(oldH264Filename) + //os.Remove(aacFilename) + //os.Remove(oldPCMFilename) + + log.Printf("Файлы %s, %s, %s успешно удалены\n", oldH264Filename, aacFilename, oldPCMFilename) + }(oldH264Filename, oldPCMFilename, oldMP4Filename) +*/ + +// createMP4FileFfmpeg takes H264 and AAC files to make MP4 file using ffmpeg. +func createMP4FileFfmpeg(H264Filename, aacFilename string, newTimestamp string) error { + muxedFilename := strings.Replace(H264Filename, ".h264", ".mp4", 1) + + cmd := exec.Command("ffmpeg", + "-y", + "-fflags", "+genpts", + "-r", "25", + "-i", H264Filename, + "-i", aacFilename, + "-c:v", "copy", + muxedFilename, + ) + + if err := cmd.Run(); err != nil { + return fmt.Errorf("combining fragments error: %v", err) + } + + return nil +} + +// Sample – единый тип для описания сэмпла (для видео и аудио) +type Sample struct { + Data []byte + Offset uint64 + Size uint32 + Dur uint32 + IsSync bool +} + +// createMP4File packs H264 and AAC to MP4. +func createMP4File(videoFile, audioFile, outputFile string, formaH264 *format.H264) error { + videoData, err := os.ReadFile(videoFile) + if err != nil { + return err + } + videoSamples := parseH264Samples(videoData) + + audioData, err := os.ReadFile(audioFile) + if err != nil { + return err + } + audioSamples := parseAACSamples(audioData) + + f := mp4.NewFile() + + videoTrack := createVideoTrack(formaH264.SPS, formaH264.PPS) + audioTrack := createAudioTrack(8000, 1) + + addSamplesToVideoTrack(videoTrack, videoSamples) + addSamplesToAudioTrack(audioTrack, audioSamples, 8000) + + moov := mp4.NewMoovBox() + moov.AddChild(videoTrack) + moov.AddChild(audioTrack) + f.AddChild(moov, 0) + + mdat := mp4.MdatBox{} + mdat.Data = append(videoData, audioData...) + f.AddChild(&mdat, 0) + + outFile, err := os.Create(outputFile) + if err != nil { + return err + } + defer outFile.Close() + return f.Encode(outFile) +} + +// createVideoTrack создаёт видео-трек на основе SPS и PPS. +func createVideoTrack(sps, pps []byte) *mp4.TrakBox { + sps = stripAnnexB(sps) + pps = stripAnnexB(pps) + + if len(sps) < 4 { + log.Fatalln("SPS слишком короткий или пустой") + } + if len(pps) < 1 { + log.Fatalln("PPS слишком короткий или пустой") + } + + trak := mp4.NewTrakBox() + timescale := uint32(90000) + + mdhd := &mp4.MdhdBox{ + Timescale: timescale, + Language: convertLanguage("und"), + } + + hdlr := &mp4.HdlrBox{ + HandlerType: "vide", + Name: "VideoHandler", + } + + avc1 := mp4.NewVisualSampleEntryBox("avc1") + avc1.Width = 1280 + avc1.Height = 720 + + avcC, err := mp4.CreateAvcC([][]byte{sps}, [][]byte{pps}, true) + if err != nil { + log.Fatalf("ошибка создания avcC: %v", err) + } + avcC.AVCLevelIndication = 0x1f + avc1.AddChild(avcC) + + stbl := mp4.NewStblBox() + stsd := mp4.NewStsdBox() + stsd.AddChild(avc1) + stbl.AddChild(stsd) + stbl.AddChild(&mp4.SttsBox{}) + stbl.AddChild(&mp4.StscBox{}) + stbl.AddChild(&mp4.StszBox{}) + stbl.AddChild(&mp4.StcoBox{}) + + trak.Mdia = &mp4.MdiaBox{ + Mdhd: mdhd, + Hdlr: hdlr, + Minf: &mp4.MinfBox{ + Stbl: stbl, + }, + } + + return trak +} + +// stripAnnexB удаляет стартовый код Annex-B из NAL-единицы. +func stripAnnexB(nalu []byte) []byte { + if len(nalu) >= 4 && bytes.Equal(nalu[:4], []byte{0x00, 0x00, 0x00, 0x01}) { + return nalu[4:] + } + if len(nalu) >= 3 && bytes.Equal(nalu[:3], []byte{0x00, 0x00, 0x01}) { + return nalu[3:] + } + return nalu +} + +// createAudioTrack создаёт аудио-трек. +func createAudioTrack(sampleRate, channels int) *mp4.TrakBox { + trak := mp4.NewTrakBox() + timescale := uint32(sampleRate) + + mdhd := &mp4.MdhdBox{ + Timescale: timescale, + Language: convertLanguage("und"), + } + + hdlr := &mp4.HdlrBox{ + HandlerType: "soun", + Name: "SoundHandler", + } + + mp4a := mp4.NewAudioSampleEntryBox("mp4a") + mp4a.ChannelCount = uint16(channels) + mp4a.SampleRate = uint16(sampleRate) + mp4a.SampleSize = 16 + + asc := getAACConfig(sampleRate, channels) + esds := createESDSBox(asc) + mp4a.AddChild(esds) + + stbl := mp4.NewStblBox() + stsd := mp4.NewStsdBox() + stsd.AddChild(mp4a) + stbl.AddChild(stsd) + stbl.AddChild(&mp4.SttsBox{}) + stbl.AddChild(&mp4.StscBox{}) + stbl.AddChild(&mp4.StszBox{}) + stbl.AddChild(&mp4.StcoBox{}) + + trak.Mdia = &mp4.MdiaBox{ + Mdhd: mdhd, + Hdlr: hdlr, + Minf: &mp4.MinfBox{ + Stbl: stbl, + }, + } + + return trak +} + +// createESDSBox создаёт ESDS-бокс для AAC. +func createESDSBox(asc []byte) *mp4.EsdsBox { + return &mp4.EsdsBox{ + ESDescriptor: mp4.ESDescriptor{ + DecConfigDescriptor: &mp4.DecoderConfigDescriptor{ + ObjectType: 0x40, + StreamType: 0x05, + DecSpecificInfo: &mp4.DecSpecificInfoDescriptor{ + DecConfig: asc, + }, + }, + }, + } +} + +func getAACConfig(sampleRate, channels int) []byte { + // Пример конфигурации для 8000 Гц, моно + return []byte{0x12, 0x10} +} + +// addSamplesToVideoTrack заполняет таблицы сэмплов видео-трека. +func addSamplesToVideoTrack(trak *mp4.TrakBox, samples []Sample) { + stbl := trak.Mdia.Minf.Stbl + + stts := mp4.SttsBox{} + for _, s := range samples { + stts.SampleCount = []uint32{1} + stts.SampleTimeDelta = []uint32{s.Dur} + } + + stsz := mp4.StszBox{} + for _, s := range samples { + stsz.SampleSize = []uint32{s.Size} + } + + stss := mp4.StssBox{} + for i, s := range samples { + if s.IsSync { + stss.SampleNumber = []uint32{uint32(i + 1)} + } + } + + stbl.AddChild(&stts) + stbl.AddChild(&stsz) + stbl.AddChild(&stss) +} + +// addSamplesToAudioTrack заполняет таблицы сэмплов аудио-трека. +func addSamplesToAudioTrack(trak *mp4.TrakBox, samples []Sample, sampleRate int) { + stbl := trak.Mdia.Minf.Stbl + + stts := mp4.SttsBox{} + for _, s := range samples { + stts.SampleCount = []uint32{1} + stts.SampleTimeDelta = []uint32{s.Dur} + } + + stsz := mp4.StszBox{} + for _, s := range samples { + stsz.SampleSize = []uint32{uint32(len(s.Data))} + } + + stbl.AddChild(&stts) + stbl.AddChild(&stsz) +} + +// parseH264Samples парсит H.264 поток на сэмплы. +func parseH264Samples(data []byte) []Sample { + var samples []Sample + start := 0 + for i := 0; i < len(data)-3; i++ { + if bytes.Equal(data[i:i+4], []byte{0x00, 0x00, 0x00, 0x01}) { + if start != 0 { + sampleData := data[start:i] + samples = append(samples, Sample{ + Size: uint32(len(sampleData)), + Dur: 3600, + IsSync: isKeyFrame(sampleData), + }) + } + start = i + 4 + } + } + if start < len(data) { + sampleData := data[start:] + samples = append(samples, Sample{ + Size: uint32(len(sampleData)), + Dur: 3600, + IsSync: isKeyFrame(sampleData), + }) + } + return samples +} + +// isKeyFrame определяет, является ли NALU ключевым (IDR). +func isKeyFrame(nalu []byte) bool { + if len(nalu) == 0 { + return false + } + nalType := nalu[0] & 0x1F + return nalType == 5 +} + +// parseAACSamples парсит AAC данные и возвращает сэмплы. +func parseAACSamples(data []byte) []Sample { + var samples []Sample + frameSize := 1024 // примерный размер AAC-фрейма + for i := 0; i < len(data); i += frameSize { + end := i + frameSize + if end > len(data) { + end = len(data) + } + samples = append(samples, Sample{ + Data: data[i:end], + Dur: 1024, + Size: uint32(end - i), + }) + } + return samples +} + +// convertLanguage преобразует строку языка в 16-битное значение. +func convertLanguage(lang string) uint16 { + if len(lang) != 3 { + return 0x55C4 + } + b1 := lang[0] - 0x60 + b2 := lang[1] - 0x60 + b3 := lang[2] - 0x60 + return uint16((b1 << 10) | (b2 << 5) | b3) +}