3588AdminBackend/internal/service/profile_editor.go

435 lines
13 KiB
Go

package service
import (
"encoding/json"
"fmt"
"strconv"
"strings"
)
type ConfigProfileEditor struct {
Name string `json:"name"`
Path string `json:"path"`
BusinessName string `json:"business_name"`
Description string `json:"description"`
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"`
}
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)
instancesRaw, _ := raw["instances"].([]any)
instances := make([]ConfigProfileInstanceEditor, 0, len(instancesRaw))
deviceCode := ""
siteName := ""
for _, item := range instancesRaw {
instanceMap, _ := item.(map[string]any)
paramsMap, _ := instanceMap["params"].(map[string]any)
sceneMeta, _ := instanceMap["scene_meta"].(map[string]any)
inputBindings := parseInputBindingEditors(instanceMap["input_bindings"])
serviceBindings := parseServiceBindingEditors(instanceMap["service_bindings"])
outputBindings := parseOutputBindingEditors(instanceMap["output_bindings"])
if deviceCode == "" {
deviceCode = firstString(sceneMeta["device_code"], stringValue(paramsMap["device_code"]))
}
if siteName == "" {
siteName = firstString(sceneMeta["site_name"], stringValue(paramsMap["site_name"]))
}
advanced := cloneMap(paramsMap)
for _, key := range []string{
"display_name",
"device_code",
"site_name",
"video_source_ref",
"rtsp_url",
"publish_hls_path",
"publish_rtsp_port",
"publish_rtsp_path",
"channel_no",
} {
delete(advanced, key)
}
if len(advanced) == 0 {
advanced = nil
}
instances = append(instances, ConfigProfileInstanceEditor{
Name: stringValue(instanceMap["name"]),
Template: stringValue(instanceMap["template"]),
VideoSourceRef: inputBindingRef(inputBindings, "video_input_main"),
DisplayName: stringValue(sceneMeta["display_name"]),
SiteName: stringValue(sceneMeta["site_name"]),
PublishHLSPath: outputBindingValue(outputBindings, "stream_output_main", "publish_hls_path"),
PublishRTSPPort: outputBindingValue(outputBindings, "stream_output_main", "publish_rtsp_port"),
PublishRTSPPath: outputBindingValue(outputBindings, "stream_output_main", "publish_rtsp_path"),
ChannelNo: outputBindingValue(outputBindings, "stream_output_main", "channel_no"),
InputBindings: inputBindings,
ServiceBindings: serviceBindings,
OutputBindings: outputBindings,
AdvancedParams: advanced,
})
}
return &ConfigProfileEditor{
Name: firstString(raw["name"], name),
Path: path,
BusinessName: stringValue(raw["business_name"]),
Description: stringValue(raw["description"]),
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)
}
if len(editor.Instances) == 0 {
return nil, fmt.Errorf("at least one instance is required")
}
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)
}
if len(instances) == 0 {
return nil, fmt.Errorf("at least one active instance is required")
}
doc := map[string]any{
"name": name,
"instances": instances,
}
setString(doc, "business_name", editor.BusinessName)
setString(doc, "description", editor.Description)
queue := map[string]any{}
if size := strings.TrimSpace(editor.Queue.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", editor.Queue.Strategy)
if len(queue) > 0 {
doc["queue"] = queue
}
return doc, nil
}
func (s *ConfigPreviewService) SaveProfileEditor(editor ConfigProfileEditor) error {
doc, err := s.BuildProfileDocument(editor)
if err != nil {
return err
}
body, err := marshalConfigJSON(doc)
if err != nil {
return err
}
if s != nil && s.assets != nil {
return s.assets.SaveProfile(
strings.TrimSpace(editor.Name),
firstProfileTemplate(editor.Instances),
strings.TrimSpace(editor.BusinessName),
strings.TrimSpace(editor.Description),
string(body),
)
}
return fmt.Errorf("asset repository is not configured")
}
func setString(m map[string]any, key string, value string) {
if strings.TrimSpace(value) != "" {
m[key] = strings.TrimSpace(value)
}
}
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 {
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(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 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
}