diff --git a/api_config.go b/api_config.go new file mode 100644 index 0000000..f918a48 --- /dev/null +++ b/api_config.go @@ -0,0 +1,324 @@ +package m7s + +import ( + "net/http" + "reflect" + "strconv" + "strings" + "time" + + "gopkg.in/yaml.v3" +) + +func getIndent(line string) int { + return len(line) - len(strings.TrimLeft(line, " ")) +} + +func addCommentsToYAML(yamlData []byte) []byte { + lines := strings.Split(string(yamlData), "\n") + var result strings.Builder + var commentBuffer []string + var keyLineBuffer string + var keyLineIndent int + inMultilineValue := false + + for _, line := range lines { + trimmedLine := strings.TrimSpace(line) + indent := getIndent(line) + + if strings.HasPrefix(trimmedLine, "_description:") { + description := strings.TrimSpace(strings.TrimPrefix(trimmedLine, "_description:")) + commentBuffer = append(commentBuffer, "# "+description) + } else if strings.HasPrefix(trimmedLine, "_enum:") { + enum := strings.TrimSpace(strings.TrimPrefix(trimmedLine, "_enum:")) + commentBuffer = append(commentBuffer, "# 可选值: "+enum) + } else if strings.HasPrefix(trimmedLine, "_value:") { + valueStr := strings.TrimSpace(strings.TrimPrefix(trimmedLine, "_value:")) + if valueStr != "" && valueStr != "{}" && valueStr != "[]" { + // Single line value + result.WriteString(strings.Repeat(" ", keyLineIndent)) + result.WriteString(keyLineBuffer) + result.WriteString(": ") + result.WriteString(valueStr) + if len(commentBuffer) > 0 { + result.WriteString(" ") + for j, c := range commentBuffer { + c = strings.TrimSpace(strings.TrimPrefix(c, "#")) + result.WriteString("# " + c) + if j < len(commentBuffer)-1 { + result.WriteString(" ") + } + } + } + result.WriteString("\n") + } else { + // Multi-line value (struct/map) + for _, comment := range commentBuffer { + result.WriteString(strings.Repeat(" ", keyLineIndent)) + result.WriteString(comment) + result.WriteString("\n") + } + result.WriteString(strings.Repeat(" ", keyLineIndent)) + result.WriteString(keyLineBuffer) + result.WriteString(":") + result.WriteString("\n") + inMultilineValue = true + } + commentBuffer = nil + keyLineBuffer = "" + keyLineIndent = 0 + } else if strings.Contains(trimmedLine, ":") { + // This is a key line + if keyLineBuffer != "" { // flush previous key line + result.WriteString(strings.Repeat(" ", keyLineIndent) + keyLineBuffer + ":\n") + } + inMultilineValue = false + keyLineBuffer = strings.TrimSuffix(trimmedLine, ":") + keyLineIndent = indent + } else if inMultilineValue { + // These are the lines of a multiline value + if trimmedLine != "" { + result.WriteString(line + "\n") + } + } + } + if keyLineBuffer != "" { + result.WriteString(strings.Repeat(" ", keyLineIndent) + keyLineBuffer + ":\n") + } + + // Final cleanup to remove empty lines and special keys + finalOutput := []string{} + for _, line := range strings.Split(result.String(), "\n") { + trimmed := strings.TrimSpace(line) + if trimmed == "" || strings.HasPrefix(trimmed, "_") { + continue + } + finalOutput = append(finalOutput, line) + } + + return []byte(strings.Join(finalOutput, "\n")) +} + +func (s *Server) api_Config_YAML_All(rw http.ResponseWriter, r *http.Request) { + query := r.URL.Query() + filterName := query.Get("name") + shouldMergeCommon := query.Get("common") != "false" + + configSections := []struct { + name string + data any + }{} + + // 1. Get common config if it needs to be merged. + var commonConfig map[string]any + if shouldMergeCommon { + if c, ok := extractStructConfig(reflect.ValueOf(s.Plugin.GetCommonConf())).(map[string]any); ok { + commonConfig = c + } + } + + // 2. Process global config. + if filterName == "" || filterName == "global" { + if globalConf, ok := extractStructConfig(reflect.ValueOf(s.ServerConfig)).(map[string]any); ok { + if shouldMergeCommon && commonConfig != nil { + mergedConf := make(map[string]any) + for k, v := range commonConfig { + mergedConf[k] = v + } + for k, v := range globalConf { + mergedConf[k] = v // Global overrides common + } + configSections = append(configSections, struct { + name string + data any + }{"global", mergedConf}) + } else { + configSections = append(configSections, struct { + name string + data any + }{"global", globalConf}) + } + } + } + + // 3. Process plugin configs. + for _, meta := range plugins { + if filterName != "" && meta.Name != filterName { + continue + } + + configType := meta.Type + if configType.Kind() == reflect.Ptr { + configType = configType.Elem() + } + + if pluginConf, ok := extractStructConfig(reflect.New(configType)).(map[string]any); ok { + pluginConf["enable"] = map[string]any{ + "_value": true, + "_description": "在global配置disableall时能启用特定插件", + } + if shouldMergeCommon && commonConfig != nil { + mergedConf := make(map[string]any) + for k, v := range commonConfig { + mergedConf[k] = v + } + for k, v := range pluginConf { + mergedConf[k] = v // Plugin overrides common + } + configSections = append(configSections, struct { + name string + data any + }{meta.Name, mergedConf}) + } else { + configSections = append(configSections, struct { + name string + data any + }{meta.Name, pluginConf}) + } + } + } + + // 4. Serialize each section and combine. + var yamlParts []string + for _, section := range configSections { + if section.data == nil { + continue + } + partMap := map[string]any{section.name: section.data} + partYAML, err := yaml.Marshal(partMap) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + yamlParts = append(yamlParts, string(partYAML)) + } + + finalYAML := strings.Join(yamlParts, "") + + rw.Header().Set("Content-Type", "text/yaml; charset=utf-8") + rw.Write(addCommentsToYAML([]byte(finalYAML))) +} + +func extractStructConfig(v reflect.Value) any { + if v.Kind() == reflect.Ptr { + if v.IsNil() { + return nil + } + v = v.Elem() + } + if v.Kind() != reflect.Struct { + return nil + } + m := make(map[string]any) + for i := 0; i < v.NumField(); i++ { + field := v.Type().Field(i) + if !field.IsExported() { + continue + } + // Filter out Plugin and UnimplementedApiServer + fieldType := field.Type + if fieldType.Kind() == reflect.Ptr { + fieldType = fieldType.Elem() + } + if fieldType.Name() == "Plugin" || fieldType.Name() == "UnimplementedApiServer" { + continue + } + yamlTag := field.Tag.Get("yaml") + if yamlTag == "-" { + continue + } + fieldName := strings.Split(yamlTag, ",")[0] + if fieldName == "" { + fieldName = strings.ToLower(field.Name) + } + m[fieldName] = extractFieldConfig(field, v.Field(i)) + } + return m +} + +func extractFieldConfig(field reflect.StructField, value reflect.Value) any { + result := make(map[string]any) + description := field.Tag.Get("desc") + enum := field.Tag.Get("enum") + if description != "" { + result["_description"] = description + } + if enum != "" { + result["_enum"] = enum + } + + kind := value.Kind() + if kind == reflect.Ptr { + if value.IsNil() { + value = reflect.New(value.Type().Elem()) + } + value = value.Elem() + kind = value.Kind() + } + + switch kind { + case reflect.Struct: + if dur, ok := value.Interface().(time.Duration); ok { + result["_value"] = extractDurationConfig(field, dur) + } else { + result["_value"] = extractStructConfig(value) + } + case reflect.Map, reflect.Slice: + if value.IsNil() { + result["_value"] = make(map[string]any) + if kind == reflect.Slice { + result["_value"] = make([]any, 0) + } + } else { + result["_value"] = value.Interface() + } + default: + result["_value"] = extractBasicTypeConfig(field, value) + } + + if description == "" && enum == "" { + return result["_value"] + } + + return result +} + +func extractBasicTypeConfig(field reflect.StructField, value reflect.Value) any { + if value.IsZero() { + if defaultValue := field.Tag.Get("default"); defaultValue != "" { + return parseDefaultValue(defaultValue, field.Type) + } + } + return value.Interface() +} + +func extractDurationConfig(field reflect.StructField, value time.Duration) any { + if value == 0 { + if defaultValue := field.Tag.Get("default"); defaultValue != "" { + return defaultValue + } + } + return value.String() +} + +func parseDefaultValue(defaultValue string, t reflect.Type) any { + switch t.Kind() { + case reflect.String: + return defaultValue + case reflect.Bool: + return defaultValue == "true" + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + if v, err := strconv.ParseInt(defaultValue, 10, 64); err == nil { + return v + } + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + if v, err := strconv.ParseUint(defaultValue, 10, 64); err == nil { + return v + } + case reflect.Float32, reflect.Float64: + if v, err := strconv.ParseFloat(defaultValue, 64); err == nil { + return v + } + } + return defaultValue +} diff --git a/pkg/config/types.go b/pkg/config/types.go index 368e5a3..8c0f89f 100755 --- a/pkg/config/types.go +++ b/pkg/config/types.go @@ -70,10 +70,10 @@ type ( SyncMode int `default:"1" desc:"同步模式" enum:"0:采用时间戳同步,1:采用写入时间同步"` // 0,采用时间戳同步,1,采用写入时间同步 IFrameOnly bool `desc:"只要关键帧"` // 只要关键帧 WaitTimeout time.Duration `default:"10s" desc:"等待流超时时间"` // 等待流超时 - WaitTrack string `default:"" desc:"等待轨道" enum:"audio:等待音频,video:等待视频,all:等待全部"` - WriteBufferSize int `desc:"写缓冲大小"` // 写缓冲大小 - Key string `desc:"订阅鉴权key"` // 订阅鉴权key - SubType string `desc:"订阅类型"` // 订阅类型 + WaitTrack string `default:"" desc:"等待轨道" enum:"audio:等待音频,video:等待视频,all:等待全部"` + WriteBufferSize int `desc:"写缓冲大小"` // 写缓冲大小 + Key string `desc:"订阅鉴权key"` // 订阅鉴权key + SubType string `desc:"订阅类型"` // 订阅类型 } HTTPValues map[string][]string Pull struct { diff --git a/server.go b/server.go index f59f48e..ca89fb9 100644 --- a/server.go +++ b/server.go @@ -277,6 +277,7 @@ func (s *Server) Start() (err error) { s.registerHandler(map[string]http.HandlerFunc{ "/api/config/json/{name}": s.api_Config_JSON_, + "/api/config/yaml/all": s.api_Config_YAML_All, "/api/stream/annexb/{streamPath...}": s.api_Stream_AnnexB_, "/api/videotrack/sse/{streamPath...}": s.api_VideoTrack_SSE, "/api/audiotrack/sse/{streamPath...}": s.api_AudioTrack_SSE,