300 lines
9.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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