recording video is corrected
This commit is contained in:
parent
47191f4be5
commit
e2ab953f8a
@ -1,14 +1,15 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/Eyevinn/mp4ff/mp4"
|
||||||
"github.com/bluenviron/gortsplib/v4"
|
"github.com/bluenviron/gortsplib/v4"
|
||||||
"github.com/bluenviron/gortsplib/v4/pkg/base"
|
"github.com/bluenviron/gortsplib/v4/pkg/base"
|
||||||
"github.com/bluenviron/gortsplib/v4/pkg/description"
|
"github.com/bluenviron/gortsplib/v4/pkg/description"
|
||||||
@ -20,19 +21,24 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
currentVideoFile *os.File
|
currentVideoFile *os.File
|
||||||
currentAudioFile *os.File
|
currentAudioFile *os.File
|
||||||
fileMu sync.Mutex
|
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
|
// Парсинг URL RTSP
|
||||||
u, err := base.ParseURL("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)
|
log.Fatalln("Ошибка парсинга URL:", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Инициализация клиента и подключение к серверу
|
// Инициализация клиента и подключение к камере
|
||||||
client := gortsplib.Client{}
|
client := gortsplib.Client{}
|
||||||
err = client.Start(u.Scheme, u.Host)
|
err = client.Start(u.Scheme, u.Host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -52,7 +58,7 @@ func main() {
|
|||||||
log.Fatalln("Ошибка настройки:", err)
|
log.Fatalln("Ошибка настройки:", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Определение формата видео- и аудио-потоков
|
// Проверка форматов для видео- и аудио-потоков
|
||||||
var h264Format *format.H264
|
var h264Format *format.H264
|
||||||
var g711Format *format.G711
|
var g711Format *format.G711
|
||||||
for _, media := range desc.Medias {
|
for _, media := range desc.Medias {
|
||||||
@ -63,57 +69,96 @@ func main() {
|
|||||||
g711Format = media.Formats[0].(*format.G711)
|
g711Format = media.Formats[0].(*format.G711)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if h264Format == nil || g711Format == nil {
|
if h264Format == nil || g711Format == nil {
|
||||||
log.Fatalln("Форматы не найдены")
|
log.Fatalln("Форматы не найдены")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Создание декодеров для видео- и аудио-потоков
|
// Создание декодера для видео-потока H264
|
||||||
decoderH264, err := h264Format.CreateDecoder()
|
decoderH264, err := h264Format.CreateDecoder()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln("Ошибка создания декодера H264:", err)
|
log.Fatalln("Ошибка создания декодера H264:", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Обработка RTP-пакетов: для видео – запись NAL-ов с префиксом, для аудио – запись PCM-данных.
|
// Обработка RTP-пакетов: для видео – сбор NAL-ов и запись в файл с префиксом, для аудио – запись PCM-данных.
|
||||||
client.OnPacketRTPAny(func(medi *description.Media, forma format.Format, pkt *rtp.Packet) {
|
client.OnPacketRTPAny(func(medi *description.Media, forma format.Format, pkt *rtp.Packet) {
|
||||||
switch forma.(type) {
|
switch f := forma.(type) {
|
||||||
case *format.H264:
|
case *format.H264:
|
||||||
|
formaH264Mu.Lock()
|
||||||
|
formaH264 = f
|
||||||
|
formaH264Mu.Unlock()
|
||||||
|
|
||||||
nalus, err := decoderH264.Decode(pkt)
|
nalus, err := decoderH264.Decode(pkt)
|
||||||
if err != nil && !errors.Is(err, rtph264.ErrMorePacketsNeeded) {
|
if err != nil && !errors.Is(err, rtph264.ErrMorePacketsNeeded) {
|
||||||
log.Printf("Ошибка декодирования H264: %v", err)
|
log.Printf("Ошибка декодирования H264: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
fileMu.Lock()
|
fileMu.Lock()
|
||||||
defer fileMu.Unlock()
|
|
||||||
if currentVideoFile != nil {
|
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 {
|
for _, nalu := range nalus {
|
||||||
if _, err := currentVideoFile.Write([]byte{0x00, 0x00, 0x00, 0x01}); err != nil {
|
if _, err := currentVideoFile.Write([]byte{0x00, 0x00, 0x00, 0x01}); err != nil {
|
||||||
log.Printf("Ошибка записи префикса: %v", err)
|
log.Printf("Ошибка записи стартового кода NALU: %v", err)
|
||||||
}
|
}
|
||||||
if _, err := currentVideoFile.Write(nalu); err != nil {
|
if _, err := currentVideoFile.Write(nalu); err != nil {
|
||||||
log.Printf("Ошибка записи NALU: %v", err)
|
log.Printf("Ошибка записи NALU: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fileMu.Unlock()
|
||||||
|
|
||||||
case *format.G711:
|
case *format.G711:
|
||||||
if strings.Contains(forma.RTPMap(), "PCMA") {
|
if strings.Contains(forma.RTPMap(), "PCMA") {
|
||||||
sampleG711a := g711.DecodeAlaw(pkt.Payload)
|
sampleG711a := g711.DecodeAlaw(pkt.Payload)
|
||||||
defer fileMu.Lock()
|
fileMu.Lock()
|
||||||
defer fileMu.Unlock()
|
|
||||||
if currentAudioFile != nil {
|
if currentAudioFile != nil {
|
||||||
if _, err := currentAudioFile.Write(sampleG711a); err != nil {
|
if _, err := currentAudioFile.Write(sampleG711a); err != nil {
|
||||||
log.Printf("Ошибка записи аудио: %v", err)
|
log.Printf("Ошибка записи аудио: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fileMu.Unlock()
|
||||||
} else if strings.Contains(forma.RTPMap(), "PCMU") {
|
} else if strings.Contains(forma.RTPMap(), "PCMU") {
|
||||||
sampleG711u := g711.DecodeUlaw(pkt.Payload)
|
sampleG711u := g711.DecodeUlaw(pkt.Payload)
|
||||||
fileMu.Lock()
|
fileMu.Lock()
|
||||||
defer fileMu.Unlock()
|
|
||||||
if currentAudioFile != nil {
|
if currentAudioFile != nil {
|
||||||
if _, err := currentAudioFile.Write(sampleG711u); err != nil {
|
if _, err := currentAudioFile.Write(sampleG711u); err != nil {
|
||||||
log.Printf("Ошибка записи аудио: %v", err)
|
log.Printf("Ошибка записи аудио: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fileMu.Unlock()
|
||||||
} else {
|
} else {
|
||||||
log.Println("Аудиокодек не идентифицирован")
|
log.Println("Аудиокодек не идентифицирован")
|
||||||
}
|
}
|
||||||
@ -126,21 +171,22 @@ func main() {
|
|||||||
log.Fatalln("Ошибка запуска воспроизведения:", err)
|
log.Fatalln("Ошибка запуска воспроизведения:", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Параметры записи
|
// Параметр записи
|
||||||
period := time.Hour
|
period := time.Hour
|
||||||
|
|
||||||
// Ожидание начала следующего часа
|
// Ожидание начала следующего часа
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
nextHour := now.Truncate(time.Hour).Add(time.Hour)
|
nextSegment := now.Truncate(time.Hour).Add(time.Hour)
|
||||||
waitDuration := nextHour.Sub(now)
|
waitDuration := nextSegment.Sub(now)
|
||||||
fmt.Printf("Ожидание до начала записи: %v\n", waitDuration)
|
log.Printf("Ожидание до начала записи: %v\n", waitDuration)
|
||||||
time.Sleep(waitDuration)
|
time.Sleep(waitDuration)
|
||||||
log.Println("Начало записи фрагмента")
|
log.Println("Начало записи фрагмента")
|
||||||
|
|
||||||
// Создаем начальные файлы
|
// Создание начальных файлов
|
||||||
initialTimestamp := time.Now().Format("15-04_02-01-2006")
|
initialTimestamp := time.Now().Format("15-04_02-01-2006")
|
||||||
videoFilename := initialTimestamp + ".h264"
|
videoFilename := initialTimestamp + ".h264"
|
||||||
audioFilename := initialTimestamp + ".pcm"
|
audioFilename := initialTimestamp + ".pcm"
|
||||||
|
mp4Filename := initialTimestamp + ".mp4"
|
||||||
|
|
||||||
fileVideo, err := os.Create(videoFilename)
|
fileVideo, err := os.Create(videoFilename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -150,150 +196,446 @@ func main() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln("Ошибка создания аудиофайла:", err)
|
log.Fatalln("Ошибка создания аудиофайла:", err)
|
||||||
}
|
}
|
||||||
// Записываем SPS и PPS в начале видеофайла, если они заданы
|
fileMP4, err := os.Create(mp4Filename)
|
||||||
if len(h264Format.SPS) > 0 {
|
if err != nil {
|
||||||
fileVideo.Write([]byte{0x00, 0x00, 0x00, 0x01})
|
log.Fatalln("Ошибка создания mp4 файла:", err)
|
||||||
fileVideo.Write(h264Format.SPS)
|
|
||||||
}
|
|
||||||
if len(h264Format.PPS) > 0 {
|
|
||||||
fileVideo.Write([]byte{0x00, 0x00, 0x00, 0x01})
|
|
||||||
fileVideo.Write(h264Format.PPS)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fileMu.Lock()
|
fileMu.Lock()
|
||||||
currentVideoFile = fileVideo
|
currentVideoFile = fileVideo
|
||||||
currentAudioFile = fileAudio
|
currentAudioFile = fileAudio
|
||||||
|
currentMP4File = fileMP4
|
||||||
|
|
||||||
|
// Начало нового сегмента – флаг сбрасывается и ожидается первый IDR
|
||||||
|
currentSegmentStarted = false
|
||||||
fileMu.Unlock()
|
fileMu.Unlock()
|
||||||
|
|
||||||
// Используем тикер для периодической ротации файлов без остановки записи
|
// Тикер для периодической ротации файлов
|
||||||
ticker := time.NewTicker(period)
|
ticker := time.NewTicker(period)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
||||||
// Горутина, отвечающая за смену файлов
|
// Горутина для смены файлов
|
||||||
go func() {
|
go func() {
|
||||||
for range ticker.C {
|
for range ticker.C {
|
||||||
// Открываем новые файлы заранее, чтобы запись шла непрерывно
|
|
||||||
newTimestamp := time.Now().Format("15-04_02-01-2006")
|
newTimestamp := time.Now().Format("15-04_02-01-2006")
|
||||||
newVideoFilename := newTimestamp + ".h264"
|
newVideoFilename := newTimestamp + ".h264"
|
||||||
newAudioFilename := newTimestamp + ".pcm"
|
newAudioFilename := newTimestamp + ".pcm"
|
||||||
|
newMP4Filename := newTimestamp + ".mp4"
|
||||||
|
|
||||||
newVideoFile, err := os.Create(newVideoFilename)
|
newVideoFile, err := os.Create(newVideoFilename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Ошибка создания видеофайла для фрагмента %d: %v", newTimestamp, err)
|
log.Printf("Ошибка создания видеофайла для фрагмента %s: %v", newTimestamp, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
newAudioFile, err := os.Create(newAudioFilename)
|
newAudioFile, err := os.Create(newAudioFilename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Ошибка создания аудиофайла для фрагмента %d: %v", newTimestamp, err)
|
log.Printf("Ошибка создания аудиофайла для фрагмента %s: %v", newTimestamp, err)
|
||||||
newVideoFile.Close()
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// Записываем SPS/PPS в новый видеофайл
|
newMP4File, err := os.Create(newMP4Filename)
|
||||||
if len(h264Format.SPS) > 0 {
|
if err != nil {
|
||||||
newVideoFile.Write([]byte{0x00, 0x00, 0x00, 0x01})
|
log.Printf("Ошибка создания mp4 файла для фрагмента %s: %v", newTimestamp, err)
|
||||||
newVideoFile.Write(h264Format.SPS)
|
|
||||||
}
|
|
||||||
if len(h264Format.PPS) > 0 {
|
|
||||||
newVideoFile.Write([]byte{0x00, 0x00, 0x00, 0x01})
|
|
||||||
newVideoFile.Write(h264Format.PPS)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Переключаемся на новые файлы
|
// Переключение на новые файлы
|
||||||
fileMu.Lock()
|
fileMu.Lock()
|
||||||
oldVideoFile := currentVideoFile
|
oldVideoFile := currentVideoFile
|
||||||
oldAudioFile := currentAudioFile
|
oldAudioFile := currentAudioFile
|
||||||
|
oldMP4File := currentMP4File
|
||||||
|
|
||||||
|
oldVideoFilename := videoFilename
|
||||||
oldAudioFilename := audioFilename
|
oldAudioFilename := audioFilename
|
||||||
|
oldMP4Filename := mp4Filename
|
||||||
|
|
||||||
currentVideoFile = newVideoFile
|
currentVideoFile = newVideoFile
|
||||||
currentAudioFile = newAudioFile
|
currentAudioFile = newAudioFile
|
||||||
// Обновляем имена для следующей итерации
|
currentMP4File = newMP4File
|
||||||
|
|
||||||
|
// Новый сегмент ещё не начат – флаг сбрасывается и ожидается первый IDR
|
||||||
|
currentSegmentStarted = false
|
||||||
|
|
||||||
|
// Обновление имен для следующей итерации
|
||||||
videoFilename = newVideoFilename
|
videoFilename = newVideoFilename
|
||||||
audioFilename = newAudioFilename
|
audioFilename = newAudioFilename
|
||||||
|
mp4Filename = newMP4Filename
|
||||||
fileMu.Unlock()
|
fileMu.Unlock()
|
||||||
|
|
||||||
// Закрываем старые файлы и запускаем обработку (слияние с аудио) в отдельной горутине
|
// Закрытие старых файлов
|
||||||
oldVideoFile.Close()
|
oldVideoFile.Close()
|
||||||
oldAudioFile.Close()
|
oldAudioFile.Close()
|
||||||
|
oldMP4File.Close()
|
||||||
|
|
||||||
// Часть программы, вызывающая ffmpeg для объединения видеофайла .h264 аудиофайла .pcm в контейнер .mp4 -
|
log.Println("Созданы новые файлы для записи:", videoFilename, audioFilename, mp4Filename)
|
||||||
// остается в программе до реализации аналогичного функционала без использования ffmpeg.
|
|
||||||
/*
|
|
||||||
// Передаем значения переменных в замыкание, чтобы избежать их изменения
|
|
||||||
go func(vFilename, aFilename string, newTimestamp string) {
|
|
||||||
muxedFilename := strings.Replace(vFilename, ".h264", ".mp4", 1)
|
|
||||||
cmd := exec.Command("ffmpeg",
|
|
||||||
"-y",
|
|
||||||
"-fflags", "+genpts",
|
|
||||||
"-r", "25",
|
|
||||||
"-f", "h264", "-i", vFilename,
|
|
||||||
"-f", "s16le", "-ar", "8000", "-ac", "1", "-i", aFilename,
|
|
||||||
"-filter_complex", "[1:a]atempo=0.5[aud]",
|
|
||||||
"-map", "0:v",
|
|
||||||
"-map", "[aud]",
|
|
||||||
"-c:v", "copy",
|
|
||||||
"-bsf:v", "dump_extra",
|
|
||||||
"-c:a", "aac",
|
|
||||||
muxedFilename,
|
|
||||||
)
|
|
||||||
if err := cmd.Run(); err != nil {
|
|
||||||
log.Printf("Ошибка при объединении фрагментов: %v", err)
|
|
||||||
} else {
|
|
||||||
log.Printf("Фрагменты объединены в файл %s", muxedFilename)
|
|
||||||
}
|
|
||||||
}(oldVideoFilename, oldAudioFilename, newTimestamp)
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Создание AAC аудиофайла из PCM аудиофайла и удаление PCM аудиофайла после успешной конвертации.
|
// Горутина для объединения видео и аудио в контейнер MP4
|
||||||
go func(aFilename string) {
|
go func(oldVideoFilename, oldAudioFilename, oldMP4Filename string) {
|
||||||
// Создание файла AAC
|
formaH264Mu.Lock()
|
||||||
muxedFilename := strings.Replace(aFilename, ".pcm", ".aac", 1)
|
h264Params := formaH264
|
||||||
audioFileACC, err := os.Create(muxedFilename)
|
formaH264Mu.Unlock()
|
||||||
if err != nil {
|
|
||||||
log.Println("Ошибка создания файла AAC:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer audioFileACC.Close()
|
|
||||||
|
|
||||||
// Открытие PCM файла
|
// Конвертация PCM в AAC
|
||||||
audioFilename, err := os.Open(aFilename)
|
aacFilename := strings.Replace(oldAudioFilename, ".pcm", ".aac", 1)
|
||||||
if err != nil {
|
if err := convertPCMtoAAC(oldAudioFilename, aacFilename); err != nil {
|
||||||
log.Println("Ошибка открытия аудиофайла PCM:", err)
|
log.Printf("Ошибка конвертации аудио: %v", err)
|
||||||
return
|
|
||||||
}
|
|
||||||
defer audioFilename.Close()
|
|
||||||
|
|
||||||
// Создание инкодера AAC
|
|
||||||
opts := &aac.Options{
|
|
||||||
SampleRate: 8000,
|
|
||||||
NumChannels: 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
encoderAAC, err := aac.NewEncoder(audioFileACC, opts)
|
|
||||||
if err != nil {
|
|
||||||
log.Println("Ошибка создания инкодера AAC:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer encoderAAC.Close()
|
|
||||||
|
|
||||||
// Запись аудиофайла AAC
|
|
||||||
err = encoderAAC.Encode(audioFilename)
|
|
||||||
if err != nil {
|
|
||||||
log.Println("Ошибка инкодирования в AAC:", err)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Println("Аудиофайл PCM конвертирован в аудиофайл ACC успешно")
|
log.Printf("Файл %s успешно конвертирован в %s\n", oldAudioFilename, aacFilename)
|
||||||
|
|
||||||
// Удаление PCM аудиофайла
|
// Создание MP4 файл
|
||||||
err = os.Remove(aFilename)
|
if err := createMP4File(oldVideoFilename, aacFilename, oldMP4Filename, h264Params); err != nil {
|
||||||
if err != nil {
|
log.Printf("Ошибка создания MP4: %v", err)
|
||||||
log.Println("Ошибка удаления PCM файла:", err)
|
|
||||||
} else {
|
} else {
|
||||||
log.Printf("PCM файл %s успешно удалён", aFilename)
|
log.Printf("Файлы %s, %s успешно объединены в контейнер MP4: %s\n",
|
||||||
|
oldVideoFilename, aacFilename, oldMP4Filename)
|
||||||
}
|
}
|
||||||
}(oldAudioFilename)
|
|
||||||
|
// Удаление временных файлов
|
||||||
|
os.Remove(oldVideoFilename)
|
||||||
|
os.Remove(aacFilename)
|
||||||
|
os.Remove(oldAudioFilename)
|
||||||
|
|
||||||
|
log.Printf("Файлы %s, %s, %s успешно удалены\n", oldVideoFilename, aacFilename, oldAudioFilename)
|
||||||
|
}(oldVideoFilename, oldAudioFilename, oldMP4Filename)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
var wg sync.WaitGroup
|
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
wg.Wait()
|
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,9 +3,11 @@ module writer
|
|||||||
go 1.23.6
|
go 1.23.6
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
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/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.11
|
||||||
|
github.com/zaf/g711 v1.4.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@ -14,7 +16,6 @@ require (
|
|||||||
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
|
||||||
github.com/zaf/g711 v1.4.0 // indirect
|
|
||||||
golang.org/x/net v0.34.0 // indirect
|
golang.org/x/net v0.34.0 // indirect
|
||||||
golang.org/x/sys v0.29.0 // indirect
|
golang.org/x/sys v0.29.0 // indirect
|
||||||
)
|
)
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
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/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=
|
||||||
@ -6,6 +8,7 @@ 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/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/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
||||||
@ -24,8 +27,6 @@ github.com/youpy/go-riff v0.1.0 h1:vZO/37nI4tIET8tQI0Qn0Y79qQh99aEpponTPiPut7k=
|
|||||||
github.com/youpy/go-riff v0.1.0/go.mod h1:83nxdDV4Z9RzrTut9losK7ve4hUnxUR8ASSz4BsKXwQ=
|
github.com/youpy/go-riff v0.1.0/go.mod h1:83nxdDV4Z9RzrTut9losK7ve4hUnxUR8ASSz4BsKXwQ=
|
||||||
github.com/youpy/go-wav v0.3.2 h1:NLM8L/7yZ0Bntadw/0h95OyUsen+DQIVf9gay+SUsMU=
|
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 v0.0.0-20190814101024-76a4a538f52b h1:QqixIpc5WFIqTLxB3Hq8qs0qImAgBdq0p6rq2Qdl634=
|
|
||||||
github.com/zaf/g711 v0.0.0-20190814101024-76a4a538f52b/go.mod h1:T2h1zV50R/q0CVYnsQOQ6L7P4a2ZxH47ixWcMXFGyx8=
|
|
||||||
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.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||||
|
Loading…
x
Reference in New Issue
Block a user