create TS files instead of mp4; using another libraries for processing RTP packets; changed project architecture; added dependence to teh private library.
This commit is contained in:
parent
e2ab953f8a
commit
dc09904c12
@ -1,641 +1,10 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import "writer/internal/procRTSP"
|
||||||
"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) в текущий сегмент
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// Парсинг URL RTSP
|
err := procRTSP.ProcRTSP(1, "rtsp://intercom-video-2.insit.ru/dp-ohusuxzcvzsnpzzvkpyhddnwxuyeyc")
|
||||||
u, err := base.ParseURL("rtsp://intercom-video-1.insit.ru/dp-gfahybswjoendkcpbaqvgkeizruudbhsfdr")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln("Ошибка парсинга URL:", err)
|
panic(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,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
@ -3,19 +3,24 @@ module writer
|
|||||||
go 1.23.6
|
go 1.23.6
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
git.insit.tech/sas/rtsp_proxy v0.0.0-20250307041058-f3c4351edaa7
|
||||||
github.com/Eyevinn/mp4ff v0.47.0
|
github.com/Eyevinn/mp4ff v0.47.0
|
||||||
github.com/bluenviron/gortsplib/v4 v4.12.3
|
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/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
|
github.com/zaf/g711 v1.4.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
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/bluenviron/mediacommon v1.14.0 // indirect
|
||||||
github.com/google/uuid v1.6.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/randutil v0.1.0 // indirect
|
||||||
github.com/pion/rtcp v1.2.15 // indirect
|
github.com/pion/rtcp v1.2.15 // indirect
|
||||||
github.com/pion/sdp/v3 v3.0.10 // indirect
|
github.com/pion/sdp/v3 v3.0.10 // indirect
|
||||||
golang.org/x/net v0.34.0 // indirect
|
golang.org/x/net v0.37.0 // indirect
|
||||||
golang.org/x/sys v0.29.0 // indirect
|
golang.org/x/sys v0.31.0 // indirect
|
||||||
)
|
)
|
||||||
|
@ -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 h1:XSSHYt5+I0fyOnHWoNwM72DtivlmHFR0V9azgIi+ZVU=
|
||||||
github.com/Eyevinn/mp4ff v0.47.0/go.mod h1:hJNUUqOBryLAzUW9wpCJyw2HaI+TCd2rUPhafoS5lgg=
|
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 h1:3EzbyGb5+MIOJQYiWytRegFEP4EW5paiyTrscQj63WE=
|
||||||
github.com/bluenviron/gortsplib/v4 v4.12.3/go.mod h1:SkZPdaMNr+IvHt2PKRjUXxZN6FDutmSZn4eT0GmF0sk=
|
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 h1:lWCwOBKNKgqmspRpwpvvg3CidYm+XOc2+z/Jw7LM5dQ=
|
||||||
github.com/bluenviron/mediacommon v1.14.0/go.mod h1:z5LP9Tm1ZNfQV5Co54PyOzaIhGMusDfRKmh42nQSnyo=
|
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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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 h1:yfrARW/aVlqKORCdKrYdU0PZUKPqQvYEUQBKfVlNa9Q=
|
||||||
github.com/gen2brain/aac-go v0.0.0-20230119102159-ef1e76509d21/go.mod h1:HZqGD/LXHB1VCGUGNzuyxSsD12f3KjbJbvImAmoK/mM=
|
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/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 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
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 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
||||||
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
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 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo=
|
||||||
github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0=
|
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.12 h1:nsKs8Wi0jQyBFHU3qmn/OvtZrhktVfJY0vRxwACsL5U=
|
||||||
github.com/pion/rtp v1.8.11/go.mod h1:8uMBJj32Pa1wwx8Fuv/AsFhn8jsgw+3rUC2PfoBZ8p4=
|
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 h1:6MChLE/1xYB+CjumMw+gZ9ufp2DPApuVSnDT8t5MIgA=
|
||||||
github.com/pion/sdp/v3 v3.0.10/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E=
|
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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
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 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
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=
|
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/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 h1:XZYkjjiAg9QTBnHqEg37m2I9q3IIDv5JRYXs2N8ma7c=
|
||||||
github.com/zaf/g711 v1.4.0/go.mod h1:eCDXt3dSp/kYYAoooba7ukD/Q75jvAaS4WOMr0l1Roo=
|
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.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
|
||||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
31
writer/internal/config/config.go
Normal file
31
writer/internal/config/config.go
Normal file
@ -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)
|
||||||
|
}
|
42
writer/internal/media/g711.go
Normal file
42
writer/internal/media/g711.go
Normal file
@ -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
|
||||||
|
}
|
43
writer/internal/media/h264.go
Normal file
43
writer/internal/media/h264.go
Normal file
@ -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
|
||||||
|
}
|
180
writer/internal/procRTSP/client.go
Normal file
180
writer/internal/procRTSP/client.go
Normal file
@ -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())
|
||||||
|
}
|
143
writer/pkg/converter/mpegts_muxer.go
Normal file
143
writer/pkg/converter/mpegts_muxer.go
Normal file
@ -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)
|
||||||
|
}
|
37
writer/pkg/converter/pcm_to_aac.go
Normal file
37
writer/pkg/converter/pcm_to_aac.go
Normal file
@ -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
|
||||||
|
}
|
364
writer/pkg/packer/mp4.go
Normal file
364
writer/pkg/packer/mp4.go
Normal file
@ -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)
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user