feat: casecade plugin support multi server

This commit is contained in:
pggiroro
2026-03-19 11:12:18 +08:00
parent d9f3e737fb
commit f18bcbc232
15 changed files with 1764 additions and 154 deletions
+6
View File
@@ -1057,6 +1057,12 @@ func (s *Server) GetRecordList(ctx context.Context, req *pb.ReqRecordList) (resp
EndTime: timestamppb.New(recordFile.EndTime),
FilePath: recordFile.FilePath,
StreamPath: recordFile.StreamPath,
Filename: recordFile.Filename,
Type: recordFile.Type,
Duration: recordFile.Duration,
AudioCodec: recordFile.AudioCodec,
VideoCodec: recordFile.VideoCodec,
CreatedAt: timestamppb.New(recordFile.CreatedAt),
})
}
return
+2 -2
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.10
// protoc v6.31.1
// protoc-gen-go v1.36.6
// protoc v5.29.3
// source: auth.proto
package pb
+1 -9
View File
@@ -43,9 +43,6 @@ func request_Auth_Login_0(ctx context.Context, marshaler runtime.Marshaler, clie
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
if req.Body != nil {
_, _ = io.Copy(io.Discard, req.Body)
}
msg, err := client.Login(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
@@ -70,9 +67,6 @@ func request_Auth_Logout_0(ctx context.Context, marshaler runtime.Marshaler, cli
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
if req.Body != nil {
_, _ = io.Copy(io.Discard, req.Body)
}
msg, err := client.Logout(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
@@ -96,9 +90,7 @@ func request_Auth_GetUserInfo_0(ctx context.Context, marshaler runtime.Marshaler
protoReq UserInfoRequest
metadata runtime.ServerMetadata
)
if req.Body != nil {
_, _ = io.Copy(io.Discard, req.Body)
}
io.Copy(io.Discard, req.Body)
if err := req.ParseForm(); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
+1 -1
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.5.1
// - protoc v6.31.1
// - protoc v5.29.3
// source: auth.proto
package pb
+183 -123
View File
@@ -4449,6 +4449,12 @@ type RecordFile struct {
StreamPath string `protobuf:"bytes,3,opt,name=streamPath,proto3" json:"streamPath,omitempty"`
StartTime *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=startTime,proto3" json:"startTime,omitempty"`
EndTime *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=endTime,proto3" json:"endTime,omitempty"`
Filename string `protobuf:"bytes,6,opt,name=filename,proto3" json:"filename,omitempty"`
Type string `protobuf:"bytes,7,opt,name=type,proto3" json:"type,omitempty"`
Duration uint32 `protobuf:"varint,8,opt,name=duration,proto3" json:"duration,omitempty"`
AudioCodec string `protobuf:"bytes,9,opt,name=audioCodec,proto3" json:"audioCodec,omitempty"`
VideoCodec string `protobuf:"bytes,10,opt,name=videoCodec,proto3" json:"videoCodec,omitempty"`
CreatedAt *timestamppb.Timestamp `protobuf:"bytes,11,opt,name=createdAt,proto3" json:"createdAt,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -4518,6 +4524,48 @@ func (x *RecordFile) GetEndTime() *timestamppb.Timestamp {
return nil
}
func (x *RecordFile) GetFilename() string {
if x != nil {
return x.Filename
}
return ""
}
func (x *RecordFile) GetType() string {
if x != nil {
return x.Type
}
return ""
}
func (x *RecordFile) GetDuration() uint32 {
if x != nil {
return x.Duration
}
return 0
}
func (x *RecordFile) GetAudioCodec() string {
if x != nil {
return x.AudioCodec
}
return ""
}
func (x *RecordFile) GetVideoCodec() string {
if x != nil {
return x.VideoCodec
}
return ""
}
func (x *RecordFile) GetCreatedAt() *timestamppb.Timestamp {
if x != nil {
return x.CreatedAt
}
return nil
}
type EventRecordFile struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id uint32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
@@ -6305,7 +6353,7 @@ const file_global_proto_rawDesc = "" +
"\x04type\x18\a \x01(\tR\x04type\x12\x1e\n" +
"\n" +
"eventLevel\x18\b \x01(\tR\n" +
"eventLevel\"\xc8\x01\n" +
"eventLevel\"\x8e\x03\n" +
"\n" +
"RecordFile\x12\x0e\n" +
"\x02id\x18\x01 \x01(\rR\x02id\x12\x1a\n" +
@@ -6314,7 +6362,18 @@ const file_global_proto_rawDesc = "" +
"streamPath\x18\x03 \x01(\tR\n" +
"streamPath\x128\n" +
"\tstartTime\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\tstartTime\x124\n" +
"\aendTime\x18\x05 \x01(\v2\x1a.google.protobuf.TimestampR\aendTime\"\xc3\x02\n" +
"\aendTime\x18\x05 \x01(\v2\x1a.google.protobuf.TimestampR\aendTime\x12\x1a\n" +
"\bfilename\x18\x06 \x01(\tR\bfilename\x12\x12\n" +
"\x04type\x18\a \x01(\tR\x04type\x12\x1a\n" +
"\bduration\x18\b \x01(\rR\bduration\x12\x1e\n" +
"\n" +
"audioCodec\x18\t \x01(\tR\n" +
"audioCodec\x12\x1e\n" +
"\n" +
"videoCodec\x18\n" +
" \x01(\tR\n" +
"videoCodec\x128\n" +
"\tcreatedAt\x18\v \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\"\xc3\x02\n" +
"\x0fEventRecordFile\x12\x0e\n" +
"\x02id\x18\x01 \x01(\rR\x02id\x12\x1a\n" +
"\bfilePath\x18\x02 \x01(\tR\bfilePath\x12\x1e\n" +
@@ -6682,127 +6741,128 @@ var file_global_proto_depIdxs = []int32{
57, // 53: global.TransformListResponse.data:type_name -> global.Transform
83, // 54: global.RecordFile.startTime:type_name -> google.protobuf.Timestamp
83, // 55: global.RecordFile.endTime:type_name -> google.protobuf.Timestamp
83, // 56: global.EventRecordFile.startTime:type_name -> google.protobuf.Timestamp
83, // 57: global.EventRecordFile.endTime:type_name -> google.protobuf.Timestamp
60, // 58: global.RecordResponseList.data:type_name -> global.RecordFile
61, // 59: global.EventRecordResponseList.data:type_name -> global.EventRecordFile
83, // 60: global.Catalog.startTime:type_name -> google.protobuf.Timestamp
83, // 61: global.Catalog.endTime:type_name -> google.protobuf.Timestamp
64, // 62: global.ResponseCatalog.data:type_name -> global.Catalog
60, // 63: global.ResponseDelete.data:type_name -> global.RecordFile
83, // 64: global.AlarmInfo.createdAt:type_name -> google.protobuf.Timestamp
83, // 65: global.AlarmInfo.updatedAt:type_name -> google.protobuf.Timestamp
69, // 66: global.AlarmListResponse.data:type_name -> global.AlarmInfo
83, // 67: global.Step.startedAt:type_name -> google.protobuf.Timestamp
83, // 68: global.Step.completedAt:type_name -> google.protobuf.Timestamp
72, // 69: global.SubscriptionProgressData.steps:type_name -> global.Step
73, // 70: global.SubscriptionProgressResponse.data:type_name -> global.SubscriptionProgressData
84, // 71: global.GlobalPullRequest.delayCloseTimeout:type_name -> google.protobuf.Duration
84, // 72: global.GlobalPullRequest.publishTimeout:type_name -> google.protobuf.Duration
84, // 73: global.GlobalPullRequest.waitCloseTimeout:type_name -> google.protobuf.Duration
84, // 74: global.GlobalPullRequest.idleTimeout:type_name -> google.protobuf.Duration
84, // 75: global.GlobalPullRequest.pauseTimeout:type_name -> google.protobuf.Duration
84, // 76: global.GlobalPullRequest.bufferTime:type_name -> google.protobuf.Duration
2, // 77: global.Formily.PropertiesEntry.value:type_name -> global.Formily
85, // 78: global.Formily.ComponentPropsEntry.value:type_name -> google.protobuf.Any
2, // 79: global.FormilyResponse.PropertiesEntry.value:type_name -> global.Formily
86, // 80: global.api.SysInfo:input_type -> google.protobuf.Empty
86, // 81: global.api.DisabledPlugins:input_type -> google.protobuf.Empty
86, // 82: global.api.Summary:input_type -> google.protobuf.Empty
34, // 83: global.api.Shutdown:input_type -> global.RequestWithId
34, // 84: global.api.Restart:input_type -> global.RequestWithId
86, // 85: global.api.TaskTree:input_type -> google.protobuf.Empty
35, // 86: global.api.StopTask:input_type -> global.RequestWithId64
35, // 87: global.api.RestartTask:input_type -> global.RequestWithId64
17, // 88: global.api.StreamList:input_type -> global.StreamListRequest
86, // 89: global.api.WaitList:input_type -> google.protobuf.Empty
20, // 90: global.api.StreamInfo:input_type -> global.StreamSnapRequest
20, // 91: global.api.PauseStream:input_type -> global.StreamSnapRequest
20, // 92: global.api.ResumeStream:input_type -> global.StreamSnapRequest
50, // 93: global.api.SetStreamSpeed:input_type -> global.SetStreamSpeedRequest
51, // 94: global.api.SeekStream:input_type -> global.SeekStreamRequest
37, // 95: global.api.GetSubscribers:input_type -> global.SubscribersRequest
20, // 96: global.api.AudioTrackSnap:input_type -> global.StreamSnapRequest
20, // 97: global.api.VideoTrackSnap:input_type -> global.StreamSnapRequest
36, // 98: global.api.ChangeSubscribe:input_type -> global.ChangeSubscribeRequest
86, // 99: global.api.GetStreamAlias:input_type -> google.protobuf.Empty
47, // 100: global.api.SetStreamAlias:input_type -> global.SetStreamAliasRequest
20, // 101: global.api.StopPublish:input_type -> global.StreamSnapRequest
34, // 102: global.api.StopSubscribe:input_type -> global.RequestWithId
86, // 103: global.api.GetConfigFile:input_type -> google.protobuf.Empty
7, // 104: global.api.UpdateConfigFile:input_type -> global.UpdateConfigFileRequest
1, // 105: global.api.GetConfig:input_type -> global.GetConfigRequest
1, // 106: global.api.GetFormily:input_type -> global.GetConfigRequest
33, // 107: global.api.SetArming:input_type -> global.SetArmingRequest
8, // 108: global.api.ModifyConfig:input_type -> global.ModifyConfigRequest
86, // 109: global.api.GetPullProxyList:input_type -> google.protobuf.Empty
42, // 110: global.api.AddPullProxy:input_type -> global.PullProxyInfo
34, // 111: global.api.RemovePullProxy:input_type -> global.RequestWithId
43, // 112: global.api.UpdatePullProxy:input_type -> global.UpdatePullProxyRequest
86, // 113: global.api.GetPushProxyList:input_type -> google.protobuf.Empty
44, // 114: global.api.AddPushProxy:input_type -> global.PushProxyInfo
34, // 115: global.api.RemovePushProxy:input_type -> global.RequestWithId
45, // 116: global.api.UpdatePushProxy:input_type -> global.UpdatePushProxyRequest
86, // 117: global.api.GetRecording:input_type -> google.protobuf.Empty
86, // 118: global.api.GetTransformList:input_type -> google.protobuf.Empty
59, // 119: global.api.GetRecordList:input_type -> global.ReqRecordList
59, // 120: global.api.GetEventRecordList:input_type -> global.ReqRecordList
68, // 121: global.api.GetRecordCatalog:input_type -> global.ReqRecordCatalog
66, // 122: global.api.DeleteRecord:input_type -> global.ReqRecordDelete
70, // 123: global.api.GetAlarmList:input_type -> global.AlarmListRequest
20, // 124: global.api.GetSubscriptionProgress:input_type -> global.StreamSnapRequest
75, // 125: global.api.StartPull:input_type -> global.GlobalPullRequest
14, // 126: global.api.SysInfo:output_type -> global.SysInfoResponse
0, // 127: global.api.DisabledPlugins:output_type -> global.DisabledPluginsResponse
11, // 128: global.api.Summary:output_type -> global.SummaryResponse
32, // 129: global.api.Shutdown:output_type -> global.SuccessResponse
32, // 130: global.api.Restart:output_type -> global.SuccessResponse
16, // 131: global.api.TaskTree:output_type -> global.TaskTreeResponse
32, // 132: global.api.StopTask:output_type -> global.SuccessResponse
32, // 133: global.api.RestartTask:output_type -> global.SuccessResponse
18, // 134: global.api.StreamList:output_type -> global.StreamListResponse
19, // 135: global.api.WaitList:output_type -> global.StreamWaitListResponse
21, // 136: global.api.StreamInfo:output_type -> global.StreamInfoResponse
32, // 137: global.api.PauseStream:output_type -> global.SuccessResponse
32, // 138: global.api.ResumeStream:output_type -> global.SuccessResponse
32, // 139: global.api.SetStreamSpeed:output_type -> global.SuccessResponse
32, // 140: global.api.SeekStream:output_type -> global.SuccessResponse
40, // 141: global.api.GetSubscribers:output_type -> global.SubscribersResponse
30, // 142: global.api.AudioTrackSnap:output_type -> global.TrackSnapShotResponse
30, // 143: global.api.VideoTrackSnap:output_type -> global.TrackSnapShotResponse
32, // 144: global.api.ChangeSubscribe:output_type -> global.SuccessResponse
49, // 145: global.api.GetStreamAlias:output_type -> global.StreamAliasListResponse
32, // 146: global.api.SetStreamAlias:output_type -> global.SuccessResponse
32, // 147: global.api.StopPublish:output_type -> global.SuccessResponse
32, // 148: global.api.StopSubscribe:output_type -> global.SuccessResponse
5, // 149: global.api.GetConfigFile:output_type -> global.GetConfigFileResponse
32, // 150: global.api.UpdateConfigFile:output_type -> global.SuccessResponse
6, // 151: global.api.GetConfig:output_type -> global.GetConfigResponse
6, // 152: global.api.GetFormily:output_type -> global.GetConfigResponse
32, // 153: global.api.SetArming:output_type -> global.SuccessResponse
32, // 154: global.api.ModifyConfig:output_type -> global.SuccessResponse
41, // 155: global.api.GetPullProxyList:output_type -> global.PullProxyListResponse
32, // 156: global.api.AddPullProxy:output_type -> global.SuccessResponse
32, // 157: global.api.RemovePullProxy:output_type -> global.SuccessResponse
32, // 158: global.api.UpdatePullProxy:output_type -> global.SuccessResponse
46, // 159: global.api.GetPushProxyList:output_type -> global.PushProxyListResponse
32, // 160: global.api.AddPushProxy:output_type -> global.SuccessResponse
32, // 161: global.api.RemovePushProxy:output_type -> global.SuccessResponse
32, // 162: global.api.UpdatePushProxy:output_type -> global.SuccessResponse
53, // 163: global.api.GetRecording:output_type -> global.RecordingListResponse
58, // 164: global.api.GetTransformList:output_type -> global.TransformListResponse
62, // 165: global.api.GetRecordList:output_type -> global.RecordResponseList
63, // 166: global.api.GetEventRecordList:output_type -> global.EventRecordResponseList
65, // 167: global.api.GetRecordCatalog:output_type -> global.ResponseCatalog
67, // 168: global.api.DeleteRecord:output_type -> global.ResponseDelete
71, // 169: global.api.GetAlarmList:output_type -> global.AlarmListResponse
74, // 170: global.api.GetSubscriptionProgress:output_type -> global.SubscriptionProgressResponse
32, // 171: global.api.StartPull:output_type -> global.SuccessResponse
126, // [126:172] is the sub-list for method output_type
80, // [80:126] is the sub-list for method input_type
80, // [80:80] is the sub-list for extension type_name
80, // [80:80] is the sub-list for extension extendee
0, // [0:80] is the sub-list for field type_name
83, // 56: global.RecordFile.createdAt:type_name -> google.protobuf.Timestamp
83, // 57: global.EventRecordFile.startTime:type_name -> google.protobuf.Timestamp
83, // 58: global.EventRecordFile.endTime:type_name -> google.protobuf.Timestamp
60, // 59: global.RecordResponseList.data:type_name -> global.RecordFile
61, // 60: global.EventRecordResponseList.data:type_name -> global.EventRecordFile
83, // 61: global.Catalog.startTime:type_name -> google.protobuf.Timestamp
83, // 62: global.Catalog.endTime:type_name -> google.protobuf.Timestamp
64, // 63: global.ResponseCatalog.data:type_name -> global.Catalog
60, // 64: global.ResponseDelete.data:type_name -> global.RecordFile
83, // 65: global.AlarmInfo.createdAt:type_name -> google.protobuf.Timestamp
83, // 66: global.AlarmInfo.updatedAt:type_name -> google.protobuf.Timestamp
69, // 67: global.AlarmListResponse.data:type_name -> global.AlarmInfo
83, // 68: global.Step.startedAt:type_name -> google.protobuf.Timestamp
83, // 69: global.Step.completedAt:type_name -> google.protobuf.Timestamp
72, // 70: global.SubscriptionProgressData.steps:type_name -> global.Step
73, // 71: global.SubscriptionProgressResponse.data:type_name -> global.SubscriptionProgressData
84, // 72: global.GlobalPullRequest.delayCloseTimeout:type_name -> google.protobuf.Duration
84, // 73: global.GlobalPullRequest.publishTimeout:type_name -> google.protobuf.Duration
84, // 74: global.GlobalPullRequest.waitCloseTimeout:type_name -> google.protobuf.Duration
84, // 75: global.GlobalPullRequest.idleTimeout:type_name -> google.protobuf.Duration
84, // 76: global.GlobalPullRequest.pauseTimeout:type_name -> google.protobuf.Duration
84, // 77: global.GlobalPullRequest.bufferTime:type_name -> google.protobuf.Duration
2, // 78: global.Formily.PropertiesEntry.value:type_name -> global.Formily
85, // 79: global.Formily.ComponentPropsEntry.value:type_name -> google.protobuf.Any
2, // 80: global.FormilyResponse.PropertiesEntry.value:type_name -> global.Formily
86, // 81: global.api.SysInfo:input_type -> google.protobuf.Empty
86, // 82: global.api.DisabledPlugins:input_type -> google.protobuf.Empty
86, // 83: global.api.Summary:input_type -> google.protobuf.Empty
34, // 84: global.api.Shutdown:input_type -> global.RequestWithId
34, // 85: global.api.Restart:input_type -> global.RequestWithId
86, // 86: global.api.TaskTree:input_type -> google.protobuf.Empty
35, // 87: global.api.StopTask:input_type -> global.RequestWithId64
35, // 88: global.api.RestartTask:input_type -> global.RequestWithId64
17, // 89: global.api.StreamList:input_type -> global.StreamListRequest
86, // 90: global.api.WaitList:input_type -> google.protobuf.Empty
20, // 91: global.api.StreamInfo:input_type -> global.StreamSnapRequest
20, // 92: global.api.PauseStream:input_type -> global.StreamSnapRequest
20, // 93: global.api.ResumeStream:input_type -> global.StreamSnapRequest
50, // 94: global.api.SetStreamSpeed:input_type -> global.SetStreamSpeedRequest
51, // 95: global.api.SeekStream:input_type -> global.SeekStreamRequest
37, // 96: global.api.GetSubscribers:input_type -> global.SubscribersRequest
20, // 97: global.api.AudioTrackSnap:input_type -> global.StreamSnapRequest
20, // 98: global.api.VideoTrackSnap:input_type -> global.StreamSnapRequest
36, // 99: global.api.ChangeSubscribe:input_type -> global.ChangeSubscribeRequest
86, // 100: global.api.GetStreamAlias:input_type -> google.protobuf.Empty
47, // 101: global.api.SetStreamAlias:input_type -> global.SetStreamAliasRequest
20, // 102: global.api.StopPublish:input_type -> global.StreamSnapRequest
34, // 103: global.api.StopSubscribe:input_type -> global.RequestWithId
86, // 104: global.api.GetConfigFile:input_type -> google.protobuf.Empty
7, // 105: global.api.UpdateConfigFile:input_type -> global.UpdateConfigFileRequest
1, // 106: global.api.GetConfig:input_type -> global.GetConfigRequest
1, // 107: global.api.GetFormily:input_type -> global.GetConfigRequest
33, // 108: global.api.SetArming:input_type -> global.SetArmingRequest
8, // 109: global.api.ModifyConfig:input_type -> global.ModifyConfigRequest
86, // 110: global.api.GetPullProxyList:input_type -> google.protobuf.Empty
42, // 111: global.api.AddPullProxy:input_type -> global.PullProxyInfo
34, // 112: global.api.RemovePullProxy:input_type -> global.RequestWithId
43, // 113: global.api.UpdatePullProxy:input_type -> global.UpdatePullProxyRequest
86, // 114: global.api.GetPushProxyList:input_type -> google.protobuf.Empty
44, // 115: global.api.AddPushProxy:input_type -> global.PushProxyInfo
34, // 116: global.api.RemovePushProxy:input_type -> global.RequestWithId
45, // 117: global.api.UpdatePushProxy:input_type -> global.UpdatePushProxyRequest
86, // 118: global.api.GetRecording:input_type -> google.protobuf.Empty
86, // 119: global.api.GetTransformList:input_type -> google.protobuf.Empty
59, // 120: global.api.GetRecordList:input_type -> global.ReqRecordList
59, // 121: global.api.GetEventRecordList:input_type -> global.ReqRecordList
68, // 122: global.api.GetRecordCatalog:input_type -> global.ReqRecordCatalog
66, // 123: global.api.DeleteRecord:input_type -> global.ReqRecordDelete
70, // 124: global.api.GetAlarmList:input_type -> global.AlarmListRequest
20, // 125: global.api.GetSubscriptionProgress:input_type -> global.StreamSnapRequest
75, // 126: global.api.StartPull:input_type -> global.GlobalPullRequest
14, // 127: global.api.SysInfo:output_type -> global.SysInfoResponse
0, // 128: global.api.DisabledPlugins:output_type -> global.DisabledPluginsResponse
11, // 129: global.api.Summary:output_type -> global.SummaryResponse
32, // 130: global.api.Shutdown:output_type -> global.SuccessResponse
32, // 131: global.api.Restart:output_type -> global.SuccessResponse
16, // 132: global.api.TaskTree:output_type -> global.TaskTreeResponse
32, // 133: global.api.StopTask:output_type -> global.SuccessResponse
32, // 134: global.api.RestartTask:output_type -> global.SuccessResponse
18, // 135: global.api.StreamList:output_type -> global.StreamListResponse
19, // 136: global.api.WaitList:output_type -> global.StreamWaitListResponse
21, // 137: global.api.StreamInfo:output_type -> global.StreamInfoResponse
32, // 138: global.api.PauseStream:output_type -> global.SuccessResponse
32, // 139: global.api.ResumeStream:output_type -> global.SuccessResponse
32, // 140: global.api.SetStreamSpeed:output_type -> global.SuccessResponse
32, // 141: global.api.SeekStream:output_type -> global.SuccessResponse
40, // 142: global.api.GetSubscribers:output_type -> global.SubscribersResponse
30, // 143: global.api.AudioTrackSnap:output_type -> global.TrackSnapShotResponse
30, // 144: global.api.VideoTrackSnap:output_type -> global.TrackSnapShotResponse
32, // 145: global.api.ChangeSubscribe:output_type -> global.SuccessResponse
49, // 146: global.api.GetStreamAlias:output_type -> global.StreamAliasListResponse
32, // 147: global.api.SetStreamAlias:output_type -> global.SuccessResponse
32, // 148: global.api.StopPublish:output_type -> global.SuccessResponse
32, // 149: global.api.StopSubscribe:output_type -> global.SuccessResponse
5, // 150: global.api.GetConfigFile:output_type -> global.GetConfigFileResponse
32, // 151: global.api.UpdateConfigFile:output_type -> global.SuccessResponse
6, // 152: global.api.GetConfig:output_type -> global.GetConfigResponse
6, // 153: global.api.GetFormily:output_type -> global.GetConfigResponse
32, // 154: global.api.SetArming:output_type -> global.SuccessResponse
32, // 155: global.api.ModifyConfig:output_type -> global.SuccessResponse
41, // 156: global.api.GetPullProxyList:output_type -> global.PullProxyListResponse
32, // 157: global.api.AddPullProxy:output_type -> global.SuccessResponse
32, // 158: global.api.RemovePullProxy:output_type -> global.SuccessResponse
32, // 159: global.api.UpdatePullProxy:output_type -> global.SuccessResponse
46, // 160: global.api.GetPushProxyList:output_type -> global.PushProxyListResponse
32, // 161: global.api.AddPushProxy:output_type -> global.SuccessResponse
32, // 162: global.api.RemovePushProxy:output_type -> global.SuccessResponse
32, // 163: global.api.UpdatePushProxy:output_type -> global.SuccessResponse
53, // 164: global.api.GetRecording:output_type -> global.RecordingListResponse
58, // 165: global.api.GetTransformList:output_type -> global.TransformListResponse
62, // 166: global.api.GetRecordList:output_type -> global.RecordResponseList
63, // 167: global.api.GetEventRecordList:output_type -> global.EventRecordResponseList
65, // 168: global.api.GetRecordCatalog:output_type -> global.ResponseCatalog
67, // 169: global.api.DeleteRecord:output_type -> global.ResponseDelete
71, // 170: global.api.GetAlarmList:output_type -> global.AlarmListResponse
74, // 171: global.api.GetSubscriptionProgress:output_type -> global.SubscriptionProgressResponse
32, // 172: global.api.StartPull:output_type -> global.SuccessResponse
127, // [127:173] is the sub-list for method output_type
81, // [81:127] is the sub-list for method input_type
81, // [81:81] is the sub-list for extension type_name
81, // [81:81] is the sub-list for extension extendee
0, // [0:81] is the sub-list for field type_name
}
func init() { file_global_proto_init() }
+6
View File
@@ -746,6 +746,12 @@ message RecordFile {
string streamPath = 3;
google.protobuf.Timestamp startTime = 4;
google.protobuf.Timestamp endTime = 5;
string filename = 6;
string type = 7;
uint32 duration = 8;
string audioCodec = 9;
string videoCodec = 10;
google.protobuf.Timestamp createdAt = 11;
}
message EventRecordFile {
+1 -1
View File
@@ -50,7 +50,7 @@ func (task *ListenQuicWork) Start() (err error) {
return
}
task.OnStop(task.Listener.Close)
task.Info("listen quic on", task.ListenAddr)
task.Info("quic start", "listen quic on", task.ListenAddr)
return
}
+201 -14
View File
@@ -3,70 +3,156 @@ package plugin_cascade
import (
"crypto/tls"
"fmt"
"sort"
"time"
"m7s.live/v5"
"m7s.live/v5/pkg/config"
"m7s.live/v5/pkg/util"
cascade "m7s.live/v5/plugin/cascade/pkg"
task "github.com/langhuihui/gotask"
"github.com/quic-go/quic-go"
)
// {{ AURA-X: M1.1 - 多服务器配置 }}
// ServerConfig 单个服务器配置(主备模式)
type ServerConfig struct {
Name string `json:"name" desc:"服务器名称"`
Addr string `json:"addr" desc:"服务器地址(仅IP"`
QuicPort int `json:"quicPort" default:"44944" desc:"QUIC端口"`
HttpPort int `json:"httpPort" default:"8080" desc:"HTTP端口"`
Secret string `json:"secret" desc:"连接密钥"`
Priority int `json:"priority" desc:"优先级 (1=最高)"`
Enabled bool `json:"enabled" default:"true" desc:"是否启用"`
}
// {{ AURA-X: GetKey 实现 util.Collection 的接口 }}
func (sc *ServerConfig) GetKey() string {
return sc.GetQuicAddr()
}
// {{ AURA-X: GetQuicAddr 获取 QUIC 连接地址 }}
func (sc *ServerConfig) GetQuicAddr() string {
return fmt.Sprintf("%s:%d", sc.Addr, sc.QuicPort)
}
// {{ AURA-X: GetHttpAddr 获取 HTTP 地址 }}
func (sc *ServerConfig) GetHttpAddr() string {
return fmt.Sprintf("http://%s:%d", sc.Addr, sc.HttpPort)
}
// {{ AURA-X: M1.1 - 扩展插件配置,保留单服务器兼容 }}
type CascadeClientPlugin struct {
m7s.Plugin
RelayAPI cascade.RelayAPIConfig `desc:"访问控制"`
AutoPush bool `desc:"自动推流到上级"` //自动推流到上级
Server string `desc:"上级服务器"` // TODO: support multiple servers
Secret string `desc:"连接秘钥"`
client *CascadeClient
// {{ AURA-X: 原有单服务器配置(保持兼容) }}
Server string `desc:"上级服务器"` // 单服务器模式
Secret string `desc:"连接密钥"` // 单服务器模式密钥
// {{ AURA-X: 新增多服务器配置(主备模式)- 用于接收YAML配置 }}
Servers []*ServerConfig `desc:"服务器配置列表"` // 主备模式配置
// {{ AURA-X: M3.3 - 拉流代理同步配置 }}
PullSyncInterval int `desc:"拉流代理同步间隔(秒)" default:"30"` // 定期同步拉流代理列表
// {{ AURA-X: M4 - 录像同步配置 }}
RecordSyncInterval int `desc:"录像同步间隔(秒)" default:"60"` // 定期同步录像列表
// {{ AURA-X: 内部运行时集合 }}
servers util.Collection[string, *ServerConfig] // 服务器配置集合(运行时)
// {{ AURA-X: 内部组件 }}
client *CascadeClient // 单服务器模式使用
clients task.WorkCollection[string, *CascadeClient] // 多服务器模式使用: Addr -> CascadeClient
// {{ AURA-X: 客户端管理器(指针) }}
clientManager *ClientManager // ClientManager,用于多服务器的状态管理
// {{ AURA-X: 客户端停止事件通道,用于 CascadeClient.Dispose 通知 ClientManager }}
clientStoppedCh chan<- *ServerConfig
}
var _ = m7s.InstallPlugin[CascadeClientPlugin](m7s.PluginMeta{
NewPuller: cascade.NewCascadePuller,
})
// {{ AURA-X: M1.2 - 扩展客户端配置,添加服务器标识和密钥 }}
type CascadeClient struct {
task.Work
cfg *CascadeClientPlugin
cfg *CascadeClientPlugin
serverID uint32 // 服务器标识(1=主, 2=备)
serverAddr string // QUIC连接地址(如 localhost:44944
secret string // 连接密钥
*quic.Conn
}
// {{ AURA-X: M1.2 - 实现 GetKey 接口,供 util.Collection 使用 }}
func (c *CascadeClient) GetKey() string {
return c.serverAddr
}
// {{ AURA-X: Dispose 实现 task.Work 的 Dispose,在连接断开时被调用 }}
func (c *CascadeClient) Dispose() {
// {{ AURA-X: 向 ClientManager 发送服务器停止的消息 }}
if c.cfg.clientStoppedCh != nil {
// {{ AURA-X: 从 servers collection 获取对应的 ServerConfig,使用 serverAddr (QuicAddr) 作为 key }}
if serverCfg, ok := c.cfg.servers.Get(c.serverAddr); ok {
c.cfg.clientStoppedCh <- serverCfg
}
}
}
// {{ AURA-X: onClientStopped 处理客户端停止事件,由 CascadeClient.Dispose 调用 }}
func (c *CascadeClientPlugin) onClientStopped(serverCfg *ServerConfig) {
// {{ AURA-X: 预留接口,实际处理在 ClientManager 中 }}
}
func (task *CascadeClient) Start() (err error) {
tlsConf := &tls.Config{
InsecureSkipVerify: true,
NextProtos: []string{"monibuca"},
}
cfg := task.cfg
task.Conn, err = quic.DialAddr(cfg.Context, cfg.Server, tlsConf, &quic.Config{
// {{ AURA-X: 复用 QUIC KeepAlive (10秒) }}
task.Conn, err = quic.DialAddr(task.cfg.Context, task.serverAddr, tlsConf, &quic.Config{
KeepAlivePeriod: time.Second * 10,
EnableDatagrams: true,
})
if err != nil {
task.Error("quic.DialAddr", "connect error", err.Error(), "server", task.serverAddr)
return
}
var stream *quic.Stream
if stream, err = task.OpenStreamSync(task.cfg); err == nil {
res := []byte{0}
fmt.Fprintf(stream, "%s", task.cfg.Secret)
// {{ AURA-X: 获取对应服务器的 Secret }}
secret := task.getSecret()
fmt.Fprintf(stream, "%s", secret)
stream.Write([]byte{0})
_, err = stream.Read(res)
if err == nil && res[0] == 0 {
task.Info("connected to cascade server", "server", task.cfg.Server)
task.Info("connected to cascade server", "server", task.serverAddr, "id", task.serverID)
stream.Close()
} else {
var zapErr any = err
if err == nil {
zapErr = res[0]
}
task.Error("connect to cascade server", "server", task.cfg.Server, "err", zapErr)
task.Error("connect to cascade server", "server", task.serverAddr, "err", zapErr)
return err
}
}
return
}
func (task *CascadeClient) Run() (err error) {
// {{ AURA-X: M1.3 - 获取 Secret(简化版) }}
func (task *CascadeClient) getSecret() string {
return task.secret
}
func (task *CascadeClient) Go() (err error) {
for err == nil {
var s *quic.Stream
if s, err = task.AcceptStream(task.Task.Context); err == nil {
@@ -81,22 +167,123 @@ func (task *CascadeClient) Run() (err error) {
return
}
// {{ AURA-X: M1.3 - 双模式启动逻辑 }}
func (c *CascadeClientPlugin) Start() (err error) {
if c.Secret == "" && c.Server == "" {
return nil
c.AddTask(&c.clients)
// {{ AURA-X: 模式1 - 单服务器模式(原有逻辑) }}
if c.Server != "" && len(c.Servers) == 0 {
if c.Secret == "" {
c.Warn("secret is empty, skip connecting")
return nil
}
c.Info("running in single server mode", "server", c.Server)
return c.startSingleServer()
}
// {{ AURA-X: 模式2 - 主备模式(新增逻辑) }}
if len(c.Servers) > 0 {
c.Info("running in multi-server failover mode")
return c.startMultiServer()
}
// {{ AURA-X: 无配置,不启动 }}
c.Warn("no server configured, skip starting")
return nil
}
// {{ AURA-X: M1.3 - 单服务器模式(复用原有逻辑) }}
func (c *CascadeClientPlugin) startSingleServer() (err error) {
connectTask := CascadeClient{
cfg: c,
cfg: c,
serverAddr: c.Server,
secret: c.Secret,
}
connectTask.SetRetry(-1, time.Second)
connectTask.SetDescription("serverAddr", c.Server)
connectTask.SetDescription("secret", c.Secret)
c.AddTask(&connectTask)
c.client = &connectTask
return
}
// {{ AURA-X: M1.4 - 多服务器模式(新增逻辑) }}
// 注意:这里只做配置检查,不启动 client,启动由 ClientManager 负责
func (c *CascadeClientPlugin) startMultiServer() (err error) {
// {{ AURA-X: 检查 Secret 是否重复 }}
secretMap := make(map[string]int) // secret -> 出现次数
for i := range c.Servers {
if c.Servers[i].Secret != "" {
secretMap[c.Servers[i].Secret]++
}
}
for secret, count := range secretMap {
if count > 1 {
c.Error("duplicate secret detected, failover requires unique secrets", "secret", secret)
return fmt.Errorf("duplicate secret detected: %s (count: %d), please use unique secrets for each server", secret, count)
}
}
// {{ AURA-X: 按优先级排序 }}
sortedServers := make([]*ServerConfig, len(c.Servers))
copy(sortedServers, c.Servers)
sort.Slice(sortedServers, func(i, j int) bool {
return sortedServers[i].Priority < sortedServers[j].Priority
})
// {{ AURA-X: 输出主备服务器信息 }}
if len(sortedServers) > 0 {
primary := sortedServers[0]
c.Info("primary server", "name", primary.Name, "addr", primary.Addr)
if len(sortedServers) > 1 {
backup := sortedServers[1]
c.Info("backup server", "name", backup.Name, "addr", backup.Addr)
}
}
// {{ AURA-X: 转换为 util.Collection 供运行时使用 }}
c.servers = util.Collection[string, *ServerConfig]{}
for i := range sortedServers {
c.servers.Set(sortedServers[i])
}
// {{ AURA-X: 创建 ServerRunner 用于追踪服务器在线状态 }}
// {{ AURA-X: 使用插件自带的数据库连接 c.DB }}
var serverRunner *cascade.ServerRunner
if c.DB != nil {
serverRunner = cascade.NewServerRunner(c.DB)
}
// {{ AURA-X: 启动 ClientManager,由它负责连接管理 }}
c.clientManager = NewClientManager(c, serverRunner, c.DB)
c.AddTask(c.clientManager)
return nil
}
// {{ AURA-X: M1.5 - 支持多服务器拉流 }}
func (c *CascadeClientPlugin) Pull(streamPath string, conf config.Pull, pub *config.Publish) (job *m7s.PullJob, err error) {
var conn *quic.Conn
// {{ AURA-X: 多服务器模式:从当前运行的服务器获取连接 }}
if c.clients.Length() > 0 {
// {{ AURA-X: 从 ClientManager 获取当前运行的服务器地址 }}
runningAddr := c.clientManager.GetRunningServer()
if runningAddr != "" {
if client, ok := c.clients.Get(runningAddr); ok {
conn = client.Conn
}
}
} else if c.client != nil {
// {{ AURA-X: 单服务器模式:使用原有 client }}
conn = c.client.Conn
}
if conn == nil {
return nil, fmt.Errorf("no available server connection")
}
puller := &cascade.Puller{
Conn: c.client.Conn,
Conn: conn,
}
job = puller.GetPullJob()
job.Init(puller, &c.Plugin, streamPath, conf, pub)
File diff suppressed because it is too large Load Diff
+28
View File
@@ -35,3 +35,31 @@ func (p *Puller) Start() (err error) {
_, err = fmt.Fprintf(stream, "%s %s\r\n", "PULLFLV", p.PullJob.Publisher.StreamPath)
return
}
// Dispose 发送 STOPFLV 命令,然后关闭 Stream
func (p *Puller) Dispose() {
if p.ReadCloser != nil {
// 先发送停止命令
if w, ok := p.ReadCloser.(interface{ Write([]byte) (int, error) }); ok {
fmt.Fprintf(w, "STOPFLV %s\r\n", p.PullJob.Publisher.StreamPath)
fmt.Printf("[cascade] 发送 STOPFLV: %s\n", p.PullJob.Publisher.StreamPath)
}
// 关闭 Stream
p.ReadCloser.Close()
p.ReadCloser = nil
}
}
// Dispose 发送 STOPFLV 命令,然后关闭 Stream
func (p *Puller) Dispose() {
if p.ReadCloser != nil {
// 先发送停止命令
if w, ok := p.ReadCloser.(interface{ Write([]byte) (int, error) }); ok {
fmt.Fprintf(w, "STOPFLV %s\r\n", p.PullJob.Publisher.StreamPath)
fmt.Printf("[cascade] 发送 STOPFLV: %s\n", p.PullJob.Publisher.StreamPath)
}
// 关闭 Stream
p.ReadCloser.Close()
p.ReadCloser = nil
}
}
+21
View File
@@ -3,6 +3,8 @@ package cascade
import (
"bufio"
"context"
"errors"
"fmt"
"io"
"net"
"net/http"
@@ -67,6 +69,25 @@ func (task *ReceiveRequestTask) Go() (err error) {
return err
}
}
// 保存 subscriber 引用,用于手动停止
subscriber := live.Subscriber
// 启动 goroutine 监听停止命令
go func() {
stopReader := bufio.NewReader(task.Stream)
for {
line, _, err := stopReader.ReadLine()
if err != nil {
break
}
if strings.HasPrefix(string(line), "STOPFLV") {
fmt.Printf("[cascade] 收到 STOPFLV: %s\n", line)
// 直接调用 subscriber.Stop() 停止订阅者
subscriber.Stop(errors.New("stop by STOPFLV command"))
cancel()
break
}
}
}()
return live.Run()
}
req, err = http.NewRequestWithContext(ctx, reqLine[0], reqLine[1], reader)
+21
View File
@@ -0,0 +1,21 @@
package cascade
import (
"time"
"gorm.io/gorm"
)
// ServerRunRecord 服务器运行记录(数据库模型)
type ServerRunRecord struct {
gorm.Model
QuicAddr string // 服务器QUIC地址 (IP:Port)
Priority int // 优先级 (1=主, 2=备)
StartTime time.Time // 服务器开始运行时间(连接成功时间)
EndTime *time.Time // 服务器结束运行时间(断开时间),nil表示当前在线
}
// {{ AURA-X: 实现 GetKey 接口,供 util.Collection 使用 }}
func (r *ServerRunRecord) GetKey() string {
return r.QuicAddr
}
+157
View File
@@ -0,0 +1,157 @@
package cascade
import (
"time"
"m7s.live/v5/pkg/util"
"gorm.io/gorm"
)
// {{ AURA-X: M2 - 服务器运行状态追踪 }}
// ServerRunner 记录每台服务器的运行时间段(在线/离线),持久化到数据库
// ServerRunner 服务器运行状态追踪器
type ServerRunner struct {
records util.Collection[string, *ServerRunRecord] // quicAddr -> 运行记录(内存缓存)
db *gorm.DB // 数据库连接
}
// NewServerRunner 创建新的服务器运行追踪器
// {{ AURA-X: 将在 M3 的 FailoverController 中被调用 }}
func NewServerRunner(db *gorm.DB) *ServerRunner {
sr := &ServerRunner{
records: util.Collection[string, *ServerRunRecord]{},
db: db,
}
// {{ AURA-X: 自动迁移数据库表 }}
sr.db.AutoMigrate(&ServerRunRecord{})
// {{ AURA-X: 加载所有离线记录到内存(在线的只保留最新一条) }}
sr.loadFromDB()
return sr
}
// loadFromDB 从数据库加载记录到内存
// {{ AURA-X: 在线记录只保留最新的,离线记录全部加载 }}
func (sr *ServerRunner) loadFromDB() {
var allRecords []ServerRunRecord
sr.db.Order("start_time asc").Find(&allRecords)
// 按 quicAddr 分组,只保留每个服务器的最新记录
latestByServer := make(map[string]*ServerRunRecord)
for i := range allRecords {
record := &allRecords[i]
// 如果记录还未结束(在线),只保留最新的
if record.EndTime == nil {
if existing, ok := latestByServer[record.QuicAddr]; !ok || existing.StartTime.Before(record.StartTime) {
latestByServer[record.QuicAddr] = record
}
} else {
// 离线记录全部加载
sr.records.Set(record)
}
}
// 添加在线记录
for _, record := range latestByServer {
sr.records.Set(record)
}
}
// RecordStart 记录服务器开始运行(连接成功)
func (sr *ServerRunner) RecordStart(quicAddr string, priority int) {
now := time.Now()
// 如果已有记录且未结束,先结束它
if existing, ok := sr.records.Get(quicAddr); ok && existing.EndTime == nil {
existing.EndTime = &now
// {{ AURA-X: 更新数据库 }}
sr.db.Model(existing).Update("end_time", now)
}
// 创建新的运行记录
record := &ServerRunRecord{
QuicAddr: quicAddr,
Priority: priority,
StartTime: now,
EndTime: nil,
}
// {{ AURA-X: 保存到数据库 }}
sr.db.Create(record)
// {{ AURA-X: 添加到内存缓存 }}
sr.records.Set(record)
}
// RecordStop 记录服务器停止运行(断开连接)
func (sr *ServerRunner) RecordStop(quicAddr string) {
now := time.Now()
// 更新现有记录
if existing, ok := sr.records.Get(quicAddr); ok && existing.EndTime == nil {
existing.EndTime = &now
// {{ AURA-X: 更新数据库 }}
sr.db.Model(existing).Update("end_time", now)
// {{ AURA-X: 从内存缓存中移除(因为已离线) }}
sr.records.RemoveByKey(quicAddr)
}
}
// IsOnline 检查服务器是否在线
func (sr *ServerRunner) IsOnline(quicAddr string) bool {
if record, ok := sr.records.Get(quicAddr); ok {
return record.EndTime == nil
}
return false
}
// GetRecord 获取服务器的当前运行记录
func (sr *ServerRunner) GetRecord(quicAddr string) *ServerRunRecord {
if record, ok := sr.records.Get(quicAddr); ok {
return record
}
return nil
}
// GetAllRecords 获取所有历史运行记录
func (sr *ServerRunner) GetAllRecords() []ServerRunRecord {
var records []ServerRunRecord
sr.db.Order("start_time desc").Find(&records)
return records
}
// GetOnlineServers 获取当前所有在线服务器地址
func (sr *ServerRunner) GetOnlineServers() []string {
var online []string
sr.records.Range(func(record *ServerRunRecord) bool {
if record.EndTime == nil {
online = append(online, record.QuicAddr)
}
return true
})
return online
}
// QueryByTimeRange 查询指定时间范围内的运行记录
func (sr *ServerRunner) QueryByTimeRange(start, end time.Time) []ServerRunRecord {
var records []ServerRunRecord
sr.db.Where("start_time >= ? AND start_time <= ?", start, end).Order("start_time asc").Find(&records)
return records
}
// GetServerHistory 获取指定服务器的所有历史记录
func (sr *ServerRunner) GetServerHistory(quicAddr string) []ServerRunRecord {
var records []ServerRunRecord
sr.db.Where("quic_addr = ?", quicAddr).Order("start_time desc").Find(&records)
return records
}
// Clear 清除所有运行记录(仅用于测试)
func (sr *ServerRunner) Clear() {
sr.db.Where("1=1").Delete(&ServerRunRecord{})
sr.records = util.Collection[string, *ServerRunRecord]{}
}
+51 -2
View File
@@ -51,8 +51,57 @@ func (p *MP4Plugin) downloadSingleFile(stream *m7s.RecordStream, flag mp4.Flag,
var file storage.File
var err error
// 最高优先级:如果 FilePath 是绝对路径,直接使用,跳过所有 storage 处理
if filepath.IsAbs(stream.FilePath) {
// 最高优先级:如果 FilePath 是 HTTP/HTTPS URL,直接读取
if strings.HasPrefix(stream.FilePath, "http://") || strings.HasPrefix(stream.FilePath, "https://") {
if flag == 0 {
// 普通 MP4:重定向到 URL,让客户端直接从源获取
p.Info("redirecting to http URL", "url", stream.FilePath)
http.Redirect(w, r, stream.FilePath, http.StatusFound)
return
}
// fMP4:下载文件到临时文件后处理
resp, err := http.Get(stream.FilePath)
if err != nil {
http.Error(w, fmt.Sprintf("failed to download file: %v", err), http.StatusInternalServerError)
p.Error("failed to download file from URL", "err", err, "url", stream.FilePath)
return
}
defer resp.Body.Close()
// 创建临时文件
tmpFile, err := os.CreateTemp("", "mp4-*.tmp")
if err != nil {
http.Error(w, fmt.Sprintf("failed to create temp file: %v", err), http.StatusInternalServerError)
p.Error("failed to create temp file", "err", err)
return
}
tmpPath := tmpFile.Name()
// 复制内容到临时文件
_, err = io.Copy(tmpFile, resp.Body)
tmpFile.Close()
if err != nil {
os.Remove(tmpPath)
http.Error(w, fmt.Sprintf("failed to save downloaded file: %v", err), http.StatusInternalServerError)
p.Error("failed to save downloaded file", "err", err)
return
}
// 打开临时文件进行 fMP4 转换
file, err = os.Open(tmpPath)
if err != nil {
os.Remove(tmpPath)
http.Error(w, fmt.Sprintf("failed to open downloaded file: %v", err), http.StatusInternalServerError)
p.Error("failed to open downloaded file", "err", err)
return
}
defer func() {
file.Close()
os.Remove(tmpPath)
}()
p.Info("reading downloaded file for fmp4 conversion from URL", "url", stream.FilePath)
// 继续执行 fMP4 转换处理
} else if filepath.IsAbs(stream.FilePath) {
if flag == 0 {
// 普通 MP4:直接 ServeFile
http.ServeFile(w, r, stream.FilePath)
+47 -2
View File
@@ -2,14 +2,18 @@ package mp4
import (
"context"
"io"
"log/slog"
"m7s.live/v5/pkg/storage"
"net"
"net/http"
"os"
"path/filepath"
"reflect"
"strings"
"time"
"m7s.live/v5/pkg/storage"
"m7s.live/v5"
"m7s.live/v5/pkg"
"m7s.live/v5/pkg/codec"
@@ -41,7 +45,48 @@ func (d *DemuxerRange) Demux(ctx context.Context) error {
if stream.EndTime.Before(d.StartTime) || stream.StartTime.After(d.EndTime) {
continue
}
if filepath.IsAbs(stream.FilePath) {
// 如果是 HTTP/HTTPS URL,下载到临时文件
if strings.HasPrefix(stream.FilePath, "http://") || strings.HasPrefix(stream.FilePath, "https://") {
resp, err := http.Get(stream.FilePath)
if err != nil {
d.Error("failed to download file from URL", "err", err, "url", stream.FilePath)
continue
}
defer resp.Body.Close()
// 创建临时文件
tmpFile, err := os.CreateTemp("", "mp4-*.tmp")
if err != nil {
d.Error("failed to create temp file", "err", err)
continue
}
tmpPath := tmpFile.Name()
// 复制内容到临时文件
_, err = io.Copy(tmpFile, resp.Body)
tmpFile.Close()
if err != nil {
os.Remove(tmpPath)
d.Error("failed to save downloaded file", "err", err)
continue
}
// 打开临时文件
file, err = os.Open(tmpPath)
if err != nil {
os.Remove(tmpPath)
d.Error("failed to open downloaded file", "err", err)
continue
}
// 延迟关闭和删除临时文件
defer func() {
if file != nil {
file.Close()
}
os.Remove(tmpPath)
}()
d.Info("reading downloaded file from URL", "url", stream.FilePath)
} else if filepath.IsAbs(stream.FilePath) {
file, err = os.Open(stream.FilePath)
if err != nil {
continue