v 0.0.72 (работатет видео)

This commit is contained in:
DND
2026-06-11 10:46:28 +07:00
parent f22642c5d4
commit 247c4d6f68
17 changed files with 1179 additions and 532 deletions
+15
View File
@@ -0,0 +1,15 @@
{
// Используйте IntelliSense, чтобы узнать о возможных атрибутах.
// Наведите указатель мыши, чтобы просмотреть описания существующих атрибутов.
// Для получения дополнительной информации посетите: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"request": "launch",
"name": "Запустить Chrome на localhost",
"url": "http://localhost:8080",
"webRoot": "${workspaceFolder}"
}
]
}
Binary file not shown.
+241 -33
View File
@@ -3,6 +3,7 @@ package database
import (
"database/sql"
"encoding/json"
"fmt"
"log"
"os"
"path/filepath"
@@ -20,17 +21,27 @@ type DB struct {
}
type ChannelConfigDB struct {
ID string `json:"id"`
Name string `json:"name"`
Device string `json:"device"`
Duration int `json:"duration"`
SampleRate int `json:"sample_rate"`
Channels int `json:"channels"`
Format string `json:"format"`
AutoRestart bool `json:"auto_restart"`
SMBFolder string `json:"smb_folder"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
ID string `json:"id"`
Name string `json:"name"`
Device string `json:"device"`
Duration int `json:"duration"`
SampleRate int `json:"sample_rate"`
Channels int `json:"channels"`
Format string `json:"format"`
AutoRestart bool `json:"auto_restart"`
SMBFolder string `json:"smb_folder"`
// Видео поля
ChannelType string `json:"channel_type"`
VideoDevice string `json:"video_device"`
VideoResolution string `json:"video_resolution"`
VideoFramerate int `json:"video_framerate"`
VideoBitrate int `json:"video_bitrate"`
VideoCodec string `json:"video_codec"`
VideoAudioOffset int `json:"video_audio_offset"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type RecordingDB struct {
@@ -114,6 +125,11 @@ func (db *DB) initDB() error {
return err
}
// Выполняем миграцию для добавления видео колонок
if err := db.migrateVideoColumns(); err != nil {
log.Printf("Warning: video columns migration error: %v", err)
}
log.Println("Database initialized successfully")
return nil
}
@@ -130,6 +146,13 @@ func (db *DB) createTables() error {
format TEXT DEFAULT 'low',
auto_restart INTEGER DEFAULT 0,
smb_folder TEXT DEFAULT '',
channel_type TEXT DEFAULT 'audio',
video_device TEXT DEFAULT '',
video_resolution TEXT DEFAULT '1280x720',
video_framerate INTEGER DEFAULT 30,
video_bitrate INTEGER DEFAULT 2000,
video_codec TEXT DEFAULT 'libx264',
video_audio_offset INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`,
@@ -223,22 +246,23 @@ func (db *DB) createTables() error {
failed_attempts INTEGER DEFAULT 0,
locked_until DATETIME
)`,
`CREATE TABLE IF NOT EXISTS video_recordings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
channel_id TEXT NOT NULL,
filename TEXT NOT NULL,
file_path TEXT NOT NULL,
size INTEGER DEFAULT 0,
duration INTEGER DEFAULT 0,
width INTEGER DEFAULT 0,
height INTEGER DEFAULT 0,
framerate INTEGER DEFAULT 0,
bitrate INTEGER DEFAULT 0,
codec TEXT DEFAULT '',
status TEXT DEFAULT 'recording',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
completed_at DATETIME,
FOREIGN KEY (channel_id) REFERENCES channels(id) ON DELETE CASCADE
id INTEGER PRIMARY KEY AUTOINCREMENT,
channel_id TEXT NOT NULL,
filename TEXT NOT NULL,
file_path TEXT NOT NULL,
size INTEGER DEFAULT 0,
duration INTEGER DEFAULT 0,
width INTEGER DEFAULT 0,
height INTEGER DEFAULT 0,
framerate INTEGER DEFAULT 0,
bitrate INTEGER DEFAULT 0,
codec TEXT DEFAULT '',
status TEXT DEFAULT 'recording',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
completed_at DATETIME,
FOREIGN KEY (channel_id) REFERENCES channels(id) ON DELETE CASCADE
)`,
}
@@ -251,6 +275,60 @@ func (db *DB) createTables() error {
return nil
}
// migrateVideoColumns добавляет колонки для видео в существующую таблицу channels
func (db *DB) migrateVideoColumns() error {
columns := []struct {
name string
colType string
defaultVal string
}{
{"channel_type", "TEXT", "'audio'"},
{"video_device", "TEXT", "''"},
{"video_resolution", "TEXT", "'1280x720'"},
{"video_framerate", "INTEGER", "30"},
{"video_bitrate", "INTEGER", "2000"},
{"video_codec", "TEXT", "'libx264'"},
{"video_audio_offset", "INTEGER", "0"},
}
for _, col := range columns {
// Проверяем существует ли колонка
var count int
query := `SELECT COUNT(*) FROM pragma_table_info('channels') WHERE name = ?`
if err := db.conn.QueryRow(query, col.name).Scan(&count); err != nil {
log.Printf("Warning: failed to check column %s: %v", col.name, err)
continue
}
if count == 0 {
alterQuery := fmt.Sprintf("ALTER TABLE channels ADD COLUMN %s %s DEFAULT %s",
col.name, col.colType, col.defaultVal)
if _, err := db.conn.Exec(alterQuery); err != nil {
log.Printf("Warning: failed to add column %s: %v", col.name, err)
} else {
log.Printf("Added column %s to channels table", col.name)
}
}
}
// Обновляем существующие записи, устанавливая значения по умолчанию
updateQuery := `UPDATE channels SET
channel_type = COALESCE(channel_type, 'audio'),
video_device = COALESCE(video_device, ''),
video_resolution = COALESCE(video_resolution, '1280x720'),
video_framerate = COALESCE(video_framerate, 30),
video_bitrate = COALESCE(video_bitrate, 2000),
video_codec = COALESCE(video_codec, 'libx264'),
video_audio_offset = COALESCE(video_audio_offset, 0)
WHERE channel_type IS NULL OR channel_type = ''`
if _, err := db.conn.Exec(updateQuery); err != nil {
log.Printf("Warning: failed to update existing channels: %v", err)
}
return nil
}
// ========== Channel methods ==========
func (db *DB) SaveChannel(config ChannelConfigDB) error {
@@ -260,13 +338,18 @@ func (db *DB) SaveChannel(config ChannelConfigDB) error {
oldConfig, _ := db.getChannel(config.ID)
query := `INSERT OR REPLACE INTO channels
(id, name, device, duration, sample_rate, channels, format, auto_restart, smb_folder, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`
(id, name, device, duration, sample_rate, channels, format, auto_restart, smb_folder,
channel_type, video_device, video_resolution, video_framerate, video_bitrate,
video_codec, video_audio_offset, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`
_, err := db.conn.Exec(query,
config.ID, config.Name, config.Device, config.Duration,
config.SampleRate, config.Channels, config.Format,
config.AutoRestart, config.SMBFolder)
config.AutoRestart, config.SMBFolder,
config.ChannelType, config.VideoDevice, config.VideoResolution,
config.VideoFramerate, config.VideoBitrate, config.VideoCodec,
config.VideoAudioOffset)
if err != nil {
log.Printf("Error saving channel %s: %v", config.ID, err)
@@ -283,14 +366,22 @@ func (db *DB) SaveChannel(config ChannelConfigDB) error {
}
func (db *DB) getChannel(id string) (*ChannelConfigDB, error) {
query := `SELECT id, name, device, duration, sample_rate, channels, format, auto_restart, smb_folder, created_at, updated_at
query := `SELECT id, name, device, duration, sample_rate, channels, format, auto_restart, smb_folder,
channel_type, video_device, video_resolution, video_framerate, video_bitrate,
video_codec, video_audio_offset, created_at, updated_at
FROM channels WHERE id = ?`
var config ChannelConfigDB
var channelType, videoDevice, videoResolution, videoCodec sql.NullString
var videoFramerate, videoBitrate, videoAudioOffset sql.NullInt64
err := db.conn.QueryRow(query, id).Scan(
&config.ID, &config.Name, &config.Device, &config.Duration,
&config.SampleRate, &config.Channels, &config.Format,
&config.AutoRestart, &config.SMBFolder, &config.CreatedAt, &config.UpdatedAt)
&config.AutoRestart, &config.SMBFolder,
&channelType, &videoDevice, &videoResolution,
&videoFramerate, &videoBitrate, &videoCodec,
&videoAudioOffset, &config.CreatedAt, &config.UpdatedAt)
switch {
case err == sql.ErrNoRows:
@@ -299,6 +390,49 @@ func (db *DB) getChannel(id string) (*ChannelConfigDB, error) {
return nil, err
}
// Установка значений по умолчанию для NULL значений
if channelType.Valid {
config.ChannelType = channelType.String
} else {
config.ChannelType = "audio"
}
if videoDevice.Valid {
config.VideoDevice = videoDevice.String
} else {
config.VideoDevice = ""
}
if videoResolution.Valid {
config.VideoResolution = videoResolution.String
} else {
config.VideoResolution = "1280x720"
}
if videoFramerate.Valid {
config.VideoFramerate = int(videoFramerate.Int64)
} else {
config.VideoFramerate = 30
}
if videoBitrate.Valid {
config.VideoBitrate = int(videoBitrate.Int64)
} else {
config.VideoBitrate = 2000
}
if videoCodec.Valid {
config.VideoCodec = videoCodec.String
} else {
config.VideoCodec = "libx264"
}
if videoAudioOffset.Valid {
config.VideoAudioOffset = int(videoAudioOffset.Int64)
} else {
config.VideoAudioOffset = 0
}
return &config, nil
}
@@ -312,7 +446,9 @@ func (db *DB) GetAllChannels() ([]ChannelConfigDB, error) {
db.mu.RLock()
defer db.mu.RUnlock()
query := `SELECT id, name, device, duration, sample_rate, channels, format, auto_restart, smb_folder, created_at, updated_at
query := `SELECT id, name, device, duration, sample_rate, channels, format, auto_restart, smb_folder,
channel_type, video_device, video_resolution, video_framerate, video_bitrate,
video_codec, video_audio_offset, created_at, updated_at
FROM channels ORDER BY created_at`
rows, err := db.conn.Query(query)
@@ -324,13 +460,63 @@ func (db *DB) GetAllChannels() ([]ChannelConfigDB, error) {
var channels []ChannelConfigDB
for rows.Next() {
var config ChannelConfigDB
var channelType, videoDevice, videoResolution, videoCodec sql.NullString
var videoFramerate, videoBitrate, videoAudioOffset sql.NullInt64
err := rows.Scan(
&config.ID, &config.Name, &config.Device, &config.Duration,
&config.SampleRate, &config.Channels, &config.Format,
&config.AutoRestart, &config.SMBFolder, &config.CreatedAt, &config.UpdatedAt)
&config.AutoRestart, &config.SMBFolder,
&channelType, &videoDevice, &videoResolution,
&videoFramerate, &videoBitrate, &videoCodec,
&videoAudioOffset, &config.CreatedAt, &config.UpdatedAt)
if err != nil {
return nil, err
}
// Установка значений по умолчанию для NULL значений
if channelType.Valid {
config.ChannelType = channelType.String
} else {
config.ChannelType = "audio"
}
if videoDevice.Valid {
config.VideoDevice = videoDevice.String
} else {
config.VideoDevice = ""
}
if videoResolution.Valid {
config.VideoResolution = videoResolution.String
} else {
config.VideoResolution = "1280x720"
}
if videoFramerate.Valid {
config.VideoFramerate = int(videoFramerate.Int64)
} else {
config.VideoFramerate = 30
}
if videoBitrate.Valid {
config.VideoBitrate = int(videoBitrate.Int64)
} else {
config.VideoBitrate = 2000
}
if videoCodec.Valid {
config.VideoCodec = videoCodec.String
} else {
config.VideoCodec = "libx264"
}
if videoAudioOffset.Valid {
config.VideoAudioOffset = int(videoAudioOffset.Int64)
} else {
config.VideoAudioOffset = 0
}
channels = append(channels, config)
}
@@ -845,6 +1031,28 @@ func (db *DB) logChannelChanges(old, new *ChannelConfigDB, changedBy string) {
if old.SMBFolder != new.SMBFolder {
db.logSettingChange("channels", new.ID, "smb_folder", old.SMBFolder, new.SMBFolder, changedBy)
}
// Видео настройки
if old.ChannelType != new.ChannelType {
db.logSettingChange("channels", new.ID, "channel_type", old.ChannelType, new.ChannelType, changedBy)
}
if old.VideoDevice != new.VideoDevice {
db.logSettingChange("channels", new.ID, "video_device", old.VideoDevice, new.VideoDevice, changedBy)
}
if old.VideoResolution != new.VideoResolution {
db.logSettingChange("channels", new.ID, "video_resolution", old.VideoResolution, new.VideoResolution, changedBy)
}
if old.VideoFramerate != new.VideoFramerate {
db.logSettingChange("channels", new.ID, "video_framerate", intToString(old.VideoFramerate), intToString(new.VideoFramerate), changedBy)
}
if old.VideoBitrate != new.VideoBitrate {
db.logSettingChange("channels", new.ID, "video_bitrate", intToString(old.VideoBitrate), intToString(new.VideoBitrate), changedBy)
}
if old.VideoCodec != new.VideoCodec {
db.logSettingChange("channels", new.ID, "video_codec", old.VideoCodec, new.VideoCodec, changedBy)
}
if old.VideoAudioOffset != new.VideoAudioOffset {
db.logSettingChange("channels", new.ID, "video_audio_offset", intToString(old.VideoAudioOffset), intToString(new.VideoAudioOffset), changedBy)
}
}
func (db *DB) GetSettingsHistory(limit int) ([]map[string]interface{}, error) {
+99
View File
@@ -295,3 +295,102 @@
{"id":452,"timestamp":"2026-06-10T16:43:54.469945728+07:00","level":"INFO","category":"WIREGUARD","message":"WireGuard запущен успешно"}
{"id":453,"timestamp":"2026-06-10T16:44:04.099343908+07:00","level":"WARNING","category":"OPENVPN","message":"Подключение не установлено, проверьте логи"}
{"id":454,"timestamp":"2026-06-10T16:44:58.629865195+07:00","level":"INFO","category":"OPENVPN","message":"Остановка OpenVPN"}
{"id":455,"timestamp":"2026-06-11T08:35:29.618048264+07:00","level":"INFO","category":"OPENVPN","message":"Восстановление подключения после перезагрузки"}
{"id":456,"timestamp":"2026-06-11T08:35:29.618542557+07:00","level":"INFO","category":"VPN","message":"VPN Manager инициализирован | VPN по умолчанию: wireguard"}
{"id":457,"timestamp":"2026-06-11T08:35:34.643389231+07:00","level":"INFO","category":"WIREGUARD","message":"Запуск WireGuard | Интерфейс: wg0"}
{"id":458,"timestamp":"2026-06-11T08:35:34.645427059+07:00","level":"INFO","category":"OPENVPN","message":"Запуск OpenVPN | Конфиг: /home/dnd/pfSense-UDP4-1194-DND-config.ovpn"}
{"id":459,"timestamp":"2026-06-11T08:35:34.648114473+07:00","level":"INFO","category":"OPENVPN","message":"OpenVPN запущен, ожидание подключения..."}
{"id":460,"timestamp":"2026-06-11T08:35:34.858237227+07:00","level":"INFO","category":"WIREGUARD","message":"WireGuard запущен успешно"}
{"id":461,"timestamp":"2026-06-11T08:35:44.659283013+07:00","level":"WARNING","category":"OPENVPN","message":"Подключение не установлено, проверьте логи"}
{"id":462,"timestamp":"2026-06-11T08:36:12.603766985+07:00","level":"INFO","category":"OPENVPN","message":"Остановка OpenVPN"}
{"id":463,"timestamp":"2026-06-11T08:36:56.800733483+07:00","level":"INFO","category":"VPN","message":"VPN Manager инициализирован | VPN по умолчанию: none"}
{"id":464,"timestamp":"2026-06-11T08:49:59.291459949+07:00","level":"SETTINGS","category":"SETTINGS","message":"Изменение настроек | Канал: channel1 | настройки канала = сохранены"}
{"id":465,"timestamp":"2026-06-11T08:49:59.299236184+07:00","level":"SETTINGS","category":"SETTINGS","message":"Изменение настроек | Канал: channel1 | расписание = обновлено"}
{"id":466,"timestamp":"2026-06-11T08:50:19.331116698+07:00","level":"INFO","category":"RECORDING","message":"Запись: НАЧАЛО | Канал: channel1 | Файл: 2026-06-11 08-50-19.wav"}
{"id":467,"timestamp":"2026-06-11T08:50:41.215507399+07:00","level":"SETTINGS","category":"SETTINGS","message":"Изменение настроек | Канал: channel1 | настройки канала = сохранены"}
{"id":468,"timestamp":"2026-06-11T08:50:41.233737179+07:00","level":"SETTINGS","category":"SETTINGS","message":"Изменение настроек | Канал: channel1 | расписание = обновлено"}
{"id":469,"timestamp":"2026-06-11T08:50:51.784094037+07:00","level":"INFO","category":"RECORDING","message":"Запись: НАЧАЛО | Канал: channel1 | Файл: 2026-06-11 08-50-51.wav"}
{"id":470,"timestamp":"2026-06-11T08:54:08.083840846+07:00","level":"SETTINGS","category":"SETTINGS","message":"Изменение настроек | Канал: channel1 | настройки канала = сохранены"}
{"id":471,"timestamp":"2026-06-11T08:54:08.100799753+07:00","level":"SETTINGS","category":"SETTINGS","message":"Изменение настроек | Канал: channel1 | расписание = обновлено"}
{"id":472,"timestamp":"2026-06-11T09:44:54.153781939+07:00","level":"INFO","category":"VPN","message":"Остановка всех VPN клиентов"}
{"id":473,"timestamp":"2026-06-11T09:44:54.153831491+07:00","level":"INFO","category":"VPN","message":"Все VPN клиенты остановлены"}
{"id":474,"timestamp":"2026-06-11T09:45:45.074318639+07:00","level":"INFO","category":"VPN","message":"VPN Manager инициализирован | VPN по умолчанию: none"}
{"id":475,"timestamp":"2026-06-11T09:46:11.592258028+07:00","level":"SETTINGS","category":"SETTINGS","message":"Изменение настроек | Канал: channel1 | настройки канала = сохранены"}
{"id":476,"timestamp":"2026-06-11T09:46:11.617861986+07:00","level":"SETTINGS","category":"SETTINGS","message":"Изменение настроек | Канал: channel1 | расписание = обновлено"}
{"id":477,"timestamp":"2026-06-11T09:46:23.469837207+07:00","level":"INFO","category":"RECORDING","message":"Запись: ВИДЕО НАЧАЛО | Канал: channel1 | Файл: 2026-06-11_09-46-23.mp4 | Длительность: 1м 0с"}
{"id":478,"timestamp":"2026-06-11T09:46:53.180643045+07:00","level":"INFO","category":"SMB","message":"Добавлен в очередь повторных попыток | Файл: 2026-06-11_09-46-23.mp4 | Папка: channel1"}
{"id":479,"timestamp":"2026-06-11T09:48:15.372621915+07:00","level":"SETTINGS","category":"SETTINGS","message":"Изменение настроек | Канал: channel1 | настройки канала = сохранены"}
{"id":480,"timestamp":"2026-06-11T09:48:15.409122328+07:00","level":"SETTINGS","category":"SETTINGS","message":"Изменение настроек | Канал: channel1 | расписание = обновлено"}
{"id":481,"timestamp":"2026-06-11T09:48:19.040243829+07:00","level":"INFO","category":"RECORDING","message":"Запись: НАЧАЛО | Канал: channel1 | Файл: 2026-06-11 09-48-19.wav"}
{"id":482,"timestamp":"2026-06-11T09:48:21.686939937+07:00","level":"INFO","category":"VIDEO","message":"Синхронная запись запущена | Канал: channel1"}
{"id":483,"timestamp":"2026-06-11T09:48:21.686974575+07:00","level":"INFO","category":"RECORDING","message":"Запись: АУДИО+ВИДЕО НАЧАЛО | Канал: channel1 | Файл: mixed_2026-06-11_09-48-21.mp4 | Длительность: 1м 0с"}
{"id":484,"timestamp":"2026-06-11T09:51:53.13985635+07:00","level":"INFO","category":"VPN","message":"Остановка всех VPN клиентов"}
{"id":485,"timestamp":"2026-06-11T09:51:53.139903325+07:00","level":"INFO","category":"VPN","message":"Все VPN клиенты остановлены"}
{"id":486,"timestamp":"2026-06-11T09:52:54.519113025+07:00","level":"INFO","category":"VPN","message":"VPN Manager инициализирован | VPN по умолчанию: none"}
{"id":487,"timestamp":"2026-06-11T09:53:28.771537241+07:00","level":"SETTINGS","category":"SETTINGS","message":"Изменение настроек | Канал: channel1 | настройки канала = сохранены"}
{"id":488,"timestamp":"2026-06-11T09:53:28.805611157+07:00","level":"SETTINGS","category":"SETTINGS","message":"Изменение настроек | Канал: channel1 | расписание = обновлено"}
{"id":489,"timestamp":"2026-06-11T09:53:34.978879667+07:00","level":"SETTINGS","category":"SETTINGS","message":"Изменение настроек | Канал: channel1 | настройки канала = сохранены"}
{"id":490,"timestamp":"2026-06-11T09:53:35.013267378+07:00","level":"SETTINGS","category":"SETTINGS","message":"Изменение настроек | Канал: channel1 | расписание = обновлено"}
{"id":491,"timestamp":"2026-06-11T09:53:41.762206646+07:00","level":"INFO","category":"RECORDING","message":"Запись: НАЧАЛО | Канал: channel1 | Файл: 2026-06-11 09-53-41.wav"}
{"id":492,"timestamp":"2026-06-11T09:53:44.408841321+07:00","level":"INFO","category":"VIDEO","message":"Синхронная запись запущена | Канал: channel1"}
{"id":493,"timestamp":"2026-06-11T09:53:44.408888495+07:00","level":"INFO","category":"RECORDING","message":"Запись: АУДИО+ВИДЕО НАЧАЛО | Канал: channel1 | Файл: mixed_2026-06-11_09-53-44.mp4 | Длительность: 1м 0с"}
{"id":494,"timestamp":"2026-06-11T09:54:35.163192108+07:00","level":"SETTINGS","category":"SETTINGS","message":"Изменение настроек | Канал: channel1 | настройки канала = сохранены"}
{"id":495,"timestamp":"2026-06-11T09:54:35.200703475+07:00","level":"SETTINGS","category":"SETTINGS","message":"Изменение настроек | Канал: channel1 | расписание = обновлено"}
{"id":496,"timestamp":"2026-06-11T09:54:39.834784507+07:00","level":"INFO","category":"RECORDING","message":"Запись: НАЧАЛО | Канал: channel1 | Файл: 2026-06-11 09-54-39.wav"}
{"id":497,"timestamp":"2026-06-11T09:55:35.2291069+07:00","level":"INFO","category":"VPN","message":"Остановка всех VPN клиентов"}
{"id":498,"timestamp":"2026-06-11T09:55:35.229155868+07:00","level":"INFO","category":"VPN","message":"Все VPN клиенты остановлены"}
{"id":499,"timestamp":"2026-06-11T10:09:26.562430876+07:00","level":"INFO","category":"VPN","message":"VPN Manager инициализирован | VPN по умолчанию: none"}
{"id":500,"timestamp":"2026-06-11T10:09:28.575306705+07:00","level":"INFO","category":"RECORDING","message":"Запись: НАЧАЛО | Канал: channel1 | Файл: 2026-06-11 10-09-28.wav"}
{"id":501,"timestamp":"2026-06-11T10:10:30.62806718+07:00","level":"INFO","category":"SMB","message":"Добавлен в очередь повторных попыток | Файл: 2026-06-11 10-09-28.wav | Папка: channel1"}
{"id":502,"timestamp":"2026-06-11T10:10:30.629318697+07:00","level":"INFO","category":"RECORDING","message":"Запись: ЗАВЕРШЕНА | Канал: channel1 | Файл: 2026-06-11 10-09-28.wav | Длительность: 1м 0с | Размер: 1 MB"}
{"id":503,"timestamp":"2026-06-11T10:10:30.63060279+07:00","level":"INFO","category":"SMB","message":"Добавлен в очередь повторных попыток | Файл: 2026-06-11 10-09-28.wav | Папка: channel1"}
{"id":504,"timestamp":"2026-06-11T10:15:20.893299271+07:00","level":"SETTINGS","category":"SETTINGS","message":"Изменение настроек | Канал: channel1 | настройки канала = сохранены"}
{"id":505,"timestamp":"2026-06-11T10:15:20.934865403+07:00","level":"SETTINGS","category":"SETTINGS","message":"Изменение настроек | Канал: channel1 | расписание = обновлено"}
{"id":506,"timestamp":"2026-06-11T10:15:24.14437315+07:00","level":"INFO","category":"RECORDING","message":"Запись: АУДИО+ВИДЕО НАЧАЛО | Канал: channel1 | Файл: 2026-06-11_10-15-24.mp4 | Длительность: 1м 0с"}
{"id":507,"timestamp":"2026-06-11T10:16:24.170745543+07:00","level":"INFO","category":"SMB","message":"Добавлен в очередь повторных попыток | Файл: 2026-06-11_10-15-24.mp4 | Папка: channel1"}
{"id":508,"timestamp":"2026-06-11T10:16:24.171947322+07:00","level":"INFO","category":"RECORDING","message":"Запись: АУДИО+ВИДЕО ЗАВЕРШЕНА | Канал: channel1 | Файл: 2026-06-11_10-15-24.mp4 | Длительность: 1м 0с | Размер: 14 MB"}
{"id":509,"timestamp":"2026-06-11T10:16:24.173207386+07:00","level":"INFO","category":"SMB","message":"Добавлен в очередь повторных попыток | Файл: 2026-06-11_10-15-24.mp4 | Папка: channel1"}
{"id":510,"timestamp":"2026-06-11T10:19:12.257700588+07:00","level":"INFO","category":"VPN","message":"Остановка всех VPN клиентов"}
{"id":511,"timestamp":"2026-06-11T10:19:12.257753979+07:00","level":"INFO","category":"VPN","message":"Все VPN клиенты остановлены"}
{"id":512,"timestamp":"2026-06-11T10:21:58.452866241+07:00","level":"INFO","category":"VPN","message":"VPN Manager инициализирован | VPN по умолчанию: none"}
{"id":513,"timestamp":"2026-06-11T10:22:22.243359639+07:00","level":"SETTINGS","category":"SETTINGS","message":"Изменение настроек | Канал: channel1 | настройки канала = сохранены"}
{"id":514,"timestamp":"2026-06-11T10:22:22.28008747+07:00","level":"SETTINGS","category":"SETTINGS","message":"Изменение настроек | Канал: channel1 | расписание = обновлено"}
{"id":515,"timestamp":"2026-06-11T10:22:25.896895117+07:00","level":"INFO","category":"RECORDING","message":"Запись: АУДИО+ВИДЕО НАЧАЛО | Канал: channel1 | Файл: 2026-06-11_10-22-25.mp4 | Длительность: 1м 0с"}
{"id":516,"timestamp":"2026-06-11T10:23:25.92267812+07:00","level":"INFO","category":"SMB","message":"Добавлен в очередь повторных попыток | Файл: 2026-06-11_10-22-25.mp4 | Папка: channel1"}
{"id":517,"timestamp":"2026-06-11T10:23:25.923952866+07:00","level":"INFO","category":"RECORDING","message":"Запись: АУДИО+ВИДЕО ЗАВЕРШЕНА | Канал: channel1 | Файл: 2026-06-11_10-22-25.mp4 | Длительность: 1м 0с | Размер: 14 MB"}
{"id":518,"timestamp":"2026-06-11T10:23:25.925154242+07:00","level":"INFO","category":"SMB","message":"Добавлен в очередь повторных попыток | Файл: 2026-06-11_10-22-25.mp4 | Папка: channel1"}
{"id":519,"timestamp":"2026-06-11T10:29:44.019876674+07:00","level":"INFO","category":"VPN","message":"Остановка всех VPN клиентов"}
{"id":520,"timestamp":"2026-06-11T10:29:44.029569614+07:00","level":"INFO","category":"VPN","message":"Все VPN клиенты остановлены"}
{"id":521,"timestamp":"2026-06-11T10:30:58.457976195+07:00","level":"INFO","category":"VPN","message":"VPN Manager инициализирован | VPN по умолчанию: none"}
{"id":522,"timestamp":"2026-06-11T10:31:23.153961679+07:00","level":"SETTINGS","category":"SETTINGS","message":"Изменение настроек | Канал: channel1 | настройки канала = сохранены"}
{"id":523,"timestamp":"2026-06-11T10:31:23.190144689+07:00","level":"SETTINGS","category":"SETTINGS","message":"Изменение настроек | Канал: channel1 | расписание = обновлено"}
{"id":524,"timestamp":"2026-06-11T10:31:26.328853046+07:00","level":"INFO","category":"RECORDING","message":"Запись: АУДИО+ВИДЕО НАЧАЛО | Канал: channel1 | Файл: 2026-06-11_10-31-26.mp4 | Длительность: 1м 0с"}
{"id":525,"timestamp":"2026-06-11T10:32:26.357400301+07:00","level":"INFO","category":"SMB","message":"Добавлен в очередь повторных попыток | Файл: 2026-06-11_10-31-26.mp4 | Папка: channel1"}
{"id":526,"timestamp":"2026-06-11T10:32:26.358956941+07:00","level":"INFO","category":"RECORDING","message":"Запись: АУДИО+ВИДЕО ЗАВЕРШЕНА | Канал: channel1 | Файл: 2026-06-11_10-31-26.mp4 | Длительность: 1м 0с | Размер: 6 MB"}
{"id":527,"timestamp":"2026-06-11T10:32:26.360142563+07:00","level":"INFO","category":"SMB","message":"Добавлен в очередь повторных попыток | Файл: 2026-06-11_10-31-26.mp4 | Папка: channel1"}
{"id":528,"timestamp":"2026-06-11T10:32:44.038745727+07:00","level":"SETTINGS","category":"SETTINGS","message":"Изменение настроек | Канал: channel1 | настройки канала = сохранены"}
{"id":529,"timestamp":"2026-06-11T10:32:44.074410691+07:00","level":"SETTINGS","category":"SETTINGS","message":"Изменение настроек | Канал: channel1 | расписание = обновлено"}
{"id":530,"timestamp":"2026-06-11T10:32:46.293341921+07:00","level":"INFO","category":"RECORDING","message":"Запись: ВИДЕО НАЧАЛО | Канал: channel1 | Файл: 2026-06-11_10-32-46.mp4 | Длительность: 1м 0с"}
{"id":531,"timestamp":"2026-06-11T10:33:46.320126373+07:00","level":"INFO","category":"SMB","message":"Добавлен в очередь повторных попыток | Файл: 2026-06-11_10-32-46.mp4 | Папка: channel1"}
{"id":532,"timestamp":"2026-06-11T10:33:46.321464394+07:00","level":"INFO","category":"RECORDING","message":"Запись: ВИДЕО ЗАВЕРШЕНА | Канал: channel1 | Файл: 2026-06-11_10-32-46.mp4 | Длительность: 1м 0с | Размер: 5 MB"}
{"id":533,"timestamp":"2026-06-11T10:33:46.322704982+07:00","level":"INFO","category":"SMB","message":"Добавлен в очередь повторных попыток | Файл: 2026-06-11_10-32-46.mp4 | Папка: channel1"}
{"id":534,"timestamp":"2026-06-11T10:36:23.861819985+07:00","level":"INFO","category":"VPN","message":"Остановка всех VPN клиентов"}
{"id":535,"timestamp":"2026-06-11T10:36:23.861870127+07:00","level":"INFO","category":"VPN","message":"Все VPN клиенты остановлены"}
{"id":536,"timestamp":"2026-06-11T10:37:02.23998958+07:00","level":"INFO","category":"VPN","message":"VPN Manager инициализирован | VPN по умолчанию: none"}
{"id":537,"timestamp":"2026-06-11T10:37:32.898081319+07:00","level":"SETTINGS","category":"SETTINGS","message":"Изменение настроек | Канал: channel1 | настройки канала = сохранены"}
{"id":538,"timestamp":"2026-06-11T10:37:32.933999337+07:00","level":"SETTINGS","category":"SETTINGS","message":"Изменение настроек | Канал: channel1 | расписание = обновлено"}
{"id":539,"timestamp":"2026-06-11T10:37:38.267008225+07:00","level":"INFO","category":"RECORDING","message":"Запись: АУДИО+ВИДЕО НАЧАЛО | Канал: channel1 | Файл: 2026-06-11_10-37-38.mp4 | Длительность: 30с"}
{"id":540,"timestamp":"2026-06-11T10:38:08.293871402+07:00","level":"INFO","category":"SMB","message":"Добавлен в очередь повторных попыток | Файл: 2026-06-11_10-37-38.mp4 | Папка: channel1"}
{"id":541,"timestamp":"2026-06-11T10:38:08.295066178+07:00","level":"INFO","category":"RECORDING","message":"Запись: АУДИО+ВИДЕО ЗАВЕРШЕНА | Канал: channel1 | Файл: 2026-06-11_10-37-38.mp4 | Длительность: 30с | Размер: 3 MB"}
{"id":542,"timestamp":"2026-06-11T10:38:08.296314264+07:00","level":"INFO","category":"SMB","message":"Добавлен в очередь повторных попыток | Файл: 2026-06-11_10-37-38.mp4 | Папка: channel1"}
{"id":543,"timestamp":"2026-06-11T10:41:22.842677909+07:00","level":"INFO","category":"VPN","message":"Остановка всех VPN клиентов"}
{"id":544,"timestamp":"2026-06-11T10:41:22.852317478+07:00","level":"INFO","category":"VPN","message":"Все VPN клиенты остановлены"}
{"id":545,"timestamp":"2026-06-11T10:42:03.403988903+07:00","level":"INFO","category":"VPN","message":"VPN Manager инициализирован | VPN по умолчанию: none"}
{"id":546,"timestamp":"2026-06-11T10:42:32.283055534+07:00","level":"SETTINGS","category":"SETTINGS","message":"Изменение настроек | Канал: channel1 | настройки канала = сохранены"}
{"id":547,"timestamp":"2026-06-11T10:42:32.318771179+07:00","level":"SETTINGS","category":"SETTINGS","message":"Изменение настроек | Канал: channel1 | расписание = обновлено"}
{"id":548,"timestamp":"2026-06-11T10:42:36.62756771+07:00","level":"INFO","category":"RECORDING","message":"Запись: АУДИО+ВИДЕО НАЧАЛО | Канал: channel1 | Файл: 2026-06-11_10-42-36.mp4 | Длительность: 30с"}
{"id":549,"timestamp":"2026-06-11T10:43:05.189238622+07:00","level":"SETTINGS","category":"SETTINGS","message":"Изменение настроек | Канал: channel2 | настройки канала = сохранены"}
{"id":550,"timestamp":"2026-06-11T10:43:05.225466874+07:00","level":"SETTINGS","category":"SETTINGS","message":"Изменение настроек | Канал: channel2 | расписание = обновлено"}
{"id":551,"timestamp":"2026-06-11T10:43:06.754044528+07:00","level":"INFO","category":"SMB","message":"Добавлен в очередь повторных попыток | Файл: 2026-06-11_10-42-36.mp4 | Папка: channel1"}
{"id":552,"timestamp":"2026-06-11T10:43:06.756317516+07:00","level":"INFO","category":"RECORDING","message":"Запись: АУДИО+ВИДЕО ЗАВЕРШЕНА | Канал: channel1 | Файл: 2026-06-11_10-42-36.mp4 | Длительность: 30с | Размер: 3 MB"}
{"id":553,"timestamp":"2026-06-11T10:43:06.75937835+07:00","level":"INFO","category":"SMB","message":"Добавлен в очередь повторных попыток | Файл: 2026-06-11_10-42-36.mp4 | Папка: channel1"}
+520 -76
View File
@@ -1,12 +1,14 @@
package recorder
import (
"bufio"
"context"
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"regexp"
"strconv"
"strings"
"sync"
@@ -21,7 +23,7 @@ import (
type Channel struct {
ID string `json:"id"`
Name string `json:"name"`
ChannelType string `json:"channel_type"` // audio, video, both
ChannelType string `json:"channel_type"`
Device string `json:"device"`
IsRecording bool `json:"is_recording"`
CurrentFile string `json:"current_file"`
@@ -33,12 +35,7 @@ type Channel struct {
SMBFolder string `json:"smb_folder"`
CreatedAt time.Time `json:"created_at"`
ScheduleEnabled bool `json:"schedule_enabled"`
StartDate string `json:"start_date"`
StartTime string `json:"start_time"`
EndDate string `json:"end_date"`
EndTime string `json:"end_time"`
RepeatDaily bool `json:"repeat_daily"`
// Видео настройки
VideoDevice string `json:"video_device"`
VideoResolution string `json:"video_resolution"`
VideoFramerate int `json:"video_framerate"`
@@ -46,6 +43,15 @@ type Channel struct {
VideoCodec string `json:"video_codec"`
VideoAudioOffset int `json:"video_audio_offset"`
// Расписание
ScheduleEnabled bool `json:"schedule_enabled"`
StartDate string `json:"start_date"`
StartTime string `json:"start_time"`
EndDate string `json:"end_date"`
EndTime string `json:"end_time"`
RepeatDaily bool `json:"repeat_daily"`
// Внутренние поля
cmd *exec.Cmd
cancelFunc context.CancelFunc
ctx context.Context
@@ -125,20 +131,27 @@ func NewChannel(id, name, device string, duration, sampleRate, channels int, for
audioDataSize := getOptimalAudioBufferSize(sampleRate, channels)
return &Channel{
ID: id,
Name: name,
Device: device,
Duration: duration,
SampleRate: sampleRate,
Channels: channels,
Format: format,
AutoRestart: autoRestart,
SMBFolder: "",
CreatedAt: time.Now(),
OutputDir: outputDir,
audioData: make([]byte, 0, audioDataSize),
stopChan: make(chan struct{}),
ScheduleEnabled: false,
ID: id,
Name: name,
Device: device,
Duration: duration,
SampleRate: sampleRate,
Channels: channels,
Format: format,
AutoRestart: autoRestart,
SMBFolder: "",
CreatedAt: time.Now(),
OutputDir: outputDir,
audioData: make([]byte, 0, audioDataSize),
stopChan: make(chan struct{}),
ScheduleEnabled: false,
ChannelType: "audio",
VideoDevice: "",
VideoResolution: "640x480",
VideoFramerate: 30,
VideoBitrate: 2000,
VideoCodec: "libx264",
VideoAudioOffset: 0,
}
}
@@ -159,6 +172,7 @@ func (ch *Channel) saveStateToDB() {
}
}
// StartRecording - основной метод запуска записи (диспетчер)
func (ch *Channel) StartRecording(outputDir string, testDeviceFunc func(string) (bool, string)) error {
ch.mu.Lock()
defer ch.mu.Unlock()
@@ -167,6 +181,21 @@ func (ch *Channel) StartRecording(outputDir string, testDeviceFunc func(string)
return fmt.Errorf("recording already in progress")
}
// Определяем тип канала
switch ch.ChannelType {
case "video":
return ch.startVideoRecording(outputDir)
case "both":
return ch.startBothRecording(outputDir, testDeviceFunc)
default: // "audio"
return ch.startAudioRecording(outputDir, testDeviceFunc)
}
}
// startAudioRecording - запись только аудио
func (ch *Channel) startAudioRecording(outputDir string, testDeviceFunc func(string) (bool, string)) error {
log.Printf("Starting audio recording on channel %s", ch.ID)
db := database.GetDB()
recordingsDir, err := db.GetSetting("recordings_dir")
if err != nil || recordingsDir == "" {
@@ -234,7 +263,6 @@ func (ch *Channel) StartRecording(outputDir string, testDeviceFunc func(string)
ch.cmd = cmd
// Оптимизированный буфер для архитектуры
bufferSize := getOptimalBufferSize()
go func() {
@@ -340,6 +368,404 @@ func (ch *Channel) StartRecording(outputDir string, testDeviceFunc func(string)
return nil
}
// supportsMJPEG - проверяет поддержку MJPEG формата камерой (только одно объявление!)
func supportsMJPEG(device string) bool {
cmd := exec.Command("v4l2-ctl", "-d", device, "--list-formats-ext")
output, err := cmd.Output()
if err != nil {
return false
}
return strings.Contains(string(output), "JPEG") || strings.Contains(string(output), "MJPG")
}
// startVideoRecording - запись только видео
func (ch *Channel) startVideoRecording(outputDir string) error {
log.Printf("Starting video-only recording on channel %s", ch.ID)
// Проверяем наличие ffmpeg
if _, err := exec.LookPath("ffmpeg"); err != nil {
return fmt.Errorf("ffmpeg not installed")
}
// Проверяем видео устройство
if ch.VideoDevice == "" {
return fmt.Errorf("video device not specified")
}
now := time.Now()
filename := now.Format("2006-01-02_15-04-05") + ".mp4"
// Сохраняем в ту же папку
channelDir := filepath.Join(outputDir, ch.ID)
if err := os.MkdirAll(channelDir, 0755); err != nil {
return fmt.Errorf("cannot create channel directory: %v", err)
}
fullPath := filepath.Join(channelDir, filename)
// Создаем контекст для остановки
ch.stopChan = make(chan struct{})
ch.stopOnce = sync.Once{}
ch.ctx, ch.cancelFunc = context.WithCancel(context.Background())
// Используем параметры из успешного теста test2.mp4
args := []string{
"-f", "v4l2",
"-input_format", "mjpeg",
"-framerate", "15",
"-video_size", "640x480",
"-i", ch.VideoDevice,
"-c:v", "libx264",
"-preset", "ultrafast",
"-crf", "23",
"-pix_fmt", "yuv420p",
"-movflags", "+faststart",
"-y", fullPath,
}
cmd := exec.CommandContext(ch.ctx, "ffmpeg", args...)
log.Printf("Video command: ffmpeg %v", args)
// Захватываем stdout и stderr
stderr, err := cmd.StderrPipe()
if err != nil {
return fmt.Errorf("error creating stderr pipe: %v", err)
}
stdout, err := cmd.StdoutPipe()
if err != nil {
return fmt.Errorf("error creating stdout pipe: %v", err)
}
if err := cmd.Start(); err != nil {
return fmt.Errorf("error starting video recording: %v", err)
}
ch.cmd = cmd
ch.IsRecording = true
ch.CurrentFile = filename
// Логируем вывод ffmpeg
go func() {
scanner := bufio.NewScanner(stderr)
for scanner.Scan() {
line := scanner.Text()
log.Printf("ffmpeg: %s", line)
// Проверяем наличие кадров
if strings.Contains(line, "frame=") {
log.Printf("FFMPEG PROGRESS: %s", line)
}
}
}()
// Читаем stdout для отладки
go func() {
buf := make([]byte, 1024)
for {
select {
case <-ch.ctx.Done():
return
default:
n, err := stdout.Read(buf)
if n > 0 {
log.Printf("ffmpeg stdout: %s", string(buf[:n]))
}
if err != nil {
return
}
}
}
}()
// Сохраняем в БД
db := database.GetDB()
recording := &database.RecordingDB{
ChannelID: ch.ID,
Filename: filename,
FilePath: fullPath,
Status: "recording",
SMBFolder: ch.SMBFolder,
CreatedAt: now,
}
if err := db.CreateRecording(recording); err != nil {
log.Printf("Warning: Failed to save video recording to DB: %v", err)
}
ch.saveStateToDB()
// Таймер для остановки
if ch.timer != nil {
ch.timer.Stop()
}
ch.timer = time.AfterFunc(time.Duration(ch.Duration)*time.Second, func() {
log.Printf("Timer triggered for video channel %s after %d seconds", ch.ID, ch.Duration)
ch.StopRecording()
// Проверяем созданный файл
if info, err := os.Stat(fullPath); err == nil {
log.Printf("Video file size: %d bytes", info.Size())
if info.Size() < 10000 {
log.Printf("WARNING: Video file is too small (%d bytes), recording may have failed", info.Size())
}
} else {
log.Printf("ERROR: Video file not found: %s", fullPath)
}
if ch.AutoRestart {
time.Sleep(1 * time.Second)
ch.StartRecording(outputDir, nil)
}
})
go func() {
err := cmd.Wait()
log.Printf("Video recording process for channel %s finished: %v", ch.ID, err)
// Дополнительная проверка файла после завершения
if info, err := os.Stat(fullPath); err == nil {
log.Printf("Final video file size: %d bytes", info.Size())
}
ch.mu.Lock()
if ch.IsRecording {
ch.IsRecording = false
ch.CurrentFile = ""
ch.saveStateToDB()
}
ch.mu.Unlock()
}()
log.Printf("✅ Video recording started on channel %s: %s", ch.ID, fullPath)
logger.AddRecording(ch.ID, filename, ch.Duration, 0, "ВИДЕО НАЧАЛО")
return nil
}
// getCameraBestSettings - получает оптимальные настройки камеры
func getCameraBestSettings(device string) (format string, width, height, fps int) {
// Получаем список форматов
cmd := exec.Command("v4l2-ctl", "-d", device, "--list-formats-ext")
output, err := cmd.Output()
if err != nil {
return "mjpeg", 640, 480, 15
}
lines := strings.Split(string(output), "\n")
var currentFormat string
var maxWidth, maxHeight int
var maxFPS int
for _, line := range lines {
line = strings.TrimSpace(line)
// Ищем формат
if strings.Contains(line, "Pixel Format:") {
if strings.Contains(line, "MJPG") || strings.Contains(line, "JPEG") {
currentFormat = "mjpeg"
} else if strings.Contains(line, "YUYV") {
currentFormat = "yuyv422"
}
}
// Ищем разрешение
if strings.Contains(line, "Size:") && strings.Contains(line, "x") {
// Извлекаем разрешение
re := regexp.MustCompile(`(\d+)x(\d+)`)
matches := re.FindStringSubmatch(line)
if len(matches) >= 3 {
w, _ := strconv.Atoi(matches[1])
h, _ := strconv.Atoi(matches[2])
if w*h > maxWidth*maxHeight {
maxWidth = w
maxHeight = h
}
}
}
// Ищем FPS
if strings.Contains(line, "Interval:") && strings.Contains(line, "fps") {
re := regexp.MustCompile(`(\d+\.?\d*) fps`)
matches := re.FindStringSubmatch(line)
if len(matches) >= 2 {
f, _ := strconv.ParseFloat(matches[1], 32)
if int(f) > maxFPS {
maxFPS = int(f)
}
}
}
}
if maxWidth == 0 {
maxWidth = 640
maxHeight = 480
}
if maxFPS == 0 {
maxFPS = 15
}
if currentFormat == "" {
currentFormat = "mjpeg"
}
return currentFormat, maxWidth, maxHeight, maxFPS
}
// startBothRecording - синхронная запись аудио и видео
func (ch *Channel) startBothRecording(outputDir string, testDeviceFunc func(string) (bool, string)) error {
log.Printf("Starting both (audio+video) recording on channel %s", ch.ID)
// Проверяем наличие ffmpeg
if _, err := exec.LookPath("ffmpeg"); err != nil {
return fmt.Errorf("ffmpeg not installed. Install: sudo apt install ffmpeg")
}
// Проверяем видео устройство
if ch.VideoDevice == "" {
return fmt.Errorf("video device not specified")
}
if _, err := os.Stat(ch.VideoDevice); os.IsNotExist(err) {
return fmt.Errorf("video device %s does not exist", ch.VideoDevice)
}
now := time.Now()
filename := now.Format("2006-01-02_15-04-05") + ".mp4"
// Сохраняем в ту же папку
channelDir := filepath.Join(outputDir, ch.ID)
if err := os.MkdirAll(channelDir, 0755); err != nil {
return fmt.Errorf("cannot create channel directory: %v", err)
}
fullPath := filepath.Join(channelDir, filename)
// Создаем контекст для остановки
ch.stopChan = make(chan struct{})
ch.stopOnce = sync.Once{}
ch.ctx, ch.cancelFunc = context.WithCancel(context.Background())
// Получаем аудио устройство
audioDevice := "default"
if ch.Device != "" && ch.Device != "default" {
audioDevice = ch.Device
}
// Формируем команду ffmpeg для микса (видео MJPEG + аудио)
args := []string{
"-f", "v4l2",
"-input_format", "mjpeg",
"-framerate", "15",
"-video_size", "640x480",
"-i", ch.VideoDevice,
"-f", "alsa",
"-i", audioDevice,
"-c:v", "libx264",
"-preset", "ultrafast",
"-crf", "23",
"-pix_fmt", "yuv420p",
"-c:a", "aac",
"-b:a", "128k",
"-y", fullPath,
}
cmd := exec.CommandContext(ch.ctx, "ffmpeg", args...)
log.Printf("FFmpeg command: ffmpeg %v", args)
// Захватываем вывод для отладки
stderr, err := cmd.StderrPipe()
if err != nil {
return fmt.Errorf("error creating stderr pipe: %v", err)
}
if err := cmd.Start(); err != nil {
return fmt.Errorf("error starting both recording: %v", err)
}
ch.cmd = cmd
ch.IsRecording = true
ch.CurrentFile = filename
// Логируем вывод ffmpeg
go func() {
scanner := bufio.NewScanner(stderr)
for scanner.Scan() {
line := scanner.Text()
log.Printf("ffmpeg (%s): %s", ch.ID, line)
}
}()
// Сохраняем в БД
db := database.GetDB()
recording := &database.RecordingDB{
ChannelID: ch.ID,
Filename: filename,
FilePath: fullPath,
Status: "recording",
SMBFolder: ch.SMBFolder,
CreatedAt: now,
}
if err := db.CreateRecording(recording); err != nil {
log.Printf("Warning: Failed to save recording to DB: %v", err)
}
ch.saveStateToDB()
// Таймер для остановки
if ch.timer != nil {
ch.timer.Stop()
}
ch.timer = time.AfterFunc(time.Duration(ch.Duration)*time.Second, func() {
log.Printf("Timer triggered for both channel %s after %d seconds", ch.ID, ch.Duration)
currentFile := ch.CurrentFile
currentPath := fullPath
smbFolder := ch.SMBFolder
ch.StopRecording()
if recording.ID > 0 {
db.UpdateRecordingStatus(recording.ID, "completed")
if fileInfo, err := os.Stat(currentPath); err == nil && fileInfo.Size() > 0 {
logger.AddRecording(ch.ID, currentFile, ch.Duration, fileInfo.Size(), "АУДИО+ВИДЕО ЗАВЕРШЕНА")
db.UpdateRecordingStats(recording.ID, fileInfo.Size(), ch.Duration)
}
}
if smbFolder != "" && onRecordingCompleted != nil {
log.Printf("Calling SMB upload callback for file: %s, folder: %s", currentFile, smbFolder)
onRecordingCompleted(currentFile, currentPath, smbFolder)
}
if ch.AutoRestart {
log.Printf("Auto-restarting both recording for channel %s", ch.ID)
time.Sleep(1 * time.Second)
if err := ch.StartRecording(outputDir, testDeviceFunc); err != nil {
log.Printf("Auto-restart failed for both channel %s: %v", ch.ID, err)
}
}
})
go func() {
err := cmd.Wait()
log.Printf("Both recording process for channel %s finished: %v", ch.ID, err)
ch.mu.Lock()
if ch.IsRecording {
ch.IsRecording = false
ch.CurrentFile = ""
ch.saveStateToDB()
}
ch.mu.Unlock()
}()
logger.AddRecording(ch.ID, filename, ch.Duration, 0, "АУДИО+ВИДЕО НАЧАЛО")
log.Printf("✅ Both (audio+video) recording started on channel %s: %s", ch.ID, fullPath)
return nil
}
// StopRecording - остановка записи
func (ch *Channel) StopRecording() {
ch.stopOnce.Do(func() {
log.Printf("=== STOPPING RECORDING for channel %s ===", ch.ID)
@@ -348,17 +774,20 @@ func (ch *Channel) StopRecording() {
currentPath := ""
smbFolder := ch.SMBFolder
log.Printf("DEBUG: currentFile=%s, smbFolder=%s", currentFile, smbFolder)
log.Printf("DEBUG: currentFile=%s, smbFolder=%s, channelType=%s", currentFile, smbFolder, ch.ChannelType)
// Получаем путь к файлу из команды
ch.mu.Lock()
if ch.cmd != nil && ch.cmd.Process != nil {
// Ищем путь к файлу в аргументах команды
for _, arg := range ch.cmd.Args {
if strings.HasSuffix(arg, ".wav") {
if strings.HasSuffix(arg, ".wav") || strings.HasSuffix(arg, ".mp4") || strings.HasSuffix(arg, ".mkv") {
currentPath = arg
break
}
}
}
if currentPath == "" && currentFile != "" {
db := database.GetDB()
recordingsDir, _ := db.GetSetting("recordings_dir")
@@ -370,6 +799,38 @@ func (ch *Channel) StopRecording() {
log.Printf("DEBUG: currentPath=%s", currentPath)
ch.mu.Unlock()
// Отправляем SIGTERM для корректного завершения ffmpeg/arecord
ch.mu.Lock()
if ch.cmd != nil && ch.cmd.Process != nil {
log.Printf("Sending SIGTERM to process %d", ch.cmd.Process.Pid)
// Отправляем SIGTERM сигнал
if err := ch.cmd.Process.Signal(syscall.SIGTERM); err != nil {
log.Printf("Failed to send SIGTERM: %v, trying kill", err)
ch.cmd.Process.Kill()
}
// Ждем завершения процесса (максимум 5 секунд)
done := make(chan error, 1)
go func() {
done <- ch.cmd.Wait()
}()
select {
case <-time.After(5 * time.Second):
log.Printf("Process didn't exit after 5 seconds, killing...")
ch.cmd.Process.Kill()
case err := <-done:
if err != nil {
log.Printf("Process exited with error: %v", err)
} else {
log.Printf("Process exited gracefully")
}
}
}
ch.mu.Unlock()
// Закрываем канал остановки
select {
case <-ch.stopChan:
log.Printf("stopChan already closed for channel %s", ch.ID)
@@ -378,34 +839,19 @@ func (ch *Channel) StopRecording() {
log.Printf("stopChan closed for channel %s", ch.ID)
}
// Отменяем контекст
if ch.cancelFunc != nil {
ch.cancelFunc()
log.Printf("Context cancelled for channel %s", ch.ID)
}
var pid int
if ch.cmd != nil && ch.cmd.Process != nil {
pid = ch.cmd.Process.Pid
log.Printf("Process PID for channel %s: %d", ch.ID, pid)
}
if pid > 0 {
log.Printf("Killing process %d for channel %s", pid, ch.ID)
if err := syscall.Kill(pid, syscall.SIGKILL); err != nil {
log.Printf("syscall.Kill error: %v", err)
}
killCmd := exec.Command("kill", "-9", strconv.Itoa(pid))
if output, err := killCmd.CombinedOutput(); err != nil {
log.Printf("kill -9 error: %v, output: %s", err, string(output))
} else {
log.Printf("kill -9 sent successfully to PID %d", pid)
}
exec.Command("pkill", "-9", "-P", strconv.Itoa(pid)).Run()
// Дополнительная очистка - убиваем все оставшиеся процессы
if ch.ChannelType == "video" || ch.ChannelType == "both" {
exec.Command("killall", "-9", "ffmpeg").Run()
} else {
log.Printf("No PID found, killing all arecord processes")
exec.Command("killall", "-9", "arecord").Run()
exec.Command("pkill", "-9", "arecord").Run()
}
exec.Command("pkill", "-9", "arecord").Run()
ch.mu.Lock()
if ch.timer != nil {
@@ -424,37 +870,29 @@ func (ch *Channel) StopRecording() {
ch.saveStateToDB()
// Проверяем существование и размер файла
fileExists := false
var fileSize int64
if currentPath != "" {
if _, err := os.Stat(currentPath); err == nil {
if info, err := os.Stat(currentPath); err == nil {
fileExists = true
fileSize = info.Size()
log.Printf("File exists: %s, size: %d bytes", currentPath, fileSize)
} else {
log.Printf("File not found: %s, error: %v", currentPath, err)
}
}
log.Printf("DEBUG: fileExists=%v, currentPath=%s, smbFolder=%s, onRecordingCompleted=%v",
fileExists, currentPath, smbFolder, onRecordingCompleted != nil)
log.Printf("DEBUG: fileExists=%v, fileSize=%d, currentPath=%s, smbFolder=%s, onRecordingCompleted=%v",
fileExists, fileSize, currentPath, smbFolder, onRecordingCompleted != nil)
if fileExists && currentPath != "" && smbFolder != "" && onRecordingCompleted != nil {
log.Printf("✅ Sending file to SMB: %s, folder: %s, path: %s", currentFile, smbFolder, currentPath)
db := database.GetDB()
if currentFile != "" {
recordings, _ := db.GetRecordingsByChannel(ch.ID, 1000)
for _, rec := range recordings {
if rec.Filename == currentFile {
if fileInfo, err := os.Stat(currentPath); err == nil {
logger.AddRecording(ch.ID, currentFile, ch.Duration, fileInfo.Size(), "ЗАВЕРШЕНА")
db.UpdateRecordingStats(rec.ID, fileInfo.Size(), ch.Duration)
}
db.UpdateRecordingStatus(rec.ID, "completed")
break
}
}
}
// Отправка на SMB если файл существует и не пустой
if fileExists && fileSize > 0 && smbFolder != "" && onRecordingCompleted != nil {
log.Printf("✅ Sending file to SMB: %s, size: %d bytes, folder: %s", currentFile, fileSize, smbFolder)
onRecordingCompleted(currentFile, currentPath, smbFolder)
} else {
log.Printf("❌ SMB отправка НЕ выполнена: fileExists=%v, currentPath=%s, smbFolder=%s, callback=%v",
fileExists, currentPath, smbFolder, onRecordingCompleted != nil)
log.Printf("❌ SMB отправка НЕ выполнена: fileExists=%v, fileSize=%d, smbFolder=%s, callback=%v",
fileExists, fileSize, smbFolder, onRecordingCompleted != nil)
}
log.Printf("=== RECORDING STOPPED for channel %s ===", ch.ID)
@@ -512,15 +950,22 @@ func (ch *Channel) UpdateConfig(config ChannelConfig) {
db := database.GetDB()
dbConfig := database.ChannelConfigDB{
ID: ch.ID,
Name: ch.Name,
Device: ch.Device,
Duration: ch.Duration,
SampleRate: ch.SampleRate,
Channels: ch.Channels,
Format: ch.Format,
AutoRestart: ch.AutoRestart,
SMBFolder: ch.SMBFolder,
ID: ch.ID,
Name: ch.Name,
Device: ch.Device,
Duration: ch.Duration,
SampleRate: ch.SampleRate,
Channels: ch.Channels,
Format: ch.Format,
AutoRestart: ch.AutoRestart,
SMBFolder: ch.SMBFolder,
ChannelType: ch.ChannelType,
VideoDevice: ch.VideoDevice,
VideoResolution: ch.VideoResolution,
VideoFramerate: ch.VideoFramerate,
VideoBitrate: ch.VideoBitrate,
VideoCodec: ch.VideoCodec,
VideoAudioOffset: ch.VideoAudioOffset,
}
db.SaveChannel(dbConfig)
}
@@ -622,7 +1067,6 @@ func (ch *Channel) StartSchedule() {
ch.ID, targetStart.Format("2006-01-02 15:04:05"), startDuration,
targetEnd.Format("2006-01-02 15:04:05"), endDuration)
// Для ARM добавляем небольшую задержку перед стартом
if optimization.IsARM() {
time.Sleep(500 * time.Millisecond)
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+304 -423
View File
File diff suppressed because it is too large Load Diff