diff --git a/server/cmd/api/api/api.go b/server/cmd/api/api/api.go index b9b0a346..1ee18820 100644 --- a/server/cmd/api/api/api.go +++ b/server/cmd/api/api/api.go @@ -234,6 +234,42 @@ func (s *ApiService) StopRecording(ctx context.Context, req oapi.StopRecordingRe return oapi.StopRecording200Response{}, nil } +func (s *ApiService) MarkRecording(ctx context.Context, req oapi.MarkRecordingRequestObject) (oapi.MarkRecordingResponseObject, error) { + log := logger.FromContext(ctx) + + if req.Body == nil { + return oapi.MarkRecording400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "request body is required"}}, nil + } + + recorderID := s.defaultRecorderID + if req.Body.Id != nil && *req.Body.Id != "" { + recorderID = *req.Body.Id + } + + rec, exists := s.recordManager.GetRecorder(recorderID) + if !exists { + log.Error("attempted to mark non-existent recording", "recorder_id", recorderID) + return oapi.MarkRecording409JSONResponse{ConflictErrorJSONResponse: oapi.ConflictErrorJSONResponse{Message: "no active recording to mark"}}, nil + } + + name, offsetMs, err := rec.Mark(req.Body.Name) + if err != nil { + switch { + case errors.Is(err, recorder.ErrNotRecording): + log.Error("attempted to mark recording that is not in progress", "recorder_id", recorderID) + return oapi.MarkRecording409JSONResponse{ConflictErrorJSONResponse: oapi.ConflictErrorJSONResponse{Message: "no active recording to mark"}}, nil + case errors.Is(err, recorder.ErrInvalidMarkerName): + log.Warn("invalid marker name", "recorder_id", recorderID) + return oapi.MarkRecording400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "marker name must be non-empty and at most 200 characters"}}, nil + default: + log.Error("failed to mark recording", "err", err, "recorder_id", recorderID) + return oapi.MarkRecording500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to mark recording"}}, nil + } + } + + return oapi.MarkRecording201JSONResponse{Name: name, OffsetMs: offsetMs}, nil +} + const ( minRecordingSizeInBytes = 100 ) diff --git a/server/cmd/api/api/api_test.go b/server/cmd/api/api/api_test.go index 4f2c2165..7945450b 100644 --- a/server/cmd/api/api/api_test.go +++ b/server/cmd/api/api/api_test.go @@ -126,6 +126,71 @@ func TestApiService_StopRecording(t *testing.T) { }) } +func TestApiService_MarkRecording(t *testing.T) { + ctx := context.Background() + name := "checkpoint" + + t.Run("no recorder maps to 409", func(t *testing.T) { + mgr := recorder.NewFFmpegManager() + svc, err := newSvc(t, mgr) + require.NoError(t, err) + + resp, err := svc.MarkRecording(ctx, oapi.MarkRecordingRequestObject{Body: &oapi.MarkRecordingJSONRequestBody{Name: name}}) + require.NoError(t, err) + require.IsType(t, oapi.MarkRecording409JSONResponse{}, resp) + }) + + t.Run("missing body maps to 400", func(t *testing.T) { + mgr := recorder.NewFFmpegManager() + svc, err := newSvc(t, mgr) + require.NoError(t, err) + + resp, err := svc.MarkRecording(ctx, oapi.MarkRecordingRequestObject{}) + require.NoError(t, err) + require.IsType(t, oapi.MarkRecording400JSONResponse{}, resp) + }) + + t.Run("not recording maps to 409", func(t *testing.T) { + mgr := recorder.NewFFmpegManager() + rec := &mockRecorder{id: "default", markErr: recorder.ErrNotRecording} + require.NoError(t, mgr.RegisterRecorder(ctx, rec)) + + svc, err := newSvc(t, mgr) + require.NoError(t, err) + resp, err := svc.MarkRecording(ctx, oapi.MarkRecordingRequestObject{Body: &oapi.MarkRecordingJSONRequestBody{Name: name}}) + require.NoError(t, err) + require.IsType(t, oapi.MarkRecording409JSONResponse{}, resp) + require.True(t, rec.markCalled) + }) + + t.Run("invalid name maps to 400", func(t *testing.T) { + mgr := recorder.NewFFmpegManager() + rec := &mockRecorder{id: "default", markErr: recorder.ErrInvalidMarkerName} + require.NoError(t, mgr.RegisterRecorder(ctx, rec)) + + svc, err := newSvc(t, mgr) + require.NoError(t, err) + resp, err := svc.MarkRecording(ctx, oapi.MarkRecordingRequestObject{Body: &oapi.MarkRecordingJSONRequestBody{Name: " "}}) + require.NoError(t, err) + require.IsType(t, oapi.MarkRecording400JSONResponse{}, resp) + }) + + t.Run("success returns 201 with offset", func(t *testing.T) { + mgr := recorder.NewFFmpegManager() + rec := &mockRecorder{id: "default", isRecordingFlag: true, markOffsetMs: 1234} + require.NoError(t, mgr.RegisterRecorder(ctx, rec)) + + svc, err := newSvc(t, mgr) + require.NoError(t, err) + resp, err := svc.MarkRecording(ctx, oapi.MarkRecordingRequestObject{Body: &oapi.MarkRecordingJSONRequestBody{Name: name}}) + require.NoError(t, err) + r, ok := resp.(oapi.MarkRecording201JSONResponse) + require.True(t, ok, "expected 201 response, got %T", resp) + assert.Equal(t, name, r.Name) + assert.Equal(t, int64(1234), r.OffsetMs) + }) +} + func TestApiService_DownloadRecording(t *testing.T) { ctx := context.Background() @@ -226,6 +291,11 @@ type mockRecorder struct { recordingErr error recordingData []byte deleted bool + + markCalled bool + markName string + markOffsetMs int64 + markErr error } func (m *mockRecorder) ID() string { return m.id } @@ -257,6 +327,14 @@ func (m *mockRecorder) ForceStop(ctx context.Context) error { return nil } +func (m *mockRecorder) Mark(name string) (string, int64, error) { + m.markCalled = true + if m.markErr != nil { + return "", 0, m.markErr + } + return name, m.markOffsetMs, nil +} + func (m *mockRecorder) IsRecording(ctx context.Context) bool { return m.isRecordingFlag } func (m *mockRecorder) Recording(ctx context.Context) (io.ReadCloser, *recorder.RecordingMetadata, error) { diff --git a/server/lib/oapi/oapi.go b/server/lib/oapi/oapi.go index 901aa0a9..7ad8ef0d 100644 --- a/server/lib/oapi/oapi.go +++ b/server/lib/oapi/oapi.go @@ -3260,6 +3260,24 @@ type LogEvent struct { Timestamp time.Time `json:"timestamp"` } +// MarkRecordingRequest defines model for MarkRecordingRequest. +type MarkRecordingRequest struct { + // Id Identifier of the recorder to mark. Alphanumeric or hyphen. + Id *string `json:"id,omitempty"` + + // Name Name of the marker, used as the MP4 chapter title. + Name string `json:"name"` +} + +// MarkRecordingResult defines model for MarkRecordingResult. +type MarkRecordingResult struct { + // Name Name of the recorded marker. + Name string `json:"name"` + + // OffsetMs Provisional offset of the marker from the recording start, in milliseconds, measured against the start time at mark time. The authoritative offset is the chapter start written at finalize. + OffsetMs int64 `json:"offsetMs"` +} + // MousePositionResponse defines model for MousePositionResponse. type MousePositionResponse struct { // X X coordinate of the cursor @@ -3955,6 +3973,9 @@ type ProcessStdinJSONRequestBody = ProcessStdinRequest // DeleteRecordingJSONRequestBody defines body for DeleteRecording for application/json ContentType. type DeleteRecordingJSONRequestBody = DeleteRecordingRequest +// MarkRecordingJSONRequestBody defines body for MarkRecording for application/json ContentType. +type MarkRecordingJSONRequestBody = MarkRecordingRequest + // StartRecordingJSONRequestBody defines body for StartRecording for application/json ContentType. type StartRecordingJSONRequestBody = StartRecordingRequest @@ -5162,6 +5183,11 @@ type ClientInterface interface { // ListRecorders request ListRecorders(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) + // MarkRecordingWithBody request with any body + MarkRecordingWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + MarkRecording(ctx context.Context, body MarkRecordingJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + // StartRecordingWithBody request with any body StartRecordingWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -6064,6 +6090,30 @@ func (c *Client) ListRecorders(ctx context.Context, reqEditors ...RequestEditorF return c.Client.Do(req) } +func (c *Client) MarkRecordingWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewMarkRecordingRequestWithBody(c.Server, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) MarkRecording(ctx context.Context, body MarkRecordingJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewMarkRecordingRequest(c.Server, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + func (c *Client) StartRecordingWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewStartRecordingRequestWithBody(c.Server, contentType, body) if err != nil { @@ -8101,6 +8151,46 @@ func NewListRecordersRequest(server string) (*http.Request, error) { return req, nil } +// NewMarkRecordingRequest calls the generic MarkRecording builder with application/json body +func NewMarkRecordingRequest(server string, body MarkRecordingJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewMarkRecordingRequestWithBody(server, "application/json", bodyReader) +} + +// NewMarkRecordingRequestWithBody generates requests for MarkRecording with any type of body +func NewMarkRecordingRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/recording/mark") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + // NewStartRecordingRequest calls the generic StartRecording builder with application/json body func NewStartRecordingRequest(server string, body StartRecordingJSONRequestBody) (*http.Request, error) { var bodyReader io.Reader @@ -8679,6 +8769,11 @@ type ClientWithResponsesInterface interface { // ListRecordersWithResponse request ListRecordersWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*ListRecordersResponse, error) + // MarkRecordingWithBodyWithResponse request with any body + MarkRecordingWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*MarkRecordingResponse, error) + + MarkRecordingWithResponse(ctx context.Context, body MarkRecordingJSONRequestBody, reqEditors ...RequestEditorFn) (*MarkRecordingResponse, error) + // StartRecordingWithBodyWithResponse request with any body StartRecordingWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*StartRecordingResponse, error) @@ -9815,6 +9910,31 @@ func (r ListRecordersResponse) StatusCode() int { return 0 } +type MarkRecordingResponse struct { + Body []byte + HTTPResponse *http.Response + JSON201 *MarkRecordingResult + JSON400 *BadRequestError + JSON409 *ConflictError + JSON500 *InternalError +} + +// Status returns HTTPResponse.Status +func (r MarkRecordingResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r MarkRecordingResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + type StartRecordingResponse struct { Body []byte HTTPResponse *http.Response @@ -10645,6 +10765,23 @@ func (c *ClientWithResponses) ListRecordersWithResponse(ctx context.Context, req return ParseListRecordersResponse(rsp) } +// MarkRecordingWithBodyWithResponse request with arbitrary body returning *MarkRecordingResponse +func (c *ClientWithResponses) MarkRecordingWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*MarkRecordingResponse, error) { + rsp, err := c.MarkRecordingWithBody(ctx, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseMarkRecordingResponse(rsp) +} + +func (c *ClientWithResponses) MarkRecordingWithResponse(ctx context.Context, body MarkRecordingJSONRequestBody, reqEditors ...RequestEditorFn) (*MarkRecordingResponse, error) { + rsp, err := c.MarkRecording(ctx, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseMarkRecordingResponse(rsp) +} + // StartRecordingWithBodyWithResponse request with arbitrary body returning *StartRecordingResponse func (c *ClientWithResponses) StartRecordingWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*StartRecordingResponse, error) { rsp, err := c.StartRecordingWithBody(ctx, contentType, body, reqEditors...) @@ -12543,6 +12680,53 @@ func ParseListRecordersResponse(rsp *http.Response) (*ListRecordersResponse, err return response, nil } +// ParseMarkRecordingResponse parses an HTTP response from a MarkRecordingWithResponse call +func ParseMarkRecordingResponse(rsp *http.Response) (*MarkRecordingResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &MarkRecordingResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 201: + var dest MarkRecordingResult + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON201 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: + var dest BadRequestError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON400 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 409: + var dest ConflictError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON409 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: + var dest InternalError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON500 = &dest + + } + + return response, nil +} + // ParseStartRecordingResponse parses an HTTP response from a StartRecordingWithResponse call func ParseStartRecordingResponse(rsp *http.Response) (*StartRecordingResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) @@ -12984,6 +13168,9 @@ type ServerInterface interface { // List all recorders // (GET /recording/list) ListRecorders(w http.ResponseWriter, r *http.Request) + // Add a named marker to an in-progress recording + // (POST /recording/mark) + MarkRecording(w http.ResponseWriter, r *http.Request) // Start a screen recording. Only one recording per ID can be registered at a time. // (POST /recording/start) StartRecording(w http.ResponseWriter, r *http.Request) @@ -13293,6 +13480,12 @@ func (_ Unimplemented) ListRecorders(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotImplemented) } +// Add a named marker to an in-progress recording +// (POST /recording/mark) +func (_ Unimplemented) MarkRecording(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotImplemented) +} + // Start a screen recording. Only one recording per ID can be registered at a time. // (POST /recording/start) func (_ Unimplemented) StartRecording(w http.ResponseWriter, r *http.Request) { @@ -14270,6 +14463,20 @@ func (siw *ServerInterfaceWrapper) ListRecorders(w http.ResponseWriter, r *http. handler.ServeHTTP(w, r) } +// MarkRecording operation middleware +func (siw *ServerInterfaceWrapper) MarkRecording(w http.ResponseWriter, r *http.Request) { + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.MarkRecording(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + // StartRecording operation middleware func (siw *ServerInterfaceWrapper) StartRecording(w http.ResponseWriter, r *http.Request) { @@ -14681,6 +14888,9 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl r.Group(func(r chi.Router) { r.Get(options.BaseURL+"/recording/list", wrapper.ListRecorders) }) + r.Group(func(r chi.Router) { + r.Post(options.BaseURL+"/recording/mark", wrapper.MarkRecording) + }) r.Group(func(r chi.Router) { r.Post(options.BaseURL+"/recording/start", wrapper.StartRecording) }) @@ -16693,6 +16903,50 @@ func (response ListRecorders500JSONResponse) VisitListRecordersResponse(w http.R return json.NewEncoder(w).Encode(response) } +type MarkRecordingRequestObject struct { + Body *MarkRecordingJSONRequestBody +} + +type MarkRecordingResponseObject interface { + VisitMarkRecordingResponse(w http.ResponseWriter) error +} + +type MarkRecording201JSONResponse MarkRecordingResult + +func (response MarkRecording201JSONResponse) VisitMarkRecordingResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(201) + + return json.NewEncoder(w).Encode(response) +} + +type MarkRecording400JSONResponse struct{ BadRequestErrorJSONResponse } + +func (response MarkRecording400JSONResponse) VisitMarkRecordingResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(400) + + return json.NewEncoder(w).Encode(response) +} + +type MarkRecording409JSONResponse struct{ ConflictErrorJSONResponse } + +func (response MarkRecording409JSONResponse) VisitMarkRecordingResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(409) + + return json.NewEncoder(w).Encode(response) +} + +type MarkRecording500JSONResponse struct{ InternalErrorJSONResponse } + +func (response MarkRecording500JSONResponse) VisitMarkRecordingResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + type StartRecordingRequestObject struct { Body *StartRecordingJSONRequestBody } @@ -17162,6 +17416,9 @@ type StrictServerInterface interface { // List all recorders // (GET /recording/list) ListRecorders(ctx context.Context, request ListRecordersRequestObject) (ListRecordersResponseObject, error) + // Add a named marker to an in-progress recording + // (POST /recording/mark) + MarkRecording(ctx context.Context, request MarkRecordingRequestObject) (MarkRecordingResponseObject, error) // Start a screen recording. Only one recording per ID can be registered at a time. // (POST /recording/start) StartRecording(ctx context.Context, request StartRecordingRequestObject) (StartRecordingResponseObject, error) @@ -18579,6 +18836,37 @@ func (sh *strictHandler) ListRecorders(w http.ResponseWriter, r *http.Request) { } } +// MarkRecording operation middleware +func (sh *strictHandler) MarkRecording(w http.ResponseWriter, r *http.Request) { + var request MarkRecordingRequestObject + + var body MarkRecordingJSONRequestBody + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) + return + } + request.Body = &body + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.MarkRecording(ctx, request.(MarkRecordingRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "MarkRecording") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(MarkRecordingResponseObject); ok { + if err := validResponse.VisitMarkRecordingResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + // StartRecording operation middleware func (sh *strictHandler) StartRecording(w http.ResponseWriter, r *http.Request) { var request StartRecordingRequestObject @@ -18844,366 +19132,370 @@ func (sh *strictHandler) StreamTelemetryEvents(w http.ResponseWriter, r *http.Re // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "H4sIAAAAAAAC/+y9iXIjOXYo+isIPke0ZJOUqrp67KkKxwu1pJqWuxY9SdVtz6gfCWYekhglgRwASYnd", - "UY77EfcL75e8wDlALiSSi5Za/CrC4akWE+tZcdY/Ooma5UqCtKbz8o+OBpMraQD/40eeXsA/CjD2VGul", - "3Z8SJS1I6/7J8zwTCbdCyYO/GyXd30wyhRl3//onDePOy87/dVDNf0C/mgOa7ePHj91OCibRIneTdF66", - "BZlfsfOx2zlWcpyJ5FOtHpZzS59JC1ry7BMtHZZjl6DnoJn/sNt5p+xrVcj0E+3jnbIM1+u43/znhAo2", - "mR6rWV5Y0EeJ+zwAyu0kTYX7E8/OtcpBW+EQaMwzA8srHLGRm4qpMUv8dIzjfIZZxeAOksICM25yaQXP", - "skW/0+3ktXn/6PgB7p/N2d/rFDSkLBPGuiVWZ+6zU/yHUJIZq3LDlGR2CmwstLEM3M24BYWFmdl0j80L", - "cfCaCXlGI591O3aRQ+dlh2vNF3ihGv5RCA1p5+XfyjP8Vn6nRn8Hwr4ftbo1oI9yccyz7HTuAb50k5L9", - "dHV1zhKeZWzKZZpBykYLPMwNaAlZT8z4BEyP54IZRKzVq0y4hYnSC/dvkMXMbc3hmFZZbWvGaiEnbmsp", - "txvRK7L9EzfMoZQqdAJbToAjL2nEx27H6kK67aard3GlC2BijGd3O2RjAVnKbrlh5SiWFuAQwYjfgWVi", - "Jqxx1+FPOFIqA44wtBHEwq0wK2ZgLJ/lTEj2QYo7NhOJVgYSJVOcbaz0jNvOy46Q9k8vqumFtDABJGn6", - "S3XbPBcDB8PIdS+hjDVhwm4Ft/JOt0SkEw/AHWj2HHQPsSzni0zxlI2VZsOw7yEDN69Zxa200MidBrPI", - "jf7Ks6yXZCq5YeE7R7EOgoTM2l3yTGSZqN2vP6EsZiO6TbceLSIiePE+B3l0fsbKr87SsMjMsSFImVaO", - "3+xBf9Jnw1yrBIxxLGLYZUPLb+Ay0QDSTJUd7td2UFGEJj4YXd/dnP+didQxtLEAzcZazVroNHw9E2ma", - "wS3XEF3UWG6LyK0iRwhCnNFXLFFpfZYSF5fQq3aQpXst1+s2YLoG4xy6XVqe3Kxu8fjknF0U0tFSHz+5", - "0jwBpiHXYNwVyQnezX/wOb/EccTijPuWcYs/utHI4CVhX5+9dhRvWGGAuRUkn7mJEiXdzygENLdT0MxO", - "uWRG8hsYJNwgS0BcwHmPp1rNgJ3A/EqpzLBzraxKVMZuhQZG1N2/lhE2mmWvNZ/BFkIJTzPGj7vMYZ+e", - "KWNJADVEz9ISKitm8h1h/soifwWteiNuIGX0ISMaYbfCTgWJuEzIKB50O+NCojh6x2ewOncNEuFDd7/Q", - "ZUozmOV2wQgzkTFwqeRipgpTfmyiKOx2s8Vp3GeRs9DX8dPQb2dpHPfov2vkGN1dobPV4R8u3rgju7MH", - "NuJnG4ssRqhLFNa45to+abnGlXSb8I6RWlO9WGLaq5yQmD3L+AgyBBRuH4nKIgUSD+RmIROW8MJAnN/l", - "XAcFNMvejzsv/7aVMK84wsffVgQMTtnYDGISbgX/avorl1kjubWMKLfJlF+qbA4XYIrMtqlTLKFPmXHf", - "Mm6tQ22mgaOc4MwRqnBXqAqbqBlsqUzRrA9VplrO8U2vatWr/MUPEJwDjXf2hDrWOgDtrm4F7GtoXLET", - "tWtf4etwL0uc0CP7HGSqNBvzmcgWfSfv0iIBbZh0N545mOZazUUKumdySMRYJMxyc4Nc0DAhrWJ2Kgwz", - "YF8ycA/ZXAsDbM614NIaxyk1BOJKVJbx3EAYCEKzOWjjZMqoSG7Asr35c3bA5t/vdxmXKeNy4bj+hEll", - "WaLmKEuJV7nLPVFOEL21/kBdlmdcSPb++GKfCePUCqUdlnLDhsopAEOS3wFNpoFAHR6EO5s/b/7n9w4p", - "Ci2NFZnDjAmAdW/fbgenjBP3rtovaoXEfIzl2jqiivGcFR0YH60Dp+WtLoT4WAMdfosaoXv4jrnICl2q", - "v6cXF+8vBsdH51fHPx0NPry7fP/ml6Mf35wO9/vsaOSUMzfIFIlTknfSS6+Wz8GGfprhSzqzZhrcFSOr", - "LQwfZeB+wJd6nw39TmNfS3+oPQPAhtVluF0PHWtRha3GpSJFTKLxdZXCCRTQ3xl2y4VloyKdgO2zIR9x", - "mSoJ6fCl/4QlXCaQufe2F6M5nwCTfC4myBH5LV84Db6HazbxzR/b8TQ6krtG2mSn2ykXi6KUo7voO8ND", - "mRsjJu5OasoNe5/zfxTQdZrxuCDJb4rcUQVzPNb0NIxBg0wgDtJbGBlhYTBVJiI2f1Kk1Ja3cDsFDf4+", - "ieSdtMCLSNfOn3M7jbyguJ1uPz/7fwrQpTYKd0lWpNFlV3SJGq+8x2snzY+VlJDYdlsN3HkTX5IJR0hE", - "cklhrJqBZpcnP3fZecYXt1pMprbLzos8Bwug990jxs0NKSOWiQ+cX2F0qZBf5lrdLciMJQz75e3WRh43", - "qdtfDNW+KRSrCkWaD/ytPaUekeYnwiS7olNajoG0si9sQBR2zgW9qvBrMZtBKriFbMFyDQmkjoqGtXMP", - "g7XUuCeQsRr47FHQbRdNeOWCvinBa3G2Qo1Pirb31Hyr3S4pv42TPL7RsULQreyOMzCGT2CQqCJGofRs", - "d3M7EvQfO2004wunIKDkjawLAm1UqdD0t7iBQwM3sUf+r9PF8pwgnQBkQ2ITgyRTxilR+BVxDiGFFYjD", - "9EdlnHZW5ETdg2TK5QSVH7SNiWLGNKB+CinpOGBQe3e6Okpp5DJWaWCpupXMqPpqiSqy1L0HPIz5hAtp", - "yKgn4ZaFdetbQJVu+LL8jaXCaZI63CvLi1lOSiCdVUkLd3ZQqmn+wMG26n9HCq5UuT27yIVT8BbeWcLM", - "tLDuCPtNDa5+lZ1uZ/mm6n/CPaEtZ2lHmymxjsfL6FZiwDqCVNKoDNDV12ryGNG37kbcx16RVpo5tlZM", - "prZuhYW7BHJCKjK5ns6ErcTNrXJCyAqZWER64hmGxEsqxqhkWuKgZspzMP3SDuzXPzo/O+YEDP+Xvn+v", - "8Cwz+w613OvUsAzmkHWZu9Mu43pi6KmIpqIBGpCqucttX021w8e98mzlL/Wpac5MSOh6S2rXH2VQ6Cyy", - "jjc8uzeF98i6p4vX1Ggk4xoYxwdU3HgclZfu/A8WlstY8E1WtstKuitPtE8oKqMw2dWeiiOPia90PnaX", - "vQWOKCIUn2UlrXM9KWZuZpYo0Am9Luisps/OyRnDlMwW7s0lPSp7am8j3Ib/YvX9umSxJvqKGKcaHoyG", - "xb/2/qv4EaIXUvfWG1/iCnE5i2wm7kUIt+gGsTnP3AubZ7d8Ydg1GWSuOw+6xai/ZHUvb2rukc93URWD", - "bHGarDhLmJ2iK0/DbXOPj7CxhjkqMOqt7eylm6LbQdpaZUEokoLu4b6p9iwkGyk7DXw/53ZqNpsfcJ1V", - "jvHbCs94oyZby/JMTUhQV8I0U5Nu+L0v5FhV/3XLtewysEl/v/8ZBFQ42DfxtFE8ZWry9MKpAY8vSzTt", - "JGHWcPBW3dPN0WU5NwbfRFoVkykr5FhkFn0PyIUoUKDv7c1DdDWowtvoGpqEf6ky98wBnr5iPMsYug3Y", - "siAxToMErplj3X12CWTBMTkkpcd2XGQZczhBmuSnYXmvMThuGTyr0NnM6ggg3S1YXgOLVnbkP/IcLrzo", - "kOiqMLjAEmdKCuseNtIqvP7jk/NeECrekMDOgs2c3uWW6wnYLgVqkNrvDfz4AspVMnXUfTsVPnSEdqKS", - "pNDuGRrR83GqqP3eQRl/rUcJ1VwTtJm4WqB4Crp11lQlBCv6rjZ/173jAT06wJNp7XTRdSSfDwz8Y3WV", - "t0oqq6R/OguZuLcp+uuq66JwziRoKl36zO0L0nIDVuU9RI/6yOglbME9vVWi9V6C1aIeleUpjNapGVGi", - "90FfRecPuOknqi2xZyy+Dr39pzqnCQflzPLR/roVg1zYgrKvcMSVG7AupEVDBnMuyeE4FYZQ+RX5W9wH", - "Ywx6KWHiaAF/I9LploaV8luwt0rf1Gx065lCDVj1i20euULBNeKrrgrsaHvUag6SOySdgeWoHXjILRw2", - "E6F7M4Fm4G0fJeWvak0Q19SCi73mk0XOgVFF3hHbJpuGeL117lVabvCq44hzI2TapqqEA/XRwhqsfLEI", - "OC/GSt+CZ659NqQoxgHPxfAl+xn/gx2dnwUz2p7jM3oOZMilP/YmIEGjuhV2zoZwZ0E6RBi+ZEL+nXwZ", - "fj/lb302zFTCs4GP1Ry+ZGZhLMyY/wPThZQOYjxTcmJECo3tNk15ad7pdqr9u5/CQh3HW2sLRTXdgCrt", - "yBZRUjbhQ5BmhAyOWxEdHHg6OSBRcXbSgHeghSXaQuCvoZifrM1/AicbTPshrC5WCAZDTac0ks147qB7", - "y3WKsRY94THF7d6xNlXYMqSEhAz7xb2aDdrGaqZX0vLYqLBsxhdsBIzLBfuPy/fvUEVqaD0rh8E8Coqs", - "P85EcrPxsVTgi8l9GjQJntvCaXlzwSskRG5XhRxufh2JaiMPfSFFz/TtndT6Tqpd/QAh+4SvpXbYPPKb", - "yUAGiVWRUNnjy0sWfkV7QzA949kdf81Q0WpRKSaxGPK3b5jlk0ac69JsDmBFnoPGEGpiVD9+uLp6/67L", - "jrrs5OyXFh0mqsz/IoxAo7njej7DqWXhLrMa/dTR6e9ic8MtBrvc9RKldCokt81TubO4W8zFHWQmbuBa", - "rJl4cf+Jl/DwruNW6lbQJgitfSbVUPBnWGxkeDewGCmu06+B3YXzfGN2WzG7G1h8GlbXgMsjMzp3iJUL", - "/BkWZGOvtM+fPR7T3RIDOnVb7LIfeXJjcp64V3ucC92Dmwa+h2brKQYlJIUh8zRl8iwQY3INxrRwp+25", - "LU6+ntuevTv/cNVlV6f/eXV0cdrOc5fVQXgAg7lMtMqyS7A2g3QjqzH4NTP0uWc44d3Ex7b6JFdG1DIy", - "0ZEu5KT7ZbOn1dv4xqi2YlQE9YFHjE/Ds1qA9cjcy7GnQUQJodXZXa/EdJ/HRoHelXvMfTUB45B+G7UE", - "11u0rrd47PW8PeYe/JPW2qSOqtjlvcbAcbN6hchC3OThBIHVbHMSFbu3xlKLR1lqOQWMMKQEnT+039Dq", - "Da9lzW/EHJwauiH4mGViDmwu4LaKwlqKKHbv+HGRBd79nWG/wuji6ri04byDG7XfZz/575TMFq8w5iUw", - "9LHSOEsGxjBKaP3UkaGx6/jGkltZssOKgcOKTxDV3Aqa3QNEg+W+ER26cpb2ANF1noE3JaGs+gf67LJh", - "vC9jGE2XGcU4s5pLg+QV7N+jTOQs4RKJBCPkvBG1DLnGOOphtaXhTsbyLS58cyz5KneIx5JvyyKqmPIY", - "VEaLleN+DhbxLYJ8dy7xSeLI1wHo0XnFFxRPfl+u9MoXsgjB5JqKP1DmRhtX3NEjt2UW1Fvysp/UuEcL", - "z7nyqSm1O7IqeHocVWTK2D67Ql3R6kVgm94hkGqV55CyQlqRBef+oOTH7nWptZiD6bMrDdyiB0HIXq7V", - "xD3PQ+UhDOS1wPY8vx6INMPIjwkMMr5QhQ1vlH3GDSukhkygCKCV7RTkdgzM7/Gh3Kvthr+xr1b2FbCj", - "LtOekH2thdAm/tXEo7Ykjwv8exmtUB0MnWoJEtGgTNEoHbqldzT80q/7QZdGbb6hzQkI/irOpLCvucg2", - "MoPA2yhDxD0tRuCTUzLxO+33U1Pa0ua/0dlGOnMAG4zxyp6ezGLg2Y3IjIW8HSVnYKcKk7xLPPTxTBZy", - "MgXTUb1NluJt+gbsUWHVkbU8mW5hk8VNbD7tRRBwW5FTVLY2aEtDDzAeSZhpaZGFuykvjKX4iax65JAN", - "CYtSmD57p9i40FROaVlI34os8wK4zDX1tP05SDh2a9/oeCMdl4D/ZMTcCqgnEZsNxPaVGPrVXweeDpwA", - "JTpwGB4IgN2CBoYemiIvw1t8ZYdxkWULFLNKh1pmTYKsS97Iio8ofC/gwar40qkiLIMv6yCnxAiCZTAt", - "ynuY8BzjfUi/P26q4VitxYBFc8pSuGGwqFjNkxs3m1dV2FiDmQYjhTAsV0Laz8pnvvGYnXnMJ2UvD2Et", - "gVa3NQpgmcKl5z+z/AaQympZ0KV/oUlK29zvCm+IbXLz/VR1LlsNhTlooVKRMFN+G6wdwec790Ex21Fg", - "Nc8jEeHSIb7R4EYaXAuCRybBGHR2o8BcRiIofuQG/vSiBzJRKaTs/N1ftkTQ8tpGCwsbtXS39pozviMJ", - "dZZmsDEyIkgzkYbI7aW4CM5+ODycGfaPQoD1dEc2damYkL1xJiZTy3y1Vwy+39Lb5pd+KL0t+cG/Udgq", - "hdWNik9IWx7v3iieCjlZ+zRcRcCMRoVXrK/rcDZulMtwt80zDTxduPvxuIeRT05z5PjMdW9gqViuhdJs", - "GM7upxjiHHVPsbD7XTYsdDbssmHIi3L/LtOZhpRzNdTgk4vdBQxrlRResWEEGTETL+eaSsezXOVFhliC", - "SUTcsoQb2LYIwyMRSyuIvsmnjdTjMfTpX6HrgfTIcUJUB2YTzOoEGEYspzZimM0kUg+5BjoqiRgPvX4X", - "UrUwVbX2mzdpSbAvX55eXAyO3797d3p8dfb+3eDi9PWHy9OT3cuhO3YRKYeOHqzwRFRaTITkaIFaYiOt", - "ziu3ao1LxBf2J+1f+E+vFjnUzAG4wkrabz2TxWf8/izVraRwVMOExBKD7MSnWXbZa7DJtMv+86eLLqPC", - "OV12aRcZmCm4t+3ZjE+gy95CKniXvVZuzBXc2Sv3su2yGnV3q9JtXfaWSzHGHZ5rGNMa7+0UNLHJmdJb", - "1J9uVHivYUW3Qsi18Ub+CkNTmG2lTAAfVkhoSZZ7evZb3/U3xruR8XqgPT3HXYHLI/PakAG9sTpJmSqN", - "ekKzLJq/jSjvmday53bZdz3zbrUmur+WkGHXdyv5PTmybWVzZ+GbPpamETLFHkGYwYrqT2GaZ7o3zzOe", - "u+VcG8eHcg1OWhNDwgIH0esSZqCBCtytoxy0BnpRYfx+TZFRWx8WZoiTDPltWrpjeKcONywUNHaTY38H", - "Enl/Ob3qsvP3l1ct9e+VsYPAfuIwG6l0gaLFzXJw/uGqfKR13eH4nIuMjzJoEWV0tDi+vifxmGGu9QjG", - "ytf4CaMQDHgwVNBrl43XqAt4JKndZYUU/yig0ZShcvN8k9APl9AejbtNFlYxnBWGsJ3wpuYwO0hv301G", - "QwJiXj0TX7tN10yX5YeI/g4o3mdAw7rod0SsDFnD5CX8PMpA7Ra+aQNbaAN0X59CHViGzCPrAw47o0Dy", - "kGigccVOsRoZMiO4s+zt2dtTKtnzSVUCv7O6TrCNrPMKjgqyY502MxOzNh5dHjpMWF4VCU53MwdTO8u6", - "bLk34be34hcviR6pqViYpsXeEJ2rVu3i/c9dVnah3L+vwCwL+AdCXCsZz/kETtTsmBLP3yiebmFCPXn/", - "tjEg1Npz6OMm7KfljDgXSssta+vlfPLgwnqth/om7VqlHUb+pmo28DUI0Pr4pFbH9VB6bKtjmg/Ke4sw", - "Pgr6mIV6Xox82JR4LSQL/mtufTGkFRIYu/voYpVzK+YI4kAuIfyUojb2nCqIUMNCavt99sEAG1pDBY5u", - "mx70SMD8cv+Oxsk2EvsbDO7eNo+ZQsFb8pif+WvxejDaYjHVoPLWWdBzwIpEYaapGONTsHqbz4UpOPY4", - "HIlM2EWfnfJk2hhAwTH0FH7W86u6Q+tPx1S+uf224yHN7IEn5h8emx2ObC4OW8wKT5wN3No7fnO571G7", - "zPg6B40XIBNgV2IG2Irx6Pzs0wqx5eN9k1/b4Z67sE+MeU9ivvVRTKsXebKUcdVAaJBWL1ZCr/Z8ie5D", - "FDMNdsxy0FhpdT+an1W/1UEKlovM7J6QFsipdnGMW6vFqLBgNlAeHmmV9qY8HWhInLoiZF7Y9SjduCRf", - "sCSBlByLWA0NJwlWPQxD6fpOWk5QCc8fjt9cxlEe1YVIDlt9XZMoHd5TwnhY7WHbcXcTIQj1zeV+XPSv", - "4KR/0O1YYDUUW8G/V+XSG1dU1nON1isQsXa5UeBV9B7D1s0ZgsspA0sH9nupcvW2UIKSfKO4eMP1xD2m", - "vZo3LjJ2zoV75rw5Pv9S5YU/1zc5sUFOJPlTi4c6JB5ZLGRJfk827HG6QmnC6IeyYV/XJMp9RFpNH+j/", - "zfF5VdNOjIOdsbXG8yDObNzLq+yGvjTvVonHUqXtLPPk/VvmPohwzdo6bU2qZAq6ZdsX+OO2G3/lBTb1", - "qySrn68xUmZfXImZkJPeUZap2x55yeKJ1uJ3aK9AyDXwlg1RiRdm/lHwpjyo5t7kYa7PiFFw7ghMaTYX", - "KajwU0vB5KcVevWtOR5G0HsCuYcLxZSzewu9zZJO8c2v/OrlvmzIy8Lwz2HCK/f+TZxtEGeKP/lDuwGL", - "L9w4hzpmhc5fi2nuXZn3tR3F1psM+KaEy/SL/OJdaM6832fHXGsBWH6/rLU9pi5uQiLXGmG1ast8xfku", - "w75JoTJ+3RK33BPi03KHpdv6xiPW84gKWE/MKWJw2S3T5X5SvWpBjl/s2jDkHdyy9U1DWNlivHy9b+gb", - "knPt1OL285zjB6tHokblI/p7rVPGKx//TzuI9Axp6Uy/c0OQR2v78Wm7eVQ4YNWjtd6gwKOa5lVh0dak", - "sN7fEpqJYoBQiyOu7DyyZGBnUz4HaryGcq501Zsm7jRcLmVHeWFYbXryxGAnAgzRY2cyhdxpw1STvJ7W", - "84pxZoScZMDcF5SXTOEHqQLqCTpCWSm2bvz5zU3zOeTBJ3LVXPHR+xzkGqejhNtSwbF85B6Hnp+4a1U4", - "mHQbX2wkpF9dKfoD4j7iNY0z+xSpZ0K0KG9U2xGmSuDytUDdFkL3K6Ma5fo2JWt5famZplVTnEqqQPRz", - "emUshavPjpU0xQy0e4dShtqSnobtY0LLkClWNbFY6ktYp6txtOQLnu2U7vVYWlkTyt+UsvVEaPloQHj9", - "SYnvHjoZ7jKuOV2ttPryGpmjYcwn8KSLxKAkUCC4XOyqZFTdgGKtyyTcZotyKT56Es3DCptFzD+UeJB5", - "3uO+KbVSZCjxzUTVmDBVzXTWPscyiqzVYdagyCXouUjgWHMzXcOgZ1zyCaRY9VQkwOBOWKxFCHc5VpfI", - "Fk5ncFoLYpWvaU/RbQ6buFW6V2WXlE3mWaqQNfqOXHW2+X/+1/+m+NNqFVzXUNN90DMf1olP4N5EzKFX", - "5L4gLbWXS9W2XJC6aT2UD0Zu8xsjbGWEHpkGCV3XEzLCNrjsXlsV99qsrLp0jLKoKju9ExYjRql9vpg4", - "dHU6AlYQv3OqQJnZWsgUdIY994JlimhOl+XEkimXEjJUT5AuKPmECJLYol10yQQmxpAsEqehT7khWzft", - "PHh2mZCkvezh06NMztknF9DZCW5UQ66wamKEinDmWNnWLZbusyESbZEP2Qy4JKtSOHgq3L2Q3iYwplk7", - "jQi1Nc6mwDM7XZQN77CMUp8N/X+HCTnLNcyFKky2KMc0Vmgyr+GEz2EQ31CARFmsijkuFIoxlfWxEMqW", - "qrRa7WD5ismqZlwbolDtOPeEq8wLAaxUctWoGdhprQCUKZ9WJS3RdXa6HX8PnW7HnyjK1PKoUeLspCzk", - "S3sMV9BnR6Mqvyp2N24xVuSrBfWi1yScQswyJd3QsrwVp6rc52cnLTHW/gIlj3pitJpoPms28PLHCPfp", - "O01i5U9RzJw6PyusBe3+RR0Re+Ri6/FcDLcpY1jfU9dTxTpWhILmvZr9LLJsTXmyN0IWd4y2xN6/f9u7", - "EVmGlQdR7mHVlBIIQpYdH39522eX9c7xw4MU5gc3MzMZhjeRQzMuK3LAqUtW5Nf0QmMGM6UXJUDJnBDi", - "Yrx93rvN0HLl58TSzNyW7M4UubsoE+clTyiRV677m0BuF8h4WQOlZgOHEk8pkONg2V0eu30uiePmIdpr", - "nCdKGqu5iFHgr9MmLUAiUrIVBFLss6FUEoK4mGRqxLNVannFhjOYJTWxlEy0KvLwJUIfsWMq7Cs2TPLC", - "gB2yAxyn9GKQq0wkCzIuvPvw9uiA/tBLtZiDRNqt2LOSfsuGqQxjDaZcsh/6h94/loq07F/iW+PoIqFu", - "U0OlZni0l0OWCQlNAeMOi8kms8TJFton/aHaZUu32NlgrAEGN6NI7xkNEFrZ+isRkv0sfgy9e+rBEm5z", - "XZaCxoTMMmBl6GZ/+W7oSU3IGui+M+wtzHpncqxYWszyPjsyppiBg8QLXIcKiYjfoc9OgqEmJC1pSDIu", - "Zlj9PHEKSOh6YWY8y7wfEmPeOcvcswuhNrDK8mxwMxpi7XZjHY468NON02EdyN1SqPixKdcpdVHDipwe", - "mp6NBCSsw45TAjrurDyg8QX06t1ia+Re31qEcblfHgyKd3ifhl0cvSUsegA4nuYWNmk+XhgGxSc+B/3Y", - "oogcq9ksPhvDGBPSqZfE7d6M37FnPzgtX5tuTVY0PmvJKDQmCtILMPguYAYsCZv4rjyY90yB++ZSyZ42", - "psvGIgP6F+q20xnM3H/u99mV01J9iYJ8ujAiqbhfXT10aF5gY/wWJGprVJUPLDc3JoanOauUjBEWncVT", - "9gzYHp7SLzVTs1pLVcJYQ3fvpiTfxZK21EDV4ZXbAr0whsx7Rk5nuV2sQ0pvAHPfHnN8DHDLfsDwH+HU", - "IsVGqkCXDkktRHZEVmGBSmvuqti4fSKF87szmuOH8la51nxBSouYTEAPNhGA/672FN2GFH2fNZk6TjY8", - "Pv/wkr1zmrz7H0cQL4c+gbcmWyJwD3vcmsBKRJsqA4xnmaIE3NInVav94fdtFRNyrm5IYa506z57P7b+", - "eYM+NG7YsL6TIdurTeOJqJYcC3ofgygSLlkqxmPQ9VaZOCihbfqf3Z3ORWLFrM/ebkP/jXtrK9lYvzvi", - "dyWL2FYlQ4TaTRs7Kp2CHiIU77aJqlAKrOhm28P9IXxzEyWslQHbM92mFF3lSls00CMgeohuhmXNdL3O", - "ll7PaicDtnuyeVMsYXaZUN5wF3U7I57cOEVWpgP/l/AQvlX6BrT7w5RrSKv/xuI4UQ0x7DoU1T+mp4QA", - "c6zkWEzuY6fzr5FapX7fVhVTGrHbvnsuBK9j6yOB5zaZ7h73tnyWhT/Jan2DY1qBGZXNIVhJmCpsomZA", - "1Q5qncCecB/UBo38ogcpWEzFLK15wdni0CfX6m6BKkHZRS3s0yjykjzVJmkFdz15YdlepiZddsu17FIt", - "v33clWMBxWRqGdwlkPvoGNqf1Sp7wv0dTZwi4p9m1IPVMD7hQhpbr2DYZaZIpk7ACEnaQMKzzARn9Iol", - "yreb8/Wwqr6+T3eSD5QVVS4VjJV72OTddNkNLFJ1K92LCDuH7uPmQsGap9tYvfzvQVlTYQaWp9zyPkVf", - "TZ4SCc8xaLqkjHAx9YgbKgm2lPn85vicLqlWfvoJdxnqlo+alaJDLehauWjTZz+JyZTNVVbM4BVT47ET", - "nymMeZFZqmyW256QtHsyzD3dzkNI2S9vveG+ohZV2J4a9/xDA20fVN2D/JO9YOclu6+j+o9rpGTLjnaT", - "NjQoZGWiaTVoQnXZQytEMmikeyqkMRMUUKsZ9P0EcYb/pvbgfXYmWb3Mmu9B7ANcCGIvmZoJ60O/hPEm", - "kj3PzG+nCi0bNPk+y4DPQ1O9sKIaj73Rw63lFzcM7nhivRMqKeU1Kj2OiekCaH9HV8c/1QrBte3G+Egy", - "bBvqHlgELTb84+NwH+N1mFQ9lb9qbk6DdbwV/THoXXJ6FzmErhSju2VKs1QY6mJaDZ0LTrvrsoUq2Kyg", - "Wp0pbuEuz0QiLBu6gwzdDEME/rChgJeW2q2Q7D7IVTUDTCJo5oVJn12uAr7P3odnWdBxbmBR3vXyRe87", - "qAUNCW3XnvgN2JcMi1/fApbAJm++9/KjT9P4LFaVdWsdVbrelkT97JMp3++zXym/duh3NOxWjswaDjlw", - "ODzypPESsQntnwH1XzEuF+QRUz6C0h18PO6zsrVLNd+e10u6IYyRuh126+KNFIdhxRPJ4BrhhIgoI3DX", - "jL5Mq/rsqDqSB1SohEGb9CdhSQZcE33ZOGTpAEPfqGBYoese9avJfJdxYoz75Eiz1RwOyaeg4RXmLGfq", - "1jBeWDXj1seNuQcpelR5/ZqajCXiofHH2zZ0plWTj5LKsXfHBVYKp06X25FUXlM7I3pwjrA9aMqCo6+8", - "IMCwEm/fNd4qTp5qKra9yqFnYIxXKFZfkHEneLnagKae8dxQ7xP0hR6MRQZEHd6afwB3FqQRSh7kWrmf", - "D1Jh8owvmEPYV2V8s58Q64Q61ufDfh0ouBW+jEq9x2FzJ/iIrM8UfY/F+8O9zz3VV3e53BGu3/BIq3wQ", - "7p+qU2lb/4Nvv4gX4K662ykvwf2HP3/4UBSzwTjjE0PwcVe02T8VzhxAGHsyHzt99q0qDPhCpjvGx40K", - "a2OFGnBKRr+SyYmYDXKG2j1lMLbuUe+Yq9uqSNMsvLDJjXfLdRqFE6riLQXdrvzbHr/xT5Haqk5173Q7", - "GDGAn0QXmKosHdzAwsSOl1IUnvvZnc99W+96RbPWjJqrIXlLBkpZzAb0uqDlkOd2Xj5bpvR3mHOFhgsx", - "A09YOXj7Slh31WJzt3qK/2SJUjpFh3oZM4A3lisKIovOFCml+F/3mWkJXe86buoWJM1HiuvUJ3DviKPx", - "qnTHXmFKwuRUks5nH2yOOXSTRjdLHc70UfVGvYcZ0bcr1h53HZThDpLCol0259oXRUZW78UmdcsgrZKU", - "Yc/ir2U1S04Zu+TTWO0fTaPdJaBm6T7wY3Ou+QwsaNO/lqde/1Wy/J1GNmo1ooUtaAy5VnORtkRAICnP", - "HM/YJGNXGdbHbifVfLLd8BPNJ8ujZ2oO241+q+awPBr9lo5NbBp87j78GRa1sWQ72DTwEr+qDwM7SApt", - "1EaN5BLsMX5YH50BCbi1A91HHoVrsRKrkTrBirqCYQ05XINv475p5tCRoLrK8moasG2cPBwkxrmrSTcc", - "08mJK7iz5fUsU3m8xHK3c6yBWzjBKttKL+4nPGcqhTWaRhpmZ+5DtqcS9FHjKbsMY7n+9Ycf9vvshIQF", - "yoJ//eEHVOK4taDddP/v3w57//rbH993X3z8p3iynp1Ggp5HRmWO21SbcB/iOwmPvrTIQf+fN7tm3Eqx", - "yzyBDCycczu93z1uOELYeIrLPP7GLyBB2Te53+5jbpizlaQCHRapnYQdZfmUy2IGWiTumT5d5KE/fQ3+", - "vPf7Ue+vh70/9377l3/ars7ECamfW77al4pT4Uu5XeAG1Z6+q8pstFQUwW6fA80tbJ7Sf8009haV7Kff", - "2d6ML5z4kUWWMTHG92IKFhJ0Uu9HF70VaQyhllfDz9buP3q1yxLoaRRuxzZblO1SySatOxpjCO7xUddD", - "D5dVlRP3yUq1tRHYWwAZNuIUbR8ZzLX12Ov4P+OZKhMyLabQz4QUM7fRwxhM1rbf9Kk4GN7DwpcrewtO", - "HEdaGuiG3F5mZWivmSllp/9O9ju0IKGpKZgQnMbtzjDiBlKMTMcFkb9kICf+HPyOzvHs8PDwsHauH6IH", - "e8grwx1hp0dGnFO+11j3hWXCoFr5t7suW/xWV+lzLrQpYReqX99ORUabmGAsyVun6nndkXHLMuDGsufU", - "oBfdi+VOl7dcD9Qqwzie4+VV/7F8mrU/EiwbOOzgGvH0sGkx47KXiRtgP8LvAmtm6jlU2IwQvuULOggT", - "0ljgWGM9ExK4dxXlKvNWrF8x7sGthkYCM8hBDwxMENOIHCAfIJENZgZtbWIiVbP2Ti0StvF540g/7EiX", - "ZTEQ3NcKBM9oF6vUsJE+V87ZfMUetj9jyy0hbtG+sDCjvy8fQodson2D7C1tjz1r7PXZ5uCCNuFemuG2", - "NYgtTbzO7HJKb7nzjC9ukQtvKwzizWdqr8NqSky+icT9pi32EqpGf/AffM7pn5S9U81Nz0z845QbxrFJ", - "uPv9u5xP4Lsu+85n7H5Hr8vvvNn0OzbnWjhx65+OszyDl+y6w2+5sGiN7k+UVXvfTa3NzcuDA6Bv+oma", - "fbf/immwhZas9jnmGu7tv7ruxEKCqEgUFQtIGnj4pxU8fEvc2p8RnzC+gXOIJg/qNROG/emwweG/b/D3", - "zbiGl78lPhjc8I7oELolLWFBdbpVD1zA8qU4e+wN6FHY6U3V/fi2jPEuC37Tq+9EihYmSFbxSbi5PUqL", - "3Sc2koKO7OcyBNdR08Iyrqp+sIglN1Wx4qjlZD6QYsvZqCv+Ol8l1G8b0kYj/bjjrJFM4xeIIchrkcGZ", - "HKtVfiTMIBV6/a5QfqEbsXzOtXTRUq1FB50on6FC4kMMQy2oMtUi5RZ6vibpahx8lO+4Y9HrdiSsz5jt", - "sutOqm/vdM/933XHPWyuOz1929M993/XnXg8Wzxq7kduoJEUNRbBKbp6E1u/ioPOuook4ncYjBYWInhy", - "6cPh8Oe+r28YtiHAbBEJF6IaOer1tcW6AQ9qMPSX3oZOFPLYkoT1uvTRYOblBNqaOG6Dfnw8pgTmrfHw", - "vrAsl7ovUHfDkrhZzOcoLXKo28COL06Prk473c6vF2f4vyenb07xHxen747enm6Rb0SpRq0KC3aeWfZB", - "tsD3RLj/Crl0hfQ1tcsyJKV/1sf3hN4Hnm//TPG8GKJVhcPzMqGGZ8zyOyXVbPESk+0oqd033qtmN1YD", - "n/nw5WHKLScXstIz1CyULGGNOoTbyggydcv2yMJNWyLTt4+UGLbfw7DLNEy4TjOnuaixW5jlxSgTmCcp", - "bJ8d8ywD3av+6C8AAybeX16xg3L3B/6nkOVXplSFuirC0M2+YgaADZf2Ur5HsQ+hmfIc+uwXnom0LHGe", - "4GZCrHw9lk6Y8oJDIkLiC6h8Z0KzneARRR0prSBOAn/G81xQc32ei4Fba4Nj+ygX7noIpbohOnSAsZuD", - "IPzXzuDDPS/dCNJWysnSfODDJzbNkebH9GF9rDvetsNPym/LGShGYuC1ofUT0LeoIS2Pz9Rku9Fv1CSM", - "rcVhkANwwwxn1ffoDInNg+6IbWf5GRaxOcgCX1ZJ2no6clc0Kn91O5mYw2Au4HZLIL8Rc/hFwO0SpKtp", - "toZ3mGkV6D7MpDbVxmO+pSEntRHLswkpbGhdvtVkZ1LYeg//aioNfpWd5rsIozZMuvN8q3PVQzm3meqy", - "/D7MVC+ttl0bx7M0g+XRSx3j79mavzZh6IS8c5fpxhy+f+Lu3Sk73da+VPfsABZmXOpSs3UrliY1rzYd", - "2b2nSzlNku9Q4b8cpXi6SynlMK5WDnTnUqurc+xwjy01EbsrBbF2rTVWSz4JpWR2LtPT6a5kv+9aWMAn", - "hrqXweIdau+koH7sdpSE7QOll+Xjx+4uw2pCecuBMRredWidcncbG2FCu01QccMtx8XweoehceaywwQV", - "Re4waAnjd1lumevsMjbwnN3Xq5P4vQBznxniiuHug0t9cPehEd1vy0laNITdRq/qZbuNX1F17jn8HvTc", - "ogxuObrxMtuWZS69o7YftqxKbzkyqtPvOPaeS7e9O7ccHhV3962JR/Xv3whj0cgWMUhpzRfu+b9q3hKS", - "rK2YkkYp9f1tU+dLE3LEL1yK20j1w0xNlrOZa62e10aML/etmZQeBQt3trXPSEs/hCsx8926yh1RNzPK", - "2N3WFt3ipqsvHbOuYYDFuY9mvSh1+2Vz/LZhtiGI7f7htW0zbB1WuxLNuFskyiNGZGB43wNjMVJhLJcJ", - "NBx0Pzx1BIbb804RGA8PS/BW9CoGwf2TS7t0i3HD+ib0rEI8AoYxq+6FptvOtBO63j9GMAVjB5tiHcFY", - "7C2vZOnh2RQq2O0YnWyamEqCbT3nsl8wLNCtnSJ2Q+9v6nxpB8fxX6i0Nnv/c9mnfZWvq5uNWHtGpfbB", - "BM9nf7PXU91Ez3LObTL1YYj3g3hbHOJJe/xhySievzjcPRrxpDUKsc/OxiFXr8sK47NMp2IyBWOr+qM0", - "JHBFDYg+Xsh6P9KfDrvfH3af/9B9dvhbfIt4td6gtgleYx+lpGHseAclaYnfgVhwWd/AKSFlAOqBBjym", - "MBj0PYc4p/HZXlXO02qQa7U6FckMmXC+jGZ1/uCDtIqBNAVVRuUpzynmWcJtqGFWhWogTuBdToGn4yLr", - "UiZl+EvWgp6t4Z8nrWGfJdp8//xwuyDQ5VyA+0neDQGaQeoGsUW1FRaGojKXW7HVUNSB+7BL33INzGIh", - "p80xYGsEaRnUPtskUW9gQbXgmHGX4yX69gI2vv4bH9roZjeL2UhRuQlcyDded0uExgIjYLz2LTNFXtUt", - "u0uVVSq7lnsGgP3ns2d4lsWMpTDGIt9Kmv0+84FOpqynd925wPCX606XXXfQJkH/PLY6o38dZf5Pr3+4", - "7vSvKbyRIuCEofjMBDfIM6PcLhM1G3mRZXxOAM33LzZETuB/4Wr/csVHOO0OF7rErfF2o/yaqgWd3kHy", - "aLFs3B1vhvGSC+n4iMSCxquiietJMyzyb5HaKTQT15Oi7Li4PVZxM9BKNYMa48comhWCMRPaDWW5FnOR", - "wQRa2A43g8InGa+fMjQmc1+7qWSRofQIPH41U5LOHolUwIsOZQLMFLKsvHInC4p4f6fkNlaxQWksY1w9", - "Vvd4PbJi38/ofdW0CDXsXD7AZp0L5Lwdvf6IxbN7mP3xcRlgp3IutJL48CjjFLE+rW+sEq+FVWH+Sqzh", - "buGF7QBsjyIkcG4kwweFEPI60ZUAK8+xSoRr34On5fnbHoPxOmNwJ+wgHrN6HiqthULzLWWzMaJwMPrT", - "i3hAUa04DH3KRsV43NLBiyIKt51MFbZ9so/t0PtZVOl+u4HvksrsI/bKsrtPDXubIKMKFg2m1rk6vXjb", - "WT9vPazJf/7z2Zs3nW7n7N1Vp9v56cP55mgmv/YaJL5AVfS+0oRqYbLzq//qjXhy0yxquhwTnZl4Z7yy", - "z0aismJGbebWxft2O1rdbprLfbJjkDrO2qWNrrmxy5zfyvqFbVWiKCK6V3uV+tqSMLB2sVkKHvmvGWe5", - "gSJVvfL0e+dX/7W/zFhJs0dBVIagzIEkUou4jAMttJFZBhw9aOqHwLCp5dSGHUC6spL77P7LfIx2SW3C", - "9R78/KxmMOYjx5A4M262dfQQrQz5/rIEVluHglB7Mzb8Eku49cpekpFORrX9lHbcohBpnBFj49cBt3E7", - "MVWHX+nX4IftYCpuJTXLbbFrH/rjWpGmwpCUbedKeTHIk1jbRGPFDOM2j88/sALt6TnoBKTlE4h2KV8j", - "Rqs+LaJZW3TKje90tI2OQgW2WyKfqx2HcsWhWjLtvgyKbpHgUXPLeQVT24i0rXqA0PbjsqgdsKmQ9xM6", - "J9xyx8lutSAD6BLqUdKBkHkRCaROueVbKRZpfZXNLTrKeX/beOYH6YtuOz7B07jpVk/ovrAg25CkygjD", - "D5j/vN/Z1qTij6KBV1Htu+hOl6dlVWoNuQbjOFStJZHPFlF6pfzhQ6FZOtYqZHGniKqgEPfTvWluaSX8", - "3JFCNNV3K9ZQMlKaXBh2jQOvO20k6/YfkQJkCPdh36rWKCSZFvKmWUEJk3fKlKAtiZjithH+D7NDjFS6", - "oMaaNGWoz0cXID11L4eyr+8FHssTKIsjstJGhnaKdC6M0ouXvtrdjVS3YXVf6SV0wCpbNC/VLuSFnSot", - "LGZXZlSyltJMTa0AYZ+dIUCp2ZzxJbUKSQsmhbEONxc5mK5DA7K9YgUu4jHNRhmhCG5VC7UbiibXK7dW", - "1Wgb9X3L8pqNKqFlpHkVebq2MU5bEUG6PE/i/Qd3wdmQ+7G+GfS2dUaotgToeO7XWEhMUthGDaoKSIRR", - "bUrQRnsS6XerfzZlJYza74005q2Vtmq3ftA9N7t0z6hM1vcZu/MqBukCJtvUcNrO7/STryAZ6nlMvBFk", - "TfWLFk/Er+iB2GWiLaMSaK7vjO+xPnbcX0t4UJzCDnNGXcHhFrrhYjeB7D4eFV0CekMhpiZiREVQs1zT", - "rl7qzPLB3XrHzk9Ki9+VxGJAuBbjM1VI22cUnuIezvh3wzAFuMskTHjj7w4OcclNO9hQ++MXt+Nki/VT", - "dSsjyxd5fPGHRGKUBaO2N+pvogpufYnMqqpVc6ndiWLnKbcOj1gp9bUj1xJpCnJDcjOFcVQ+Mj9oo4/f", - "f9ey7dcig3PQM4E9x8399o99xeKGN2o5Rnmjmv2lYb3YNUE5UoPrTy9e7O9Wckvdypifx+0Vf0LPTtjv", - "h5b9bpPMSnmVeXW35M4lzyG61NP7lsNak1xcrx23Y+8KXhiolxqgeuI5JI7209J3sKPzoe4Jx6JxMd9D", - "vahDI2jscCNR1hePXohTYV6bX7lNHrXCWVl+Ds0BWAkyXpbBEa6Yw2a7bUntfj5Wjs0WW8TytEYm4Q08", - "sE7aWPMZxCNvLirdNnzkQDzOHcXOQWuRgglvJX8D+3WYPz/cZASOmkTDgy1izKwpsIC090jV2nDTAaHP", - "5CUhcLvjsdpH3fEWAjDX387aC5nxO6wiIH6HM/n2x/YdYBRzaAX09sctIbJcPOtZS0CWO91RkQq1GbmP", - "fYF37j6nAmTYOHYuUlB9dkGIbOrvaqdn8Dm4hz+N8pF87il9XmQGjvxfkxuo6pBDSq2yMD2eGbCGjZSd", - "1mqp7/saURSk1MQXYWhHPfci729Zb//Sqvyh9KV0Am6ezTd5NptBKriFbIE9nzHKQRWWTTRPYFxkZYt7", - "XxxghmFxaCoUEuM6tC6whDweFXEk7ubZpToiAcxt6AlLI1ZZ/HIOmcp3jbW8wgp0NJSV7hCLfTVr5WLY", - "UgWCSO+MYAhcW+C0WQcCi8f+o9WW3pspqaySIimDrxg5Eaqd8kQrY8pG6/WOhr5ND/tgfF/QN9zYHq7c", - "Ozvx0YWFD+K/vDwNdkBv/hSGKsWRRWmlg+8O7lJ3xmAp/W0tDNuSHpYKYFDpq1uhoZfBHDJvS8KiDVgI", - "K68Vx/CQYyBTPA9yi1BAw5fAqE7fZ0d6JKzmOtSx8Ool9XbxRTGqEhCOgaU0WZ+9Xmnita5SRzdWYgN3", - "DLqHNitCG5aqBIOkoOwnO/RGsH/2tSsOlv5ygvPWAuC6bLVAx6bm12vNo1+0kbEC4X9cvn9X2hhj8MmE", - "8fe6vlAJ1W0id8QyvJo1u2OQIEC6C3+6Zt+XYAOWeXFZuglae39bx+t9v8yy//f27b+x13ej+3ej8Xej", - "ErJ/serQMJx250Ncd+wR/rRG3hL2l8HTeQ+fcltLldVgyTzPRIsV9leeZb0kU8kNXVllrqhdZrMZj4Ov", - "n5LydGwoz1jtaKlPy/YO+K5v4bFz5xXfb+Xe4s5LtIwbuyKJq07ZGgzYIBGb10LP63gv+h2elv74dI4o", - "7ixVMN/Z3viwOr83sDBWqxsw0dqc0eiXeP3Qe+VFhYDNah8hL6yWH+U40R22Pc74on8tG0xCF8D2Qr/b", - "WciIO0hDleb9Prukrl5lQsG19BHgjgW4tagHumQqvAdr6zVuiu3h3/790N2LT9va71/LWr1YbELhbm2R", - "k5S4VTrFxs4p+Uh9SHF5ciGt5j33FS1orqXTGySnMlwoEOnnnBfGwekKG1y5vfmW8SZUn42CLtrzq9vS", - "VcOhIt4rtgUgYTBVGLZODS1ayqipgSOYBNbjIvY2nXIn4J2uv8gVE/LvvvmXe/W/YjNhLL8BUpRQTqIO", - "gnc24smNyXkCFRKwwz57L7OFZ2EmdgNsz4gMpM0WjXu6ltVniBv7dFXlE/aw/yyK9SEsZ9uOIr9qYaHs", - "gXI/Ql8PrUbASij7Fxa8byuUj9itk9yWWK+187LjtdEzbO3Jjs7POt3OHLSh7Rz2n/UP0UCag+S56Lzs", - "fN8/7H/vi97hQQ5CPtEB9UMi41gSsY69BT0BzA3CLwkF4E4YDOpQEkyXFbkTPmxp0khG0ly451kOGr3y", - "aZeIDAvSFtKKjLrDhq9PYH6lVGbYdQfVPSnk5LqDecuZkNjASo1QZ0rZCMZKh8qoaC/yqXOITGW/ubMU", - "7aM2mYZVXvt+UL5W0Y8qXVAwa9Ujp0rTPvi7IWssScyIKznc5pJ2EY5Ed2gVm+G1+kqdf7vu9Ho3Qpkb", - "Slvp9Xyvwd4kL647v+3fP9OENhRHq+o7R5+UbIZZi7jO88PDiCEf90/wTvFlVR7NA3u5XuvHbucFzRTT", - "PMoVD37kgSapYvTHbueHbcZh2QzJMz8KK8zOZtw9hTofCC/LLWa8kMnUA8Ft3u8Zh1XYW3YT20QVhQHd", - "Cx15qmUAy5hrYYBRZzZW2erKkJcRL3/uO6zqXsuN5MJ2p5ZruSu5HIPGyvPhFkJfVPdI8Q3ohRxrHopU", - "eixmp6Hx2qXvjd29ltjkuYelySEtZ6RzlPMHNESj7/HJ+UHITseOihrYyGnSkF5LtHCEu9xI2edVU7j7", - "EndcNMQ0qm2A32c/h1xA/5PkMzDXcs9nnHlpeqzUjQDj7/G6Q30lsfSzdz1Nyxnor/1reQnAQuFv6opX", - "7aQ/UWqSQYnYB+QSKvNlw999EBJl3Lnz/8iNSI4KO30/B/2Ttflp6DRLdxDdMJqW3MfmQz7RPAVTjvJC", - "9S2/Oy4tCeYc9LnDk87L7593O+cqL3JzlGXqFtLXSn/QmUHn52pR885vHx+LrwVc+WpZ2zLaubO0c7gi", - "zxRPe1WvxB6XaS9869ieMhFF5wMOo3Kyms0cBymnYL+LnHGdTMXcUTjcWWxUaKcwY4VMQbODqZrBAbGQ", - "qlelObguDg+/Txwp4L+gey3de1A7Hjerr0B8W8h7KBol57yWn1DRoPsqGaM5kumFv+N1PGlWZFbk2ONT", - "6Vkv2MradI5ax8vWhN3qG6d8EPgpQDKxYs5to/pGc/p4EenXKnMwRfe6VSzPeAK++HsA125QX3IpHPX+", - "ynu/H/b+3B/0fvvjWff5Dz/EowB+F/kAG3mubPGvFUKGdio++rSQOeUyVeRT7noPO+2FZOMZl2IMxqKI", - "3q9bIUZCOkrcpNWX2/PVuGMvk7UKXA2699PinsUikktsIFSAtBvhdkQ1JXFgtCpPPzffW2FBJTRrSL7H", - "jWNIZr/OBMsjem7o39IHo6DjxbneacijlkwttfhZ6i9pyC3nm08enZ9h6ek+O/K/ouSncCWnzpC1zArf", - "U1lkgOFYIUT6LskK45DXqT/YPl0qpjCwAJMfqi7ahiVcko0Cu69jf5AQ/WGsyk0wIoyFNtZ3fwitK8PF", - "M1HWHSFrZWhJSW15r2UoUF4YdE5iz+Cpp6oUKIPLvQsrOyAm51BBHbfaDSyoR6i/rmsZPJ45X7hZvCOC", - "aVXItGe1yJlTHWVCMeSABQZkKuYiLXjmp4lx3h9REWz2EL2/GrjWZrq6UtUG8X7KCE7Z0v7ic9JeSQjU", - "LzVKAHWcXiKzpfakgdiagKsakz4RvCKdT+8JJuoVF/q6BrL+rBC6FLMio4RRorp65+a4IXEFRmSuOnCs", - "vh1MF8DT45ppK3ZbjwWuZtNihNbS26vsPeyXRDm1QjcPvl13aLIsl5lGK1a+tutE22D7fTaNk0+E+nEL", - "6H3RH62ePrsMG5qWUPhiGNavZJANxvQt4FW2A46DqYwOfiIIrTYa3ho4j7J+rfRZjM4ocHkuQkuM8rX8", - "xUD8J5H6Iizqtl7fsQnmZqPruNaHtaVQa8EQ+cBQqSNnt3RSOc2Nh6qKblltySuEoQdyuUvnRMxDI0RS", - "TDPgBlC3qveX2tBCMqbxlA1Rnwg1V1t+35NvuIm+EHGJW6kqZxKYOMJhCWMmYAlhBmUn/lYm8RewjSqn", - "Tyke4+VU47SLUQd00vIQj3GLfwHbCGzwmgcxi7DSNspHs4N8/HLLaqtPhOarvekfpB36W3An+7yo/jYU", - "EW1AJ0jFMjWg4jRmG4g1uvav4aMhHLhcB934yDNr/v4yL4Hs5FWCTK3c3LWMFZGjEDEsdJZrmIKkd/Nq", - "tbouMwDX0m0mXnGOcVuZ0SfC9scaIAVzY1XeV3pycOf+X66VVQd3z57RP/KMC3lAk6Uw7k+Jn/twrqmS", - "Spt64IePfgzndS9qH3Wf+KvA/ArjTWgEBZVGPR6+BOITkcNyhcX7UgMCFLHlS9IWSMbXbUmIl1sgfr1l", - "TxuruuI3cFkPjXwSjXElZfOjh9FaiYOBrAc5pRhXK222bq4IlmoDFB37WQFa5iawCkAhCG0DOFWWtTMx", - "SkZlc5+wSVUADpSj7ZBE6v5mazpejZM2tcWGna9Rx9OrgY1sUN/WWrJMTTBX1IrkxrA9qazPVCYTZw2D", - "2AimfC4cSvMFm3O9eMVsgVY638W/VnQAY6YwraI6CrkbQ3IqprJ626V3dXcbRRN8yA96ehomzb1yDlSF", - "qwX2Ke4DrUgULBRiwQMrHIbYMDJg9HoacuCWvWO9HgVdHTLyIJBCTj6EYYxDXoac0Cciv1qW8n25o0ev", - "L8SGRJupdAUCD7dOM95BmwtBvy3M0QdcPhFcluM5H2TkoCDCL0ZqubORUWMdFHyMcDtPq2oJB3cjc/+P", - "wpAXy+HJyLVKF5Gx3CloVuU5JmMkwPYoIKF7Lb1PtvLGdB3jwPw1747r1nQ+Xw7aiN+FnOz7V3O5kCiL", - "jTG444nNFtcSl2t4pjTwVEgny93r2b3HMYo6rDGkEtqFzoa4nmc7nI3A2B6Mx0rba1n1IysLZ4dZg5fC", - "zYyKmnvY8AkwSk/40fFGB4TQxFTPeIahplZdy2FQJ4e+AQOXC7xptlAFSxWGQEtwOz6yLAPulFYZDMsU", - "n+G+Rr/kCJgvqdS/lhchcKYJK+r9rwtZVjxGt9XLWvxNHTYeAl1yr3dROZbLEOtHQYLFbggcJPpAphQY", - "WybtUMz6tbSaSxPU25dMjBlH146uwn/cvtHZ5DbIdebEYkV0DJMMARsTh0y4GRfS4QOuTYHACXhcdX+S", - "Svae3915f1euVc4nTiD3r+W5hjGq1u56nBgzkHPMeB1W0QX/PKTkoQN/R0P05/noViKbDIJ3sWe1mEzA", - "6UnXkmBAlCQkwtMnsFbh+zFhFW75uKTfRwwUoLCgQT28bSm+4+p179987k0zdonNeM7+z//63wxjvA3M", - "uLQiwSLK50dXxz+x1ei5eM1j/9WgJVCytgPycbPhH9cUxHjdeVmPk/zt43DLDeHo6G48WLfZxswxDdRM", - "4u+k1T4LQ7aHJVcOqODKAdikHxJWqd54CKheRSAKKTfd4J/FtN8yQWSZG4uKFTfClhqU2iTSaEm0NXEk", - "p/UwH4NWyLD7xEmspMDKJNUUfYwMoWNUmQFr4472+5uDUB4cIvL08RsYM+6GDDzvXL1Ny3X/d2Nj0SmY", - "9gUGr3fYiJ3BYFOfxuiZs2cFps88OwvxV75kBRZM9w2uqsBBP9j9P3NQa5yPGryBzI3fQ3c7hdqxoQ/z", - "O6BV0LE/3KcE1aG7t3xQkcSQpAKySAK3j2cIh7VTXsbXGCfv8INbzfMcVhr5bwSXL4flhHuEjC/elN4f", - "L97BC/eKC68V36UtqMsykBOyzyecaM2y54cv/o3qLHYr0nMATDDYl8IokEd4ANAuRhm01MVu3uUapa1K", - "sAo3iN6DaizlcGuRk7NyCSdLrNhzMrKsLOQzibA2PtwRRW5M5v6iXFQNTcjzy1eVulligZs5g2XfVf8h", - "iv2Lwz9vHuc2mIlk5TnwOM7yZe0hPB9a7wlQ4XL/i7y8jOlOWT7leMX1l8cR6jP0bE9LhQaf8j47t6mJ", - "5llhVu4+VPw6qEnfMso+Es7tpepTGTgjDZI+MUb71UOy5SqwPngva3grNS75s2Hsg2OXW47jUGNsDhIN", - "3MKg7IOBaFLEIobww7KIz1OFDTVX2QlVnq2rOUTn/ILMC3RSxjHnq7r+AJcUHNvcAi4n+OFTw4VWqTe0", - "u7dfugQJHTF9GGW92DzunbKvVSHTR3Ro484Zb4db0IPXgOw1qbtfNrSwotz/AEAhPEoYqVvpNGZHXYPf", - "BZYQmoCNVeqyhZaGcfbXs3NWvgVqb4jwNCiLylTV3wJq9FdjSPz6J0L/VeQYka/5DCxog+0v2ho+lpSD", - "OqhVpa7vVINwKHzduXH/KADZAb3pQh28Jg5060aMTXX1fttJOPt7fZDTy916OGNZOwkRq37BXyNeemDV", - "WYh7DRCihQdtHF+NTbdA2PD23bNc1x7As+AcRj3UzbW/Fq+v5RrEZn81NmVqPAZtmBETKcYi4Zh6PuaG", - "nn+0oNdfr2UK9T+5f3NNL8DfRe4NLjyZCphju1ywy7MgGcUjs2pU5e7oayGr7h+rzd/K42IEQ5/9JCZT", - "0PRfZQ9pZmY8y+rmiFFhmeU3wDIlJ6D717JHkDD2JftvB22agj3rMp/47wALKdv77+8PD3s/HB6ytz8e", - "mH030Bc2aA78vstGPOMycaqUG3mAEGB7//3sh9pYAlxz6L92AzzDkB8Oe//WGLSyzWdd/Gs54vlh70U5", - "ogUiNWwZ4DSdOjiq1lHhX1WlJn9VnW7tN9oy/sPEWhLsyhU99T6ILV4t2bX+f8Ial8x5JXtEg0uo3eDZ", - "YpM1lM3kt+UJyAn8ta70tf9SJOxuOmHVUH8VoVDLq3Xr/wrR5i9g6yco20etQK9Em0wYi3q6acWbN8Jg", - "vWdzT2HydWJKdeoIqlTPt4xqk3yFuILZugh5SiRcxQ1slN/2fAut3Z8wNPYxnm4YilqZO75COOEJsJk3", - "ernWEbMGnpaP7igtXwBP/ZN7O1LGxYJK6Ob/UqhZJRZsr2pa9CBdAll/NI/rK0MWzBpruOtK5DBAjH5Q", - "qy3fSt2rJf6fLgmppZfAvatr1Ern+5ShrxCQl2BXCb3eFuAA2w6YqchLCJMHtD0IC+ucmJqj1OeOK13F", - "l5BA8KH6GmbK8wDKZeu3VJ0I6sGjRY+UGkmLiz4FYwcb2im4b3yf9ZKD+appXqHdppFCt3Nfb7735Fdb", - "3bkcA93Co1ViQCiVRRi+dlYXKc4w9vpanRyCaXNtkRmOhheKQcOO2VRPRlhT2TZX0leW8auNOMi6+Wik", - "sSvqp/WOE7VKOVWMhNqODh4psmUdPdwTsf8q8gqtawD8H4PkvF7waAlFV/DdG1c2IPyuptE2uriWmwlj", - "s4m0YRG9lksm0fZyR97G+WjE1RpFdTWFZdNLKUK2iBv6bEQbj/JpK9b6bvtAH9/Gy+8NixlheV+HTr0e", - "ftOrxu33d6uhHODwJOziyN/h/3CWsYyuLWzjdrkg0dJLoNYI6aneAJFeS9vD9p7FU/HY0bbnH6T4RwGx", - "BkEVVd7669gqXm25XrtNpuyxa/x9JmSjw9SN1L5Qk5zUNDG8rYM/wpV/9GXMgYqULOObyit0WzJSoOHB", - "Wxq83aGE4zrbw2ZTw4tYYX0CFAU7f+WAusSWPyGuPGbtWwbSAeXItZqSqGv3a3NKn31CWC2bhSzcWdpt", - "1B60yR9wiU9b32wnknNaNb1R49pb2OcQYpNTnuKp/+j8Z+/y8rTnywf1rqKtKN5CKrivtj7GrjLYesOn", - "JO4tM7H9hucueOlWWF3EKffxa0RT6i60fMu+5Amx3RJj3WN+fZARFuXZxuB5UlO++Irx8xP6vd9XDQlC", - "G8vWDpaN3il/evGibZvY9rFlW2v7XhLxbSPxH2iOvac1oywJ9bWLUTRLOckZ4iGrUK1MTcxBdbFxF52a", - "GCKdFj68hBC+u9A6zA2MxqN4Vd822tU/vsxYZZm6jUceNFp/1/rkLYMZEzzKtD0xDu38hGF+a2sIs12q", - "7LJO7ezx1aoPBjm1qel8Non2Rk22FGUOsb5o6RWTDG7TlEN5eXlKBJJnfHGrKe2NikZuUV61bP51Xo5m", - "iWO26AsdazDTWlNbBM2dZXzChTT0Eg9ZCLqQWMJZKskylfBsqox9+efnz59TdirOOuUGe84ZZNXf5XwC", - "33XZd37e7yih5zs/5Xdlp5hQpcH3YfSxGDhjtTkslWsLLavWbwG9YoYTfwXVuY9JOjzFy25lrc+U9RDZ", - "h7vQeLJKeblfYjnU6ghYduASd04YEUFOTyDEk5A62h/6vsGWW+jJ6vuUK3wmPGjsoA0DqmrG2n/zRZTB", - "TdRs5riEWchkqpVUhQlVbwOATc5v5UYIX+JXTwpiXOLzwthvoQ3I+PNnLn6yClu+Brh/+H/g2/xGNCsI", - "RQH9s8BSNJvf5dXMa1XCUpMvCpE+5LFwL4C603yRlUrf//xVxhc4ViIm7qVpFQtqazvGUWGAjTh3QZ/9", - "j8E6Os83vHu8ACWsL8HZ+dV/9UbUSmEz8hnLbdFuigwsn7761Lj3xHKMDhUTYf6XrzJK2QOAmXC8dtCn", - "YgudBr/6H8N18DifWX+iLbTpTz8usHUHmd++WotbJfkY4dlaPFSF3WSIqy5PFXatRe4z8aMHWJbKs7lh", - "W9qYwu2qwuYFddXPxBiSRZLBNwfK0zlQalitCrtkMNOQYLnQyUHlhI1zV8ocvgjfP2midrnK5tqyy+me", - "fuDnS9H+TLUtysTuXMNc4JuREXAhZXORgqr5EWpQ98llrVwsZJ/VAb/We1Y6rfzqut5kn6qQ+Sb+jWqu", - "RajV7b0C5fA2RxYyvbgbi/d+P+r99bD3595v//JP92KNeGEHs/zFg9MJKoz0MY8NBlf+2nstJDap7x3F", - "Gj2LGRjLZ7ljctScHy271dQ0uM/+UnDNpQWKlxsBu3h9/P333/+5v94D0tjKJcWj3GsnPpblvhtxW3l+", - "+HwdYWNxOZFlTGCxyIkGY7osx34WzOoF2T6pxmPzui/A6kXvaOx+WC2FW0wmlCuKbTWwA6SQrGqYH7ov", - "6gURQXWIMpbtWSSW7eNXnHBKpXgN0iI1UN+Co2SCpEdr/uCFJ2zz0P4UZT7AOoESVqNMz5Ug+xV6DY0r", - "dbnLR0uw41lWn7Z5bSsdUCOhd08tfJuLrJW9z9aRqGcCX2GFKLyBsop7xdf67D2VnK3zuhw0OzvBFohY", - "23wijMUujViy2nGQ/iqUVb4OyCp/ehjX1ri/euVD4T5vwXCr8qb4oes2Cc/Aqt9BqwPfz35tmxB6K7iJ", - "fnlLRQvdDFj4QzE3S9cBl+s0w+fLmP10dXXOrObjsUiYkkzYPjvmWRZqhRydn1GJbGHclLdOWt3yG2DC", - "shEkvDDAPkhxo/nY0q+h83jiGzvdgG9SsghFDELOyS9vo6U+6JiX7uRX6q+gVWebsEb8vmdVz52S+btK", - "HwU4ZynMcmVJbPiZ8V4h3GrtivqrgAO5Hm4XYKzSYHzZTJq6PErZiaBao+v4r7pFFQJvs7kZ0hpQoxFp", - "BgRQGluqOb+8ZVL5UiJYOdt43WYKWcq4A1vUyy4fDhuQTwQamngTZCxkMHO6z8ZCO/WGTOWoZqm9Pgsf", - "vzh8wcS49h1V7a6KpEZbz/wF7FW5nye0fpWLXFpuo2b3q/gB76u7rXa3ap+/rFy5xM649k0wKN+VANIK", - "CJRqCbcwoUq8cOcuSzjEMFg/ol5HhY1UusBqshTUnb4KL7n6FBosp3FCl5hgqEO/2Qn0zPf1ZzCH+tYd", - "vvoFKSeGaOMlwy7/LMmAaxOKNtVOG+ti5G6xiUxP0KmXAjDKZeoFNz+dLffe2PwVZ077gp/ryKiIdd0B", - "u4FuAhY/P3zWxOJbTmhcs8JUGP3KB2e5cYdunLBugEP0DJIQwKVy2xPyJeOV6J9y67HczV6ntj2+VAKX", - "kvWkshjc74jYKQ66gC5TOlBSIJ4g8fdbieYVCQL3f6XU8AJxN6Z9XtjPR2dfPF3tmr/0NBsy8HkDnC7X", - "ibyGGlJLSImrh2fy79ivg0vyBTARclOrBcg10WUT7pv6YVJlggXJl7dRJ/lDojH82hgxkZAykHPIVA6V", - "suiXNYynwdz5/PBF5PexyOjZuCdVWD6UuPbJZvjtd6YiXGEq2kXCfnF46LS2Oc9Eysvu+S3NPs6LUSZM", - "JffIgfNEXkxaC5f4TF7M6pweSNFIPwRHTrt1rLqEaMJ16FJQwZv6jSXQJ+qN6O80IU8SyBG9CltBej2u", - "vSIJErbygNrwzaaDNOEWJLFMbCuOzmWLP0hqHjwH1vT5VTMTwfbZKU+mbKz5jAKhsfyG0jM2FOlL9oeB", - "f3y8vpYpt/wl+yOAoOfg7f5+fS2HTlrS3fteBGWLuASM6c2UVFZJkaCDMQdt0PSWaGXMErvzqYmvGGdv", - "uLE9hFjv7IRsANgtyUtxN1BWEhqpDB/oGkwxC89+OnafnWiV06YoqIoAPuG5CQr1UKRD6lGCHYm8DQPE", - "HFL6TRiqYmGnXLJnjE+BpyHkO3N7NQASP+0GX+ctaMcoBOYtl33iR8V4DLrPjjOBX/neplbz5CYym1MW", - "UrCQWNxvn73G6Pfq+CboF0tXhia/atlK7/egcsDAtAoDgAW+adevWM6NYcP/W0Oe8cW/8ywbUl55YzqV", - "pVj0Ep8Wjtt6/DUWuG/8dCvcfU95jmka2A4RJGiRsGGTzw2pZ2vQmvztgX/IeMr8GVufUGNJtuc+X2AL", - "Jodt1CiQs1QlxQykGzW0ixyG1ESsZNZD6pnicE7pWVk0pGro4/WVf8ZtneDHxLK6zKBCSPuhyaMdBhHh", - "msfbWJnvwqFs6EaCyp1p0pPvFqY0MyBTdhiBRwBvaMu3LU12mVFNwprzrKCchhk4MtMaEqwFQUtxt4aQ", - "ts+u+A1gJ9cEUlwI/dhDwpshiVVsJ0kLY6syXM4xJF5Y1dPg0bhaLgMusVEWIhKZ/Xs0pYPQVBgsb1lV", - "ViVvUuWEbBDBbklG54j4uyB8n11gDWAkaZY4fsIte3b4/MUrHFAiM69xAgwTL/SYJ0BFQ8dCG0vEPsEM", - "M+25TL+1gCzdSDx0IsvuVwP2AcEnW8nzN1sIo68u42n5BA6il+g/7106evQcwI3+/wIAAP//guav5CfA", - "AQA=", + "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/server/lib/recorder/ffmpeg.go b/server/lib/recorder/ffmpeg.go index 5308d30d..1f0c736e 100644 --- a/server/lib/recorder/ffmpeg.go +++ b/server/lib/recorder/ffmpeg.go @@ -41,6 +41,18 @@ var ErrRecordingFinalizing = errors.New("recording is being finalized") // can use errors.Is to surface a client-facing error instead of a 500. var ErrInvalidParams = errors.New("invalid recording parameters") +// ErrNotRecording is returned by Mark when no recording is actively running. +// Callers can use errors.Is to surface a 409. +var ErrNotRecording = errors.New("no active recording") + +// ErrInvalidMarkerName is returned by Mark when the marker name is empty or too +// long. Callers can use errors.Is to surface a 400. +var ErrInvalidMarkerName = errors.New("invalid marker name") + +// maxMarkerNameLen bounds marker names so a chapter title can't be abused to +// bloat the metadata file. +const maxMarkerNameLen = 200 + // FFmpegRecorder encapsulates an FFmpeg recording session with platform-specific screen capture. // It manages the lifecycle of a single FFmpeg process and provides thread-safe operations. type FFmpegRecorder struct { @@ -57,6 +69,7 @@ type FFmpegRecorder struct { exitCode int exited chan struct{} deleted bool + markers []Marker stz *scaletozero.Oncer // flight coordinates concurrent operations using different keys: @@ -394,6 +407,9 @@ func (fr *FFmpegRecorder) finalizeRecording(ctx context.Context) error { } outputPath := fr.outputPath binaryPath := fr.binaryPath + markers := fr.markers + startTime := fr.startTime + durationMs := fr.endTime.Sub(fr.startTime).Milliseconds() fr.mu.Unlock() // Check if the recording file exists @@ -409,15 +425,30 @@ func (fr *FFmpegRecorder) finalizeRecording(ctx context.Context) error { // Create temp file for the remuxed output tempPath := outputPath + ".tmp" + // When markers exist, build an ffmetadata file and inject it as a second + // input so the remux carries MP4 chapters. A failure here must never fail + // or corrupt the recording: fall back to the plain no-chapter remux. + metaPath := "" + if len(markers) > 0 { + path, ok, err := buildChapterMetadata(outputPath, markers, startTime, durationMs) + if err != nil { + log.Warn("failed to build chapter metadata, finalizing without chapters", "err", err) + } else if ok { + metaPath = path + defer os.Remove(metaPath) + } + } + // Remux: copy streams without re-encoding, move moov atom to start with faststart - args := []string{ - "-i", outputPath, + args := []string{"-i", outputPath} + args = append(args, chapterInputArgs(metaPath)...) + args = append(args, "-c", "copy", "-movflags", "+faststart", "-f", "mp4", // Explicitly specify format since .tmp extension isn't recognized "-y", tempPath, - } + ) log.Info("finalizing recording", "cmd", fmt.Sprintf("%s %s", binaryPath, strings.Join(args, " "))) @@ -449,6 +480,46 @@ func (fr *FFmpegRecorder) finalizeRecording(ctx context.Context) error { return err } +// Mark records a named marker at the current time. It returns the stored +// (trimmed) marker name and the offset from the recording start in +// milliseconds. The offset is provisional: it is measured against startTime at +// mark time, while the authoritative value is the chapter start computed at +// finalize. +// +// Returns ErrNotRecording if the recording isn't actively running and +// ErrInvalidMarkerName if the name is empty or exceeds maxMarkerNameLen. +func (fr *FFmpegRecorder) Mark(name string) (string, int64, error) { + fr.mu.Lock() + defer fr.mu.Unlock() + + if fr.cmd == nil || fr.exitCode >= exitCodeProcessDoneMinValue { + return "", 0, ErrNotRecording + } + + name = strings.TrimSpace(name) + if name == "" || len(name) > maxMarkerNameLen { + return "", 0, ErrInvalidMarkerName + } + + now := time.Now() + fr.markers = append(fr.markers, Marker{Name: name, At: now}) + return name, now.Sub(fr.startTime).Milliseconds(), nil +} + +// chapterInputArgs returns the extra remux arguments that pull MP4 chapters +// from the ffmetadata input at metaPath: the second input plus the stream and +// chapter mapping. When metaPath is empty it returns nil so the remux args are +// byte-identical to the no-marker path. The ffmetadata demuxer must be named +// explicitly with -f since the .ffmeta extension isn't auto-detected. -map 0 +// preserves all streams from the recording; -map_chapters 1 takes chapters from +// the metadata input. +func chapterInputArgs(metaPath string) []string { + if metaPath == "" { + return nil + } + return []string{"-f", "ffmetadata", "-i", metaPath, "-map", "0", "-map_chapters", "1"} +} + // IsRecording returns true if a recording is currently in progress. func (fr *FFmpegRecorder) IsRecording(ctx context.Context) bool { fr.mu.Lock() diff --git a/server/lib/recorder/markers.go b/server/lib/recorder/markers.go new file mode 100644 index 00000000..3b98f6a1 --- /dev/null +++ b/server/lib/recorder/markers.go @@ -0,0 +1,91 @@ +package recorder + +import ( + "fmt" + "os" + "sort" + "strings" + "time" +) + +// Marker records a named point in time during a recording. At finalize the +// markers are written into the output MP4 as chapters so they can be scrubbed +// to in any tool that reads MP4 chapter metadata. +type Marker struct { + Name string + At time.Time +} + +// sentinelChapterName is the synthetic first chapter covering the span before +// the first real marker. ffmpeg forces the first chapter to start at 0, so +// without it the first real marker's timestamp would be clamped to 0. +const sentinelChapterName = "_recording_start" + +// buildChapterMetadata writes an ffmetadata file describing one MP4 chapter per +// marker and returns its path. Each marker's chapter starts at its offset from +// startTime; markers before startTime are dropped. durationMs is the recording +// length and becomes the END of the final chapter. +// +// ok is false when there are no usable markers (all dropped or none supplied), +// signalling the caller to fall back to the normal no-chapter remux. The +// metadata file is written next to outputPath with a ".ffmeta" suffix. +func buildChapterMetadata(outputPath string, markers []Marker, startTime time.Time, durationMs int64) (string, bool, error) { + type chapter struct { + startMs int64 + title string + } + + chapters := make([]chapter, 0, len(markers)) + for _, m := range markers { + startMs := m.At.Sub(startTime).Milliseconds() + if startMs < 0 { + continue + } + chapters = append(chapters, chapter{startMs: startMs, title: m.Name}) + } + if len(chapters) == 0 { + return "", false, nil + } + + sort.SliceStable(chapters, func(i, j int) bool { + return chapters[i].startMs < chapters[j].startMs + }) + + // Prepend the sentinel so the first real marker keeps its true start. + if chapters[0].startMs > 0 { + chapters = append([]chapter{{startMs: 0, title: sentinelChapterName}}, chapters...) + } + + var b strings.Builder + b.WriteString("FFMETADATA1\n") + for i, c := range chapters { + endMs := durationMs + if i+1 < len(chapters) { + endMs = chapters[i+1].startMs + } + b.WriteString("[CHAPTER]\n") + b.WriteString("TIMEBASE=1/1000\n") + fmt.Fprintf(&b, "START=%d\n", c.startMs) + fmt.Fprintf(&b, "END=%d\n", endMs) + fmt.Fprintf(&b, "title=%s\n", escapeFFMetadata(c.title)) + } + + path := outputPath + ".ffmeta" + if err := os.WriteFile(path, []byte(b.String()), 0o644); err != nil { + return "", false, fmt.Errorf("failed to write chapter metadata: %w", err) + } + return path, true, nil +} + +// escapeFFMetadata escapes the characters ffmpeg treats specially in +// ffmetadata values: backslash, equals, semicolon, hash, and newline. +func escapeFFMetadata(s string) string { + replacer := strings.NewReplacer( + "\\", "\\\\", + "=", "\\=", + ";", "\\;", + "#", "\\#", + "\n", "\\\n", + ) + return replacer.Replace(s) +} diff --git a/server/lib/recorder/markers_integration_test.go b/server/lib/recorder/markers_integration_test.go new file mode 100644 index 00000000..a2e2547d --- /dev/null +++ b/server/lib/recorder/markers_integration_test.go @@ -0,0 +1,119 @@ +package recorder + +import ( + "context" + "encoding/json" + "os" + "os/exec" + "path/filepath" + "testing" + "time" + + "github.com/kernel/kernel-images/server/lib/scaletozero" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// ffprobeChapters is the subset of `ffprobe -show_chapters -print_format json` +// we assert on. +type ffprobeChapters struct { + Chapters []struct { + TimeBase string `json:"time_base"` + Start int64 `json:"start"` + End int64 `json:"end"` + Tags struct { + Title string `json:"title"` + } `json:"tags"` + } `json:"chapters"` +} + +// TestFinalize_InjectsChaptersWithRealFFmpeg records a short synthetic clip with +// real ffmpeg, marks at known offsets, finalizes, and asserts via ffprobe that +// the chapters land at the expected millisecond offsets. It uses lavfi testsrc +// (not x11grab) so it needs no display, only ffmpeg/ffprobe on PATH. +func TestFinalize_InjectsChaptersWithRealFFmpeg(t *testing.T) { + ffmpegBin, err := exec.LookPath("ffmpeg") + if err != nil { + t.Skipf("ffmpeg not available: %v", err) + } + if _, err := exec.LookPath("ffprobe"); err != nil { + t.Skipf("ffprobe not available: %v", err) + } + + const ( + fps = 25 + durationSecs = 4 + ) + tempDir := t.TempDir() + outputPath := filepath.Join(tempDir, "rec.mp4") + + // Produce a fragmented MP4 the same shape finalize expects to remux. + genArgs := []string{ + "-f", "lavfi", + "-i", "testsrc=size=320x240:rate=25", + "-t", "4", + "-c:v", "libx264", + "-pix_fmt", "yuv420p", + "-movflags", "+frag_keyframe+empty_moov", + "-y", outputPath, + } + gen := exec.CommandContext(context.Background(), ffmpegBin, genArgs...) + out, err := gen.CombinedOutput() + require.NoError(t, err, "failed to generate test clip: %s", out) + + start := time.Now() + rec := &FFmpegRecorder{ + id: "finalize-integration", + binaryPath: ffmpegBin, + outputPath: outputPath, + startTime: start, + endTime: start.Add(durationSecs * time.Second), + exitCode: 0, + markers: []Marker{ + {Name: "intro", At: start.Add(1 * time.Second)}, + {Name: "middle", At: start.Add(2500 * time.Millisecond)}, + }, + stz: scaletozero.NewOncer(scaletozero.NewNoopController()), + } + + require.NoError(t, rec.finalizeRecording(context.Background())) + + // The temp metadata file must be cleaned up after the remux. + _, statErr := os.Stat(outputPath + ".ffmeta") + assert.True(t, os.IsNotExist(statErr), "metadata temp file should be removed") + + probe := exec.CommandContext(context.Background(), "ffprobe", + "-show_chapters", "-print_format", "json", "-v", "quiet", outputPath) + probeOut, err := probe.Output() + require.NoError(t, err) + + var parsed ffprobeChapters + require.NoError(t, json.Unmarshal(probeOut, &parsed)) + require.Len(t, parsed.Chapters, 3, "sentinel + two markers") + + assert.Equal(t, "1/1000", parsed.Chapters[0].TimeBase) + + // One frame interval in ms; chapters should land within ~1.5 frames. + const tolMs = int64(1000.0 / fps * 1.5) + assertNear := func(name string, got, want int64) { + assert.LessOrEqualf(t, abs(got-want), tolMs, "%s: got %dms want ~%dms", name, got, want) + } + + assert.Equal(t, sentinelChapterName, parsed.Chapters[0].Tags.Title) + assertNear("sentinel start", parsed.Chapters[0].Start, 0) + assertNear("sentinel end", parsed.Chapters[0].End, 1000) + + assert.Equal(t, "intro", parsed.Chapters[1].Tags.Title) + assertNear("intro start", parsed.Chapters[1].Start, 1000) + assertNear("intro end", parsed.Chapters[1].End, 2500) + + assert.Equal(t, "middle", parsed.Chapters[2].Tags.Title) + assertNear("middle start", parsed.Chapters[2].Start, 2500) +} + +func abs(v int64) int64 { + if v < 0 { + return -v + } + return v +} diff --git a/server/lib/recorder/markers_test.go b/server/lib/recorder/markers_test.go new file mode 100644 index 00000000..8f959017 --- /dev/null +++ b/server/lib/recorder/markers_test.go @@ -0,0 +1,228 @@ +package recorder + +import ( + "os" + "path/filepath" + "strings" + "sync" + "testing" + "time" + + "github.com/kernel/kernel-images/server/lib/scaletozero" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func readMeta(t *testing.T, path string) string { + t.Helper() + b, err := os.ReadFile(path) + require.NoError(t, err) + return string(b) +} + +func TestBuildChapterMetadata_SentinelAndOrdering(t *testing.T) { + start := time.Unix(1000, 0) + out := filepath.Join(t.TempDir(), "rec.mp4") + markers := []Marker{ + {Name: "second", At: start.Add(5 * time.Second)}, + {Name: "first", At: start.Add(2 * time.Second)}, + } + + path, ok, err := buildChapterMetadata(out, markers, start, 10000) + require.NoError(t, err) + require.True(t, ok) + assert.Equal(t, out+".ffmeta", path) + + meta := readMeta(t, path) + assert.True(t, strings.HasPrefix(meta, "FFMETADATA1\n"), "must start with the ffmetadata header") + + // Sentinel covers [0, firstRealStart); markers are sorted ascending and the + // final chapter ends at durationMs. + expected := "FFMETADATA1\n" + + "[CHAPTER]\nTIMEBASE=1/1000\nSTART=0\nEND=2000\ntitle=" + sentinelChapterName + "\n" + + "[CHAPTER]\nTIMEBASE=1/1000\nSTART=2000\nEND=5000\ntitle=first\n" + + "[CHAPTER]\nTIMEBASE=1/1000\nSTART=5000\nEND=10000\ntitle=second\n" + assert.Equal(t, expected, meta) +} + +func TestBuildChapterMetadata_DropsNegativeOffsets(t *testing.T) { + start := time.Unix(1000, 0) + out := filepath.Join(t.TempDir(), "rec.mp4") + markers := []Marker{ + {Name: "before-start", At: start.Add(-3 * time.Second)}, + {Name: "kept", At: start.Add(4 * time.Second)}, + } + + path, ok, err := buildChapterMetadata(out, markers, start, 8000) + require.NoError(t, err) + require.True(t, ok) + + meta := readMeta(t, path) + assert.NotContains(t, meta, "before-start") + assert.Contains(t, meta, "title=kept") + // Sentinel + one kept marker = two chapters. + assert.Equal(t, 2, strings.Count(meta, "[CHAPTER]")) +} + +func TestBuildChapterMetadata_EscapesSpecialChars(t *testing.T) { + start := time.Unix(1000, 0) + out := filepath.Join(t.TempDir(), "rec.mp4") + markers := []Marker{ + {Name: "a=b;c#d\\e", At: start.Add(1 * time.Second)}, + } + + path, ok, err := buildChapterMetadata(out, markers, start, 4000) + require.NoError(t, err) + require.True(t, ok) + assert.Contains(t, readMeta(t, path), `title=a\=b\;c\#d\\e`) +} + +func TestBuildChapterMetadata_NoUsableMarkers(t *testing.T) { + start := time.Unix(1000, 0) + out := filepath.Join(t.TempDir(), "rec.mp4") + + path, ok, err := buildChapterMetadata(out, nil, start, 4000) + require.NoError(t, err) + assert.False(t, ok) + assert.Empty(t, path) + _, statErr := os.Stat(out + ".ffmeta") + assert.True(t, os.IsNotExist(statErr), "no metadata file should be written") +} + +func TestBuildChapterMetadata_NoSentinelWhenFirstAtZero(t *testing.T) { + start := time.Unix(1000, 0) + out := filepath.Join(t.TempDir(), "rec.mp4") + markers := []Marker{ + {Name: "at-zero", At: start}, + } + + path, ok, err := buildChapterMetadata(out, markers, start, 4000) + require.NoError(t, err) + require.True(t, ok) + meta := readMeta(t, path) + assert.NotContains(t, meta, sentinelChapterName) + assert.Equal(t, 1, strings.Count(meta, "[CHAPTER]")) + assert.Contains(t, meta, "START=0\nEND=4000\ntitle=at-zero") +} + +func TestMark_NotRecording(t *testing.T) { + rec := &FFmpegRecorder{ + id: "mark-idle", + exitCode: exitCodeInitValue, + stz: scaletozero.NewOncer(scaletozero.NewNoopController()), + } + _, _, err := rec.Mark("anything") + assert.ErrorIs(t, err, ErrNotRecording) +} + +func TestMark_AppendsAndReturnsOffset(t *testing.T) { + tempDir := t.TempDir() + rec := &FFmpegRecorder{ + id: "mark-active", + binaryPath: mockBin, + params: defaultParams(tempDir), + outputPath: filepath.Join(tempDir, "mark-active.mp4"), + stz: scaletozero.NewOncer(scaletozero.NewNoopController()), + } + require.NoError(t, rec.Start(t.Context())) + t.Cleanup(func() { _ = rec.ForceStop(t.Context()) }) + + name, offset, err := rec.Mark(" checkpoint ") + require.NoError(t, err) + assert.Equal(t, "checkpoint", name, "name is trimmed") + assert.GreaterOrEqual(t, offset, int64(0)) + + rec.mu.Lock() + markers := rec.markers + rec.mu.Unlock() + require.Len(t, markers, 1) + assert.Equal(t, "checkpoint", markers[0].Name) +} + +func TestMark_RejectsBadNames(t *testing.T) { + tempDir := t.TempDir() + rec := &FFmpegRecorder{ + id: "mark-bad", + binaryPath: mockBin, + params: defaultParams(tempDir), + outputPath: filepath.Join(tempDir, "mark-bad.mp4"), + stz: scaletozero.NewOncer(scaletozero.NewNoopController()), + } + require.NoError(t, rec.Start(t.Context())) + t.Cleanup(func() { _ = rec.ForceStop(t.Context()) }) + + _, _, err := rec.Mark(" ") + assert.ErrorIs(t, err, ErrInvalidMarkerName) + + _, _, err = rec.Mark(strings.Repeat("x", maxMarkerNameLen+1)) + assert.ErrorIs(t, err, ErrInvalidMarkerName) +} + +func TestMark_ConcurrentCallsAreSafe(t *testing.T) { + tempDir := t.TempDir() + rec := &FFmpegRecorder{ + id: "mark-race", + binaryPath: mockBin, + params: defaultParams(tempDir), + outputPath: filepath.Join(tempDir, "mark-race.mp4"), + stz: scaletozero.NewOncer(scaletozero.NewNoopController()), + } + require.NoError(t, rec.Start(t.Context())) + t.Cleanup(func() { _ = rec.ForceStop(t.Context()) }) + + const n = 50 + var wg sync.WaitGroup + wg.Add(n) + for i := 0; i < n; i++ { + go func() { + defer wg.Done() + _, _, _ = rec.Mark("m") + }() + } + wg.Wait() + + rec.mu.Lock() + count := len(rec.markers) + rec.mu.Unlock() + assert.Equal(t, n, count) +} + +// chapterInputArgs is the only thing finalize adds for chapters; locking its +// output both ways guarantees the no-marker remux is byte-identical to before. +func TestChapterInputArgs(t *testing.T) { + assert.Nil(t, chapterInputArgs("")) + assert.Equal(t, []string{"-f", "ffmetadata", "-i", "/tmp/x.ffmeta", "-map", "0", "-map_chapters", "1"}, chapterInputArgs("/tmp/x.ffmeta")) +} + +// finalizeRemuxArgs mirrors how finalizeRecording assembles the remux command +// so the backward-compat assertion below can lock both shapes. +func finalizeRemuxArgs(outputPath, tempPath, metaPath string) []string { + args := []string{"-i", outputPath} + args = append(args, chapterInputArgs(metaPath)...) + args = append(args, + "-c", "copy", + "-movflags", "+faststart", + "-f", "mp4", + "-y", + tempPath, + ) + return args +} + +func TestFinalizeRemuxArgs_BackwardsCompat(t *testing.T) { + // No markers: byte-identical to the pre-feature remux command. + preFeature := []string{ + "-i", "/r/out.mp4", + "-c", "copy", + "-movflags", "+faststart", + "-f", "mp4", + "-y", + "/r/out.mp4.tmp", + } + assert.Equal(t, preFeature, finalizeRemuxArgs("/r/out.mp4", "/r/out.mp4.tmp", "")) + + // Markers present: the chapter input and mapping are injected. + withChapters := finalizeRemuxArgs("/r/out.mp4", "/r/out.mp4.tmp", "/r/out.mp4.ffmeta") + assert.Contains(t, withChapters, "-map_chapters") + assert.Contains(t, withChapters, "/r/out.mp4.ffmeta") +} diff --git a/server/lib/recorder/recorder.go b/server/lib/recorder/recorder.go index 8b3b53e3..ed028edd 100644 --- a/server/lib/recorder/recorder.go +++ b/server/lib/recorder/recorder.go @@ -12,6 +12,10 @@ type Recorder interface { Start(ctx context.Context) error Stop(ctx context.Context) error ForceStop(ctx context.Context) error + // Mark records a named marker at the current time. It returns the stored + // (trimmed) marker name and the provisional offset from the recording start + // in milliseconds. + Mark(name string) (string, int64, error) IsRecording(ctx context.Context) bool Metadata() *RecordingMetadata Recording(ctx context.Context) (io.ReadCloser, *RecordingMetadata, error) diff --git a/server/openapi.yaml b/server/openapi.yaml index fcbc991a..28fbbe13 100644 --- a/server/openapi.yaml +++ b/server/openapi.yaml @@ -308,6 +308,33 @@ paths: $ref: "#/components/responses/ConflictError" "500": $ref: "#/components/responses/InternalError" + /recording/mark: + post: + summary: Add a named marker to an in-progress recording + description: >- + Records a named point in time during an active recording. At finalize + the markers are injected into the output MP4 as chapter markers. + operationId: markRecording + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/MarkRecordingRequest" + responses: + "201": + description: Marker recorded + content: + application/json: + schema: + $ref: "#/components/schemas/MarkRecordingResult" + "400": + $ref: "#/components/responses/BadRequestError" + "409": + description: No recording is currently in progress + $ref: "#/components/responses/ConflictError" + "500": + $ref: "#/components/responses/InternalError" /computer/click_mouse: post: summary: Simulate a mouse click action on the host computer @@ -3450,6 +3477,33 @@ components: description: Identifier of the recording to delete. Alphanumeric or hyphen. pattern: "^[a-zA-Z0-9-]+$" additionalProperties: false + MarkRecordingRequest: + type: object + required: [name] + properties: + name: + type: string + description: Name of the marker, used as the MP4 chapter title. + id: + type: string + description: Identifier of the recorder to mark. Alphanumeric or hyphen. + pattern: "^[a-zA-Z0-9-]+$" + additionalProperties: false + MarkRecordingResult: + type: object + required: [name, offsetMs] + properties: + name: + type: string + description: Name of the recorded marker. + offsetMs: + type: integer + format: int64 + description: >- + Provisional offset of the marker from the recording start, in + milliseconds, measured against the start time at mark time. The + authoritative offset is the chapter start written at finalize. + additionalProperties: false FileInfo: type: object required: [name, path, size_bytes, is_dir, mod_time, mode]