472 lines
14 KiB
Go
472 lines
14 KiB
Go
package service
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
type ConfigProfileEditor struct {
|
|
Name string `json:"name"`
|
|
Path string `json:"path"`
|
|
PrimaryTemplateName string `json:"primary_template_name"`
|
|
BusinessName string `json:"business_name"`
|
|
Description string `json:"description"`
|
|
OverlayName string `json:"overlay_name"`
|
|
DeviceCode string `json:"device_code"`
|
|
SiteName string `json:"site_name"`
|
|
Queue ConfigProfileQueueEditor `json:"queue"`
|
|
Instances []ConfigProfileInstanceEditor `json:"instances"`
|
|
Raw map[string]any `json:"raw"`
|
|
}
|
|
|
|
type ConfigProfileQueueEditor struct {
|
|
Size string `json:"size"`
|
|
Strategy string `json:"strategy"`
|
|
}
|
|
|
|
func DefaultConfigProfileQueue() ConfigProfileQueueEditor {
|
|
return ConfigProfileQueueEditor{
|
|
Size: "8",
|
|
Strategy: "drop_oldest",
|
|
}
|
|
}
|
|
|
|
type InputBindingEditor struct {
|
|
VideoSourceRef string `json:"video_source_ref"`
|
|
}
|
|
|
|
type ServiceBindingEditor struct {
|
|
ServiceRef string `json:"service_ref"`
|
|
}
|
|
|
|
type OutputBindingEditor struct {
|
|
PublishHLSPath string `json:"publish_hls_path"`
|
|
PublishRTSPPort string `json:"publish_rtsp_port"`
|
|
PublishRTSPPath string `json:"publish_rtsp_path"`
|
|
ChannelNo string `json:"channel_no"`
|
|
}
|
|
|
|
type ConfigProfileInstanceEditor struct {
|
|
Name string `json:"name"`
|
|
Template string `json:"template"`
|
|
VideoSourceRef string `json:"video_source_ref"`
|
|
DisplayName string `json:"display_name"`
|
|
SiteName string `json:"site_name"`
|
|
PublishHLSPath string `json:"publish_hls_path"`
|
|
PublishRTSPPort string `json:"publish_rtsp_port"`
|
|
PublishRTSPPath string `json:"publish_rtsp_path"`
|
|
ChannelNo string `json:"channel_no"`
|
|
InputBindings map[string]InputBindingEditor `json:"input_bindings,omitempty"`
|
|
ServiceBindings map[string]ServiceBindingEditor `json:"service_bindings,omitempty"`
|
|
OutputBindings map[string]OutputBindingEditor `json:"output_bindings,omitempty"`
|
|
AdvancedParams map[string]any `json:"advanced_params"`
|
|
Delete bool `json:"delete"`
|
|
}
|
|
|
|
func (s *ConfigPreviewService) GetProfileEditor(name string) (*ConfigProfileEditor, error) {
|
|
raw, path, err := s.readAssetJSON("profiles", name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
queueMap, _ := raw["queue"].(map[string]any)
|
|
templateName := stringValue(raw["primary_template_name"])
|
|
instances, siteName, deviceCode, err := s.loadRecognitionUnitEditors(name, templateName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &ConfigProfileEditor{
|
|
Name: firstString(raw["name"], name),
|
|
Path: path,
|
|
PrimaryTemplateName: stringValue(raw["primary_template_name"]),
|
|
BusinessName: stringValue(raw["business_name"]),
|
|
Description: stringValue(raw["description"]),
|
|
OverlayName: firstOverlayName(raw),
|
|
DeviceCode: deviceCode,
|
|
SiteName: siteName,
|
|
Queue: ConfigProfileQueueEditor{
|
|
Size: valueString(queueMap["size"]),
|
|
Strategy: stringValue(queueMap["strategy"]),
|
|
},
|
|
Instances: instances,
|
|
Raw: raw,
|
|
}, nil
|
|
}
|
|
|
|
func (s *ConfigPreviewService) BuildProfileDocument(editor ConfigProfileEditor) (map[string]any, error) {
|
|
name := strings.TrimSpace(editor.Name)
|
|
if name == "" {
|
|
return nil, fmt.Errorf("profile name is required")
|
|
}
|
|
if err := validateConfigName(name); err != nil {
|
|
return nil, fmt.Errorf("invalid profile name: %w", err)
|
|
}
|
|
|
|
seen := map[string]struct{}{}
|
|
instances := make([]map[string]any, 0, len(editor.Instances))
|
|
for _, inst := range editor.Instances {
|
|
if inst.Delete {
|
|
continue
|
|
}
|
|
instanceName := strings.TrimSpace(inst.Name)
|
|
if instanceName == "" {
|
|
return nil, fmt.Errorf("instance name is required")
|
|
}
|
|
if err := validateConfigName(instanceName); err != nil {
|
|
return nil, fmt.Errorf("invalid instance name %q: %w", instanceName, err)
|
|
}
|
|
if _, ok := seen[instanceName]; ok {
|
|
return nil, fmt.Errorf("duplicate instance name: %s", instanceName)
|
|
}
|
|
seen[instanceName] = struct{}{}
|
|
|
|
videoSourceRef := strings.TrimSpace(inputBindingRef(inst.InputBindings, "video_input_main"))
|
|
if videoSourceRef == "" {
|
|
videoSourceRef = strings.TrimSpace(inst.VideoSourceRef)
|
|
}
|
|
if videoSourceRef == "" {
|
|
return nil, fmt.Errorf("video source is required for %s", instanceName)
|
|
}
|
|
|
|
params := map[string]any{}
|
|
for key, value := range cloneMap(inst.AdvancedParams) {
|
|
params[key] = value
|
|
}
|
|
|
|
instance := map[string]any{
|
|
"name": instanceName,
|
|
}
|
|
if len(params) > 0 {
|
|
instance["params"] = params
|
|
}
|
|
setString(instance, "template", inst.Template)
|
|
sceneMeta := map[string]any{}
|
|
setString(sceneMeta, "display_name", inst.DisplayName)
|
|
setString(sceneMeta, "site_name", firstString(inst.SiteName, editor.SiteName))
|
|
setString(sceneMeta, "device_code", editor.DeviceCode)
|
|
if len(sceneMeta) > 0 {
|
|
instance["scene_meta"] = sceneMeta
|
|
}
|
|
inputBindings := buildInputBindingDocument(inst)
|
|
if len(inputBindings) > 0 {
|
|
instance["input_bindings"] = inputBindings
|
|
}
|
|
serviceBindings := buildServiceBindingDocument(inst)
|
|
if len(serviceBindings) > 0 {
|
|
instance["service_bindings"] = serviceBindings
|
|
}
|
|
outputBindings, err := buildOutputBindingDocument(inst)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(outputBindings) > 0 {
|
|
instance["output_bindings"] = outputBindings
|
|
}
|
|
instances = append(instances, instance)
|
|
}
|
|
doc := map[string]any{
|
|
"name": name,
|
|
"instances": instances,
|
|
}
|
|
setString(doc, "business_name", editor.BusinessName)
|
|
setString(doc, "description", editor.Description)
|
|
if overlayName := strings.TrimSpace(editor.OverlayName); overlayName != "" {
|
|
doc["overlays"] = []any{overlayName}
|
|
}
|
|
normalizedQueue := editor.Queue
|
|
if strings.TrimSpace(normalizedQueue.Size) == "" || strings.TrimSpace(normalizedQueue.Strategy) == "" {
|
|
normalizedQueue = DefaultConfigProfileQueue()
|
|
}
|
|
queue := map[string]any{}
|
|
if size := strings.TrimSpace(normalizedQueue.Size); size != "" {
|
|
value, err := strconv.Atoi(size)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("queue size must be a number")
|
|
}
|
|
queue["size"] = value
|
|
}
|
|
setString(queue, "strategy", normalizedQueue.Strategy)
|
|
if len(queue) > 0 {
|
|
doc["queue"] = queue
|
|
}
|
|
|
|
return doc, nil
|
|
}
|
|
|
|
func (s *ConfigPreviewService) SaveProfileEditor(editor ConfigProfileEditor) error {
|
|
doc, err := s.buildSceneTemplateDocument(editor)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
body, err := marshalConfigJSON(doc)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if s != nil && s.assets != nil {
|
|
templateName := strings.TrimSpace(editor.PrimaryTemplateName)
|
|
if templateName == "" {
|
|
templateName = firstProfileTemplate(editor.Instances)
|
|
}
|
|
return s.assets.SaveProfile(
|
|
strings.TrimSpace(editor.Name),
|
|
templateName,
|
|
strings.TrimSpace(editor.BusinessName),
|
|
strings.TrimSpace(editor.Description),
|
|
string(body),
|
|
)
|
|
}
|
|
return fmt.Errorf("asset repository is not configured")
|
|
}
|
|
|
|
func (s *ConfigPreviewService) buildSceneTemplateDocument(editor ConfigProfileEditor) (map[string]any, error) {
|
|
name := strings.TrimSpace(editor.Name)
|
|
if name == "" {
|
|
return nil, fmt.Errorf("scene template name is required")
|
|
}
|
|
if err := validateConfigName(name); err != nil {
|
|
return nil, fmt.Errorf("invalid scene template name: %w", err)
|
|
}
|
|
doc := map[string]any{
|
|
"name": name,
|
|
}
|
|
setString(doc, "primary_template_name", editor.PrimaryTemplateName)
|
|
setString(doc, "business_name", editor.BusinessName)
|
|
setString(doc, "description", editor.Description)
|
|
setString(doc, "site_name", editor.SiteName)
|
|
if overlayName := strings.TrimSpace(editor.OverlayName); overlayName != "" {
|
|
doc["overlays"] = []any{overlayName}
|
|
}
|
|
normalizedQueue := editor.Queue
|
|
if strings.TrimSpace(normalizedQueue.Size) == "" || strings.TrimSpace(normalizedQueue.Strategy) == "" {
|
|
normalizedQueue = DefaultConfigProfileQueue()
|
|
}
|
|
queue := map[string]any{}
|
|
if size := strings.TrimSpace(normalizedQueue.Size); size != "" {
|
|
value, err := strconv.Atoi(size)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("queue size must be a number")
|
|
}
|
|
queue["size"] = value
|
|
}
|
|
setString(queue, "strategy", normalizedQueue.Strategy)
|
|
if len(queue) > 0 {
|
|
doc["queue"] = queue
|
|
}
|
|
return doc, nil
|
|
}
|
|
|
|
func setString(m map[string]any, key string, value string) {
|
|
if strings.TrimSpace(value) != "" {
|
|
m[key] = strings.TrimSpace(value)
|
|
}
|
|
}
|
|
|
|
func firstOverlayName(raw map[string]any) string {
|
|
items, _ := raw["overlays"].([]any)
|
|
for _, item := range items {
|
|
if v := stringValue(item); strings.TrimSpace(v) != "" {
|
|
return strings.TrimSpace(v)
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func marshalConfigJSON(doc map[string]any) ([]byte, error) {
|
|
body, err := json.MarshalIndent(doc, "", " ")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return append(body, '\n'), nil
|
|
}
|
|
|
|
func firstProfileTemplate(instances []ConfigProfileInstanceEditor) string {
|
|
for _, inst := range instances {
|
|
if inst.Delete {
|
|
continue
|
|
}
|
|
if v := strings.TrimSpace(inst.Template); v != "" {
|
|
return v
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func parseInputBindingEditors(raw any) map[string]InputBindingEditor {
|
|
items, _ := raw.(map[string]any)
|
|
if len(items) == 0 {
|
|
return nil
|
|
}
|
|
out := map[string]InputBindingEditor{}
|
|
for key, value := range items {
|
|
entry, _ := value.(map[string]any)
|
|
out[key] = InputBindingEditor{
|
|
VideoSourceRef: stringValue(entry["video_source_ref"]),
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func parseServiceBindingEditors(raw any) map[string]ServiceBindingEditor {
|
|
items, _ := raw.(map[string]any)
|
|
if len(items) == 0 {
|
|
return nil
|
|
}
|
|
out := map[string]ServiceBindingEditor{}
|
|
for key, value := range items {
|
|
entry, _ := value.(map[string]any)
|
|
out[key] = ServiceBindingEditor{
|
|
ServiceRef: stringValue(entry["service_ref"]),
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func parseOutputBindingEditors(raw any) map[string]OutputBindingEditor {
|
|
items, _ := raw.(map[string]any)
|
|
if len(items) == 0 {
|
|
return nil
|
|
}
|
|
out := map[string]OutputBindingEditor{}
|
|
for key, value := range items {
|
|
entry, _ := value.(map[string]any)
|
|
out[key] = OutputBindingEditor{
|
|
PublishHLSPath: stringValue(entry["publish_hls_path"]),
|
|
PublishRTSPPort: valueString(entry["publish_rtsp_port"]),
|
|
PublishRTSPPath: stringValue(entry["publish_rtsp_path"]),
|
|
ChannelNo: stringValue(entry["channel_no"]),
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func inputBindingRef(bindings map[string]InputBindingEditor, slot string) string {
|
|
if len(bindings) == 0 {
|
|
return ""
|
|
}
|
|
return strings.TrimSpace(bindings[slot].VideoSourceRef)
|
|
}
|
|
|
|
func outputBindingValue(bindings map[string]OutputBindingEditor, slot string, field string) string {
|
|
if len(bindings) == 0 {
|
|
return ""
|
|
}
|
|
item, ok := bindings[slot]
|
|
if !ok {
|
|
return ""
|
|
}
|
|
switch field {
|
|
case "publish_hls_path":
|
|
return strings.TrimSpace(item.PublishHLSPath)
|
|
case "publish_rtsp_port":
|
|
return strings.TrimSpace(item.PublishRTSPPort)
|
|
case "publish_rtsp_path":
|
|
return strings.TrimSpace(item.PublishRTSPPath)
|
|
case "channel_no":
|
|
return strings.TrimSpace(item.ChannelNo)
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func buildInputBindingDocument(inst ConfigProfileInstanceEditor) map[string]any {
|
|
out := map[string]any{}
|
|
for key, value := range inst.InputBindings {
|
|
entry := map[string]any{}
|
|
setString(entry, "video_source_ref", value.VideoSourceRef)
|
|
if len(entry) > 0 {
|
|
out[key] = entry
|
|
}
|
|
}
|
|
if strings.TrimSpace(inst.VideoSourceRef) != "" {
|
|
if _, ok := out["video_input_main"]; !ok {
|
|
out["video_input_main"] = map[string]any{"video_source_ref": strings.TrimSpace(inst.VideoSourceRef)}
|
|
}
|
|
}
|
|
if len(out) == 0 {
|
|
return nil
|
|
}
|
|
return out
|
|
}
|
|
|
|
func buildServiceBindingDocument(inst ConfigProfileInstanceEditor) map[string]any {
|
|
out := map[string]any{}
|
|
for key, value := range inst.ServiceBindings {
|
|
entry := map[string]any{}
|
|
setString(entry, "service_ref", value.ServiceRef)
|
|
if len(entry) > 0 {
|
|
out[key] = entry
|
|
}
|
|
}
|
|
if len(out) == 0 {
|
|
return nil
|
|
}
|
|
return out
|
|
}
|
|
|
|
func buildOutputBindingDocument(inst ConfigProfileInstanceEditor) (map[string]any, error) {
|
|
out := map[string]any{}
|
|
for key, value := range inst.OutputBindings {
|
|
if key == "stream_output_main" {
|
|
value = applyDefaultStreamOutputBinding(inst.Name, value)
|
|
}
|
|
entry, err := outputBindingEntry(value)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(entry) > 0 {
|
|
out[key] = entry
|
|
}
|
|
}
|
|
if _, ok := out["stream_output_main"]; !ok {
|
|
entry, err := outputBindingEntry(applyDefaultStreamOutputBinding(inst.Name, OutputBindingEditor{
|
|
PublishHLSPath: inst.PublishHLSPath,
|
|
PublishRTSPPort: inst.PublishRTSPPort,
|
|
PublishRTSPPath: inst.PublishRTSPPath,
|
|
ChannelNo: inst.ChannelNo,
|
|
}))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(entry) > 0 {
|
|
out["stream_output_main"] = entry
|
|
}
|
|
}
|
|
if len(out) == 0 {
|
|
return nil, nil
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func applyDefaultStreamOutputBinding(instanceName string, value OutputBindingEditor) OutputBindingEditor {
|
|
name := strings.TrimSpace(instanceName)
|
|
if name == "" {
|
|
return value
|
|
}
|
|
if strings.TrimSpace(value.PublishHLSPath) == "" {
|
|
value.PublishHLSPath = "./web/hls/" + name + "/index.m3u8"
|
|
}
|
|
if strings.TrimSpace(value.PublishRTSPPath) == "" {
|
|
value.PublishRTSPPath = "/live/" + name
|
|
}
|
|
if strings.TrimSpace(value.ChannelNo) == "" {
|
|
value.ChannelNo = name
|
|
}
|
|
return value
|
|
}
|
|
|
|
func outputBindingEntry(value OutputBindingEditor) (map[string]any, error) {
|
|
entry := map[string]any{}
|
|
setString(entry, "publish_hls_path", value.PublishHLSPath)
|
|
setString(entry, "publish_rtsp_path", value.PublishRTSPPath)
|
|
setString(entry, "channel_no", value.ChannelNo)
|
|
if port := strings.TrimSpace(value.PublishRTSPPort); port != "" {
|
|
parsed, err := strconv.Atoi(port)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("publish rtsp port must be a number")
|
|
}
|
|
entry["publish_rtsp_port"] = parsed
|
|
}
|
|
return entry, nil
|
|
}
|