300 lines
9.9 KiB
Go
300 lines
9.9 KiB
Go
package main
|
||
|
||
import (
|
||
"errors"
|
||
"fmt"
|
||
"log"
|
||
"os"
|
||
"strings"
|
||
"sync"
|
||
"time"
|
||
|
||
"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
|
||
fileMu sync.Mutex
|
||
)
|
||
|
||
func main() {
|
||
// Парсинг URL RTSP
|
||
u, err := base.ParseURL("rtsp://intercom-video-2.insit.ru/dp-ohusuxzcvzsnpzzvkpyhddnwxuyeyc")
|
||
if err != nil {
|
||
log.Fatalln("Ошибка парсинга URL:", err)
|
||
}
|
||
|
||
// Инициализация клиента и подключение к серверу
|
||
client := gortsplib.Client{}
|
||
err = client.Start(u.Scheme, u.Host)
|
||
if err != nil {
|
||
log.Fatalln("Ошибка соединения:", err)
|
||
}
|
||
defer client.Close()
|
||
|
||
// Получение описания
|
||
desc, _, err := client.Describe(u)
|
||
if err != nil {
|
||
log.Fatalln("Ошибка получения описания:", err)
|
||
}
|
||
|
||
// Установка настроек
|
||
err = client.SetupAll(desc.BaseURL, desc.Medias)
|
||
if err != nil {
|
||
log.Fatalln("Ошибка настройки:", err)
|
||
}
|
||
|
||
// Определение формата видео- и аудио-потоков
|
||
var h264Format *format.H264
|
||
var g711Format *format.G711
|
||
for _, media := range desc.Medias {
|
||
switch media.Formats[0].(type) {
|
||
case *format.H264:
|
||
h264Format = media.Formats[0].(*format.H264)
|
||
case *format.G711:
|
||
g711Format = media.Formats[0].(*format.G711)
|
||
}
|
||
}
|
||
|
||
if h264Format == nil || g711Format == nil {
|
||
log.Fatalln("Форматы не найдены")
|
||
}
|
||
|
||
// Создание декодеров для видео- и аудио-потоков
|
||
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 forma.(type) {
|
||
case *format.H264:
|
||
nalus, err := decoderH264.Decode(pkt)
|
||
if err != nil && !errors.Is(err, rtph264.ErrMorePacketsNeeded) {
|
||
log.Printf("Ошибка декодирования H264: %v", err)
|
||
return
|
||
}
|
||
fileMu.Lock()
|
||
defer fileMu.Unlock()
|
||
if currentVideoFile != nil {
|
||
for _, nalu := range nalus {
|
||
if _, err := currentVideoFile.Write([]byte{0x00, 0x00, 0x00, 0x01}); err != nil {
|
||
log.Printf("Ошибка записи префикса: %v", err)
|
||
}
|
||
if _, err := currentVideoFile.Write(nalu); err != nil {
|
||
log.Printf("Ошибка записи NALU: %v", err)
|
||
}
|
||
}
|
||
}
|
||
case *format.G711:
|
||
if strings.Contains(forma.RTPMap(), "PCMA") {
|
||
sampleG711a := g711.DecodeAlaw(pkt.Payload)
|
||
defer fileMu.Lock()
|
||
defer fileMu.Unlock()
|
||
if currentAudioFile != nil {
|
||
if _, err := currentAudioFile.Write(sampleG711a); err != nil {
|
||
log.Printf("Ошибка записи аудио: %v", err)
|
||
}
|
||
}
|
||
} else if strings.Contains(forma.RTPMap(), "PCMU") {
|
||
sampleG711u := g711.DecodeUlaw(pkt.Payload)
|
||
fileMu.Lock()
|
||
defer fileMu.Unlock()
|
||
if currentAudioFile != nil {
|
||
if _, err := currentAudioFile.Write(sampleG711u); err != nil {
|
||
log.Printf("Ошибка записи аудио: %v", err)
|
||
}
|
||
}
|
||
} else {
|
||
log.Println("Аудиокодек не идентифицирован")
|
||
}
|
||
}
|
||
})
|
||
|
||
// Запуск воспроизведения
|
||
_, err = client.Play(nil)
|
||
if err != nil {
|
||
log.Fatalln("Ошибка запуска воспроизведения:", err)
|
||
}
|
||
|
||
// Параметры записи
|
||
period := time.Hour
|
||
|
||
// Ожидание начала следующего часа
|
||
now := time.Now()
|
||
nextHour := now.Truncate(time.Hour).Add(time.Hour)
|
||
waitDuration := nextHour.Sub(now)
|
||
fmt.Printf("Ожидание до начала записи: %v\n", waitDuration)
|
||
time.Sleep(waitDuration)
|
||
log.Println("Начало записи фрагмента")
|
||
|
||
// Создаем начальные файлы
|
||
initialTimestamp := time.Now().Format("15-04_02-01-2006")
|
||
videoFilename := initialTimestamp + ".h264"
|
||
audioFilename := initialTimestamp + ".pcm"
|
||
|
||
fileVideo, err := os.Create(videoFilename)
|
||
if err != nil {
|
||
log.Fatalln("Ошибка создания видеофайла:", err)
|
||
}
|
||
fileAudio, err := os.Create(audioFilename)
|
||
if err != nil {
|
||
log.Fatalln("Ошибка создания аудиофайла:", err)
|
||
}
|
||
// Записываем SPS и PPS в начале видеофайла, если они заданы
|
||
if len(h264Format.SPS) > 0 {
|
||
fileVideo.Write([]byte{0x00, 0x00, 0x00, 0x01})
|
||
fileVideo.Write(h264Format.SPS)
|
||
}
|
||
if len(h264Format.PPS) > 0 {
|
||
fileVideo.Write([]byte{0x00, 0x00, 0x00, 0x01})
|
||
fileVideo.Write(h264Format.PPS)
|
||
}
|
||
fileMu.Lock()
|
||
currentVideoFile = fileVideo
|
||
currentAudioFile = fileAudio
|
||
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"
|
||
|
||
newVideoFile, err := os.Create(newVideoFilename)
|
||
if err != nil {
|
||
log.Printf("Ошибка создания видеофайла для фрагмента %d: %v", newTimestamp, err)
|
||
continue
|
||
}
|
||
newAudioFile, err := os.Create(newAudioFilename)
|
||
if err != nil {
|
||
log.Printf("Ошибка создания аудиофайла для фрагмента %d: %v", newTimestamp, err)
|
||
newVideoFile.Close()
|
||
continue
|
||
}
|
||
// Записываем SPS/PPS в новый видеофайл
|
||
if len(h264Format.SPS) > 0 {
|
||
newVideoFile.Write([]byte{0x00, 0x00, 0x00, 0x01})
|
||
newVideoFile.Write(h264Format.SPS)
|
||
}
|
||
if len(h264Format.PPS) > 0 {
|
||
newVideoFile.Write([]byte{0x00, 0x00, 0x00, 0x01})
|
||
newVideoFile.Write(h264Format.PPS)
|
||
}
|
||
|
||
// Переключаемся на новые файлы
|
||
fileMu.Lock()
|
||
oldVideoFile := currentVideoFile
|
||
oldAudioFile := currentAudioFile
|
||
oldAudioFilename := audioFilename
|
||
|
||
currentVideoFile = newVideoFile
|
||
currentAudioFile = newAudioFile
|
||
// Обновляем имена для следующей итерации
|
||
videoFilename = newVideoFilename
|
||
audioFilename = newAudioFilename
|
||
fileMu.Unlock()
|
||
|
||
// Закрываем старые файлы и запускаем обработку (слияние с аудио) в отдельной горутине
|
||
oldVideoFile.Close()
|
||
oldAudioFile.Close()
|
||
|
||
// Часть программы, вызывающая ffmpeg для объединения видеофайла .h264 аудиофайла .pcm в контейнер .mp4 -
|
||
// остается в программе до реализации аналогичного функционала без использования 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 аудиофайла после успешной конвертации.
|
||
go func(aFilename string) {
|
||
// Создание файла AAC
|
||
muxedFilename := strings.Replace(aFilename, ".pcm", ".aac", 1)
|
||
audioFileACC, err := os.Create(muxedFilename)
|
||
if err != nil {
|
||
log.Println("Ошибка создания файла AAC:", err)
|
||
return
|
||
}
|
||
defer audioFileACC.Close()
|
||
|
||
// Открытие PCM файла
|
||
audioFilename, err := os.Open(aFilename)
|
||
if err != nil {
|
||
log.Println("Ошибка открытия аудиофайла PCM:", 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
|
||
}
|
||
|
||
log.Println("Аудиофайл PCM конвертирован в аудиофайл ACC успешно")
|
||
|
||
// Удаление PCM аудиофайла
|
||
err = os.Remove(aFilename)
|
||
if err != nil {
|
||
log.Println("Ошибка удаления PCM файла:", err)
|
||
} else {
|
||
log.Printf("PCM файл %s успешно удалён", aFilename)
|
||
}
|
||
}(oldAudioFilename)
|
||
}
|
||
}()
|
||
var wg sync.WaitGroup
|
||
wg.Add(1)
|
||
wg.Wait()
|
||
}
|