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