365 lines
9.1 KiB
Go
365 lines
9.1 KiB
Go
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)
|
||
}
|