v 0.0.72 (работатет видео)
This commit is contained in:
Vendored
+15
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user