├── .python-version ├── skellycam ├── api │ ├── __init__.py │ ├── http │ │ ├── __init__.py │ │ ├── ui │ │ │ ├── __init__.py │ │ │ └── ui_router.py │ │ ├── app │ │ │ ├── __init__.py │ │ │ ├── health.py │ │ │ └── shutdown.py │ │ ├── cameras │ │ │ └── __init__.py │ │ └── videos │ │ │ ├── __init__.py │ │ │ └── videos_router.py │ ├── middleware │ │ ├── __init__.py │ │ └── cors.py │ ├── websocket │ │ ├── __init__.py │ │ └── websocket_connect.py │ ├── server_constants.py │ └── routers.py ├── system │ ├── __init__.py │ ├── diagnostics │ │ ├── __init__.py │ │ ├── find_optimal_chunk_size_for_pipe_transer.py │ │ └── numpy_array_copy_duration.py │ ├── device_detection │ │ ├── __init__.py │ │ └── detect_microphone_devices.py │ └── logging_configuration │ │ ├── __init__.py │ │ ├── filters │ │ ├── __init__.py │ │ └── delta_time.py │ │ ├── formatters │ │ ├── __init__.py │ │ ├── custom_formatter.py │ │ └── color_formatter.py │ │ ├── handlers │ │ ├── __init__.py │ │ └── colored_console.py │ │ ├── log_format_string.py │ │ ├── log_levels.py │ │ ├── package_log_quieters.py │ │ ├── log_test_messages.py │ │ ├── logging_color_helpers.py │ │ ├── configure_logging.py │ │ └── logger_builder.py ├── tests │ ├── __init__.py │ ├── test_recorders │ │ └── __init__.py │ ├── test_timestamps │ │ └── __init__.py │ ├── test_shared_memory_objects │ │ └── __init__.py │ └── test_descriptive_statistics.py ├── core │ ├── camera │ │ ├── __init__.py │ │ ├── config │ │ │ ├── __init__.py │ │ │ ├── image_rotation_types.py │ │ │ └── image_resolution.py │ │ └── opencv │ │ │ ├── __init__.py │ │ │ └── opencv_helpers │ │ │ ├── __init__.py │ │ │ ├── create_initial_frame_recarray.py │ │ │ ├── determine_backend.py │ │ │ ├── handle_video_recording_loop.py │ │ │ ├── opencv_extract_config.py │ │ │ ├── create_cv2_video_capture.py │ │ │ └── check_for_new_config.py │ ├── ipc │ │ ├── __init__.py │ │ ├── pubsub │ │ │ ├── __init__.py │ │ │ └── pubsub_abcs.py │ │ └── shared_memory │ │ │ ├── __init__.py │ │ │ ├── multi_frame_payload_single_slot_shared_memory.py │ │ │ ├── frame_payload_shared_memory_ring_buffer.py │ │ │ ├── camera_shared_memory_ring_buffer.py │ │ │ └── shared_memory_number.py │ ├── types │ │ ├── __init__.py │ │ └── type_overloads.py │ ├── __init__.py │ ├── recorders │ │ ├── __init__.py │ │ ├── audio │ │ │ └── __init__.py │ │ ├── videos │ │ │ └── __init__.py │ │ └── recording_manager_status.py │ ├── camera_group │ │ ├── __init__.py │ │ └── timestamps │ │ │ ├── __init__.py │ │ │ └── numpy_timestamps │ │ │ ├── __init__.py │ │ │ ├── save_timestamps_statistics_summary.py │ │ │ ├── process_recording_timestamps.py │ │ │ └── calculate_frame_timestamp_statistics.py │ └── device_detection │ │ ├── __init__.py │ │ └── detect_microphone_devices.py ├── utilities │ ├── __init__.py │ ├── get_version.py │ ├── arbitrary_types_base_model.py │ ├── create_camera_group_id.py │ ├── clean_path.py │ ├── check_shutdown_flag.py │ ├── cross_platform_start_file.py │ ├── rotate_image.py │ ├── setup_windows_app_id.py │ ├── clean_up_empty_directories.py │ ├── time_unit_conversion.py │ ├── get_process_memory_useage.py │ ├── find_available_port.py │ ├── kill_process_on_port.py │ ├── check_main_processs_heartbeat.py │ ├── check_ffmpeg_and_h264_codec.py │ ├── wait_functions.py │ └── active_elements_check.py └── __init__.py ├── skellycam-ui ├── src │ ├── services │ │ ├── index.ts │ │ ├── electron-ipc │ │ │ ├── index.ts │ │ │ └── electron-ipc.ts │ │ └── server │ │ │ ├── index.ts │ │ │ └── server-helpers │ │ │ ├── server-urls.ts │ │ │ └── frame-processor │ │ │ └── frame-processor.ts │ ├── store │ │ ├── slices │ │ │ ├── theme │ │ │ │ ├── theme-types.ts │ │ │ │ ├── index.ts │ │ │ │ ├── theme-selectors.ts │ │ │ │ └── theme-slice.ts │ │ │ ├── log-records │ │ │ │ ├── index.ts │ │ │ │ ├── logs-types.ts │ │ │ │ └── log-records-slice.ts │ │ │ ├── recording │ │ │ │ ├── index.ts │ │ │ │ ├── recording-types.ts │ │ │ │ ├── recording-slice.ts │ │ │ │ └── recording-thunks.ts │ │ │ ├── framerate │ │ │ │ ├── index.ts │ │ │ │ ├── framerate-types.ts │ │ │ │ └── framerate-slice.ts │ │ │ ├── videos │ │ │ │ ├── index.ts │ │ │ │ ├── videos-types.ts │ │ │ │ ├── videos-selectors.ts │ │ │ │ └── videos-thunks.ts │ │ │ └── cameras │ │ │ │ └── index.ts │ │ ├── hooks.ts │ │ ├── types.ts │ │ ├── thunks │ │ │ ├── server-healthcheck.ts │ │ │ ├── shutdown-server.ts │ │ │ ├── pause-unpause-thunk.ts │ │ │ ├── close-cameras-thunks.ts │ │ │ ├── update-camera-configs-thunk.ts │ │ │ └── connect-to-cameras-thunk.ts │ │ ├── index.ts │ │ └── store.ts │ ├── demos │ │ ├── ipc.ts │ │ └── node.ts │ ├── vite-env.d.ts │ ├── main.tsx │ ├── components │ │ ├── update │ │ │ ├── update.css │ │ │ ├── Progress │ │ │ │ ├── progress.css │ │ │ │ └── index.tsx │ │ │ ├── README.zh-CN.md │ │ │ ├── README.md │ │ │ └── Modal │ │ │ │ ├── modal.css │ │ │ │ └── index.tsx │ │ ├── ui-components │ │ │ ├── Footer.tsx │ │ │ ├── Header.tsx │ │ │ ├── ThemeToggle.tsx │ │ │ └── BottomPanelContent.tsx │ │ ├── camera-config-tree-view │ │ │ ├── NoCamerasPlaceholder.tsx │ │ │ ├── CameraConfigTreeSection.tsx │ │ │ └── CameraGroupTreeItem.tsx │ │ ├── recording-info-panel │ │ │ └── recording-subcomponents │ │ │ │ ├── RecordingNamePreview.tsx │ │ │ │ └── DelayRecordingStartControl.tsx │ │ ├── common │ │ │ └── ErrorBoundary.tsx │ │ ├── camera-view-settings-overlay │ │ │ └── ImageScaleSlider.tsx │ │ ├── camera-config-panel │ │ │ └── CameraConfigRotation.tsx │ │ ├── PauseUnpauseButton.tsx │ │ └── camera-views │ │ │ └── CameraViewsGrid.tsx │ ├── App.tsx │ ├── layout │ │ ├── BaseContentRouter.tsx │ │ ├── AppContent.tsx │ │ ├── paperbase_theme │ │ │ └── PaperbaseContent.tsx │ │ └── BasePanelLayout.tsx │ ├── App.css │ ├── hooks │ │ └── useAppUrls.ts │ ├── pages │ │ ├── CamerasPage.tsx │ │ └── VideosPage.tsx │ └── index.css ├── public │ └── skellycam-logo.png ├── e2e │ ├── screenshots │ │ └── example.png │ └── example.spec.ts ├── old_electron │ ├── main │ │ ├── helpers │ │ │ ├── app-environment.ts │ │ │ ├── logger.ts │ │ │ ├── window-manager.ts │ │ │ └── app-paths.ts │ │ └── index.ts │ └── electron-env.d.ts ├── .npmrc ├── electron │ ├── preload │ │ └── index.ts │ └── main │ │ ├── services │ │ ├── logger.ts │ │ └── update-handler.ts │ │ ├── ipc.ts │ │ ├── app-paths.ts │ │ └── index.ts ├── tsconfig.node.json ├── index.html ├── .gitignore ├── tsconfig.json ├── electron-builder.json ├── playwright.config.ts ├── package.json └── vite.config.flat.txt ├── .env.sample ├── shared └── skellycam-logo │ ├── skellycam-logo.ai │ ├── skellycam-logo.png │ └── skellycam-favicon.ico ├── scripts └── save-env-to-workspace.ps1 ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── help_wanted.md ├── PULL_REQUEST_TEMPLATE.md └── dependabot.yml ├── installers └── nuitka_scripts │ ├── skellycam-nuitka.config.yml │ └── nuitka_installer_notes.md └── Dockerfile /.python-version: -------------------------------------------------------------------------------- 1 | 3.12 2 | -------------------------------------------------------------------------------- /skellycam/api/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /skellycam/system/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /skellycam/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /skellycam/api/http/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /skellycam/api/http/ui/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /skellycam/core/camera/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /skellycam/core/ipc/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /skellycam/core/types/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /skellycam/utilities/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /skellycam/api/http/app/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /skellycam/api/http/cameras/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /skellycam/api/http/videos/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /skellycam/api/middleware/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /skellycam/api/websocket/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /skellycam/core/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /skellycam/core/ipc/pubsub/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /skellycam/core/recorders/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /skellycam/core/camera/config/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /skellycam/core/camera/opencv/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /skellycam/core/camera_group/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /skellycam/core/device_detection/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /skellycam/core/recorders/audio/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /skellycam/core/recorders/videos/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /skellycam/system/diagnostics/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /skellycam/tests/test_recorders/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /skellycam/tests/test_timestamps/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /skellycam/core/ipc/shared_memory/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /skellycam/system/device_detection/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /skellycam/core/camera_group/timestamps/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /skellycam/system/logging_configuration/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /skellycam/core/camera/opencv/opencv_helpers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /skellycam/system/logging_configuration/filters/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /skellycam/tests/test_shared_memory_objects/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /skellycam/system/logging_configuration/formatters/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /skellycam/system/logging_configuration/handlers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /skellycam/core/camera_group/timestamps/numpy_timestamps/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /skellycam-ui/src/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './electron-ipc' 2 | export * from './server' 3 | -------------------------------------------------------------------------------- /skellycam-ui/src/store/slices/theme/theme-types.ts: -------------------------------------------------------------------------------- 1 | export type ThemeMode = 'light' | 'dark'; 2 | -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | AZURE_TENANT_ID=your-tenant-id 2 | AZURE_CLIENT_ID=your-client-id 3 | AZURE_CLIENT_SECRET=your-client-secret -------------------------------------------------------------------------------- /shared/skellycam-logo/skellycam-logo.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freemocap/skellycam/HEAD/shared/skellycam-logo/skellycam-logo.ai -------------------------------------------------------------------------------- /skellycam-ui/public/skellycam-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freemocap/skellycam/HEAD/skellycam-ui/public/skellycam-logo.png -------------------------------------------------------------------------------- /shared/skellycam-logo/skellycam-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freemocap/skellycam/HEAD/shared/skellycam-logo/skellycam-logo.png -------------------------------------------------------------------------------- /skellycam-ui/e2e/screenshots/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freemocap/skellycam/HEAD/skellycam-ui/e2e/screenshots/example.png -------------------------------------------------------------------------------- /shared/skellycam-logo/skellycam-favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freemocap/skellycam/HEAD/shared/skellycam-logo/skellycam-favicon.ico -------------------------------------------------------------------------------- /skellycam/api/server_constants.py: -------------------------------------------------------------------------------- 1 | PROTOCOL = "http" 2 | HOSTNAME = "localhost" 3 | PORT = 53117 4 | APP_URL = f"{PROTOCOL}://{HOSTNAME}:{PORT}" -------------------------------------------------------------------------------- /skellycam-ui/src/services/electron-ipc/index.ts: -------------------------------------------------------------------------------- 1 | // services/index.ts 2 | export * from './electron-ipc' 3 | export * from './electron-ipc-client' 4 | -------------------------------------------------------------------------------- /skellycam-ui/src/store/slices/theme/index.ts: -------------------------------------------------------------------------------- 1 | export * from './theme-slice'; 2 | export * from './theme-types'; 3 | export * from './theme-selectors'; 4 | -------------------------------------------------------------------------------- /skellycam-ui/src/store/slices/log-records/index.ts: -------------------------------------------------------------------------------- 1 | export * from './log-records-slice'; 2 | export * from './logs-types'; 3 | export * from './logs-selectors'; 4 | -------------------------------------------------------------------------------- /skellycam-ui/src/store/slices/recording/index.ts: -------------------------------------------------------------------------------- 1 | export * from './recording-slice'; 2 | export * from './recording-types'; 3 | export * from './recording-thunks'; 4 | -------------------------------------------------------------------------------- /skellycam-ui/src/demos/ipc.ts: -------------------------------------------------------------------------------- 1 | window.ipcRenderer.on('main-process-message', (_event, ...args) => { 2 | console.log('[Receive Main-process message]:', ...args) 3 | }) 4 | -------------------------------------------------------------------------------- /skellycam-ui/src/store/slices/framerate/index.ts: -------------------------------------------------------------------------------- 1 | export * from './framerate-slice'; 2 | export * from './framerate-types'; 3 | export * from './framerate-selectors'; 4 | -------------------------------------------------------------------------------- /skellycam-ui/src/store/slices/videos/index.ts: -------------------------------------------------------------------------------- 1 | export * from './videos-slice'; 2 | export * from './videos-types'; 3 | export * from './videos-thunks'; 4 | export * from './videos-selectors'; 5 | -------------------------------------------------------------------------------- /skellycam-ui/src/store/slices/cameras/index.ts: -------------------------------------------------------------------------------- 1 | export * from './cameras-slice'; 2 | export * from './cameras-types'; 3 | export * from './cameras-selectors'; 4 | export * from './cameras-thunks'; 5 | -------------------------------------------------------------------------------- /skellycam-ui/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | interface Window { 4 | // expose in the `electron/preload/index.ts` 5 | ipcRenderer: import('electron').IpcRenderer 6 | } 7 | -------------------------------------------------------------------------------- /skellycam/utilities/get_version.py: -------------------------------------------------------------------------------- 1 | from skellycam import __version__ 2 | 3 | 4 | def get_version() -> str: 5 | return __version__ 6 | 7 | 8 | if __name__ == "__main__": 9 | print(get_version()) 10 | -------------------------------------------------------------------------------- /scripts/save-env-to-workspace.ps1: -------------------------------------------------------------------------------- 1 | # build.ps1 2 | Get-Content .env | ForEach-Object { 3 | $name, $value = $_.split('=') 4 | if ($name -and $value) { 5 | Set-Item -Path env:$name -Value $value 6 | } 7 | } 8 | 9 | -------------------------------------------------------------------------------- /skellycam/utilities/arbitrary_types_base_model.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, ConfigDict 2 | 3 | 4 | class ArbitraryTypesBaseModel(BaseModel): 5 | model_config = ConfigDict( 6 | arbitrary_types_allowed=True 7 | ) 8 | -------------------------------------------------------------------------------- /skellycam-ui/src/demos/node.ts: -------------------------------------------------------------------------------- 1 | import {lstat} from 'node:fs/promises' 2 | import {cwd} from 'node:process' 3 | 4 | lstat(cwd()).then(stats => { 5 | console.log('[fs.lstat]', stats) 6 | }).catch(err => { 7 | console.error(err) 8 | }) 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | name: 🐞 Bug report 4 | about: Create a report to help us improve 5 | title: "[Bug] the title of bug report" 6 | labels: bug 7 | assignees: '' 8 | 9 | --- 10 | 11 | #### Describe the bug 12 | -------------------------------------------------------------------------------- /skellycam-ui/old_electron/main/helpers/app-environment.ts: -------------------------------------------------------------------------------- 1 | // Export the ENV_CONFIG object using the process.env values 2 | export const APP_ENVIRONMENT = { 3 | IS_DEV: process.env.NODE_ENV === 'development', 4 | SHOULD_LAUNCH_PYTHON: true, 5 | }; 6 | -------------------------------------------------------------------------------- /skellycam/api/routers.py: -------------------------------------------------------------------------------- 1 | from skellycam.api.http.cameras.camera_router import camera_router 2 | from skellycam.api.websocket.websocket_connect import websocket_router 3 | 4 | SKELLYCAM_ROUTERS = [ 5 | websocket_router, 6 | camera_router, 7 | ] 8 | -------------------------------------------------------------------------------- /skellycam-ui/.npmrc: -------------------------------------------------------------------------------- 1 | # For electron-builder 2 | # https://github.com/electron-userland/electron-builder/issues/6289#issuecomment-1042620422 3 | shamefully-hoist=true 4 | 5 | # For China 🇨🇳 developers 6 | # electron_mirror=https://npmmirror.com/mirrors/electron/ 7 | -------------------------------------------------------------------------------- /skellycam/utilities/create_camera_group_id.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from skellycam.core.types.type_overloads import CameraGroupIdString 4 | 5 | 6 | def create_camera_group_id() -> CameraGroupIdString: 7 | return str(uuid.uuid4())[:6] # Shortened UUID for readability 8 | -------------------------------------------------------------------------------- /skellycam-ui/src/store/slices/theme/theme-selectors.ts: -------------------------------------------------------------------------------- 1 | import { RootState } from '../../types'; 2 | 3 | export const selectThemeMode = (state: RootState) => state.theme.mode; 4 | export const selectSystemThemePreference = (state: RootState) => 5 | state.theme.systemPreference; 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/help_wanted.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🥺 Help wanted 3 | about: Confuse about the use of electron-vue-vite 4 | title: "[Help] the title of help wanted report" 5 | labels: help wanted 6 | assignees: '' 7 | 8 | --- 9 | 10 | #### Describe the problem you confuse 11 | -------------------------------------------------------------------------------- /skellycam-ui/electron/preload/index.ts: -------------------------------------------------------------------------------- 1 | 2 | import { contextBridge, ipcRenderer } from 'electron'; 3 | 4 | // Expose a simple tRPC bridge 5 | contextBridge.exposeInMainWorld('electronAPI', { 6 | invoke: (path: string, input?: any) => 7 | ipcRenderer.invoke('trpc', { path, input }) 8 | }); 9 | -------------------------------------------------------------------------------- /installers/nuitka_scripts/skellycam-nuitka.config.yml: -------------------------------------------------------------------------------- 1 | - module-name: 'skellycam.api.http.ui' 2 | data-files: 3 | patterns: 4 | - 'skellycam/api/http/ui/ui.html' 5 | 6 | - module-name: 'shared.skellycam-logo' 7 | data-files: 8 | patterns: 9 | - 'shared/skellycam-logo/skellycam-favicon.ico' 10 | -------------------------------------------------------------------------------- /skellycam-ui/src/store/hooks.ts: -------------------------------------------------------------------------------- 1 | import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'; 2 | import type { RootState, AppDispatch } from './types'; 3 | 4 | export const useAppDispatch = (): AppDispatch => useDispatch(); 5 | export const useAppSelector: TypedUseSelectorHook = useSelector; 6 | -------------------------------------------------------------------------------- /skellycam/utilities/clean_path.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | 4 | def clean_path(path: str) -> str: 5 | """ 6 | Replace the home directory with a tilde in the path 7 | """ 8 | home_dir = str(Path.home()) 9 | if home_dir in path: 10 | return path.replace(home_dir, "~") 11 | return path 12 | -------------------------------------------------------------------------------- /skellycam-ui/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "resolveJsonModule": true, 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": [ 10 | "vite.config.ts", 11 | "package.json" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /skellycam/utilities/check_shutdown_flag.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | def get_server_shutdown_environment_flag(): 5 | flag = os.getenv("SKELLYCAM_SHOULD_SHUTDOWN") 6 | if flag: 7 | for _ in range(20): 8 | print(f"SHUTDOWN FLAG DETECTED! os.getenv('SKELLYCAM_SHOULD_SHUTDOWN'): {flag}") 9 | return flag 10 | -------------------------------------------------------------------------------- /skellycam/utilities/cross_platform_start_file.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | import sys 4 | 5 | 6 | def open_file(filename: str): 7 | if sys.platform == "win32": 8 | os.startfile(filename) 9 | else: 10 | opener = "open" if sys.platform == "darwin" else "xdg-open" 11 | subprocess.call([opener, filename]) 12 | -------------------------------------------------------------------------------- /skellycam/api/middleware/cors.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from fastapi.middleware.cors import CORSMiddleware 3 | 4 | 5 | def cors(app: FastAPI) -> None: 6 | app.add_middleware( 7 | CORSMiddleware, 8 | allow_origins=["*"], 9 | allow_credentials=True, 10 | allow_methods=["*"], 11 | allow_headers=["*"], 12 | ) 13 | -------------------------------------------------------------------------------- /skellycam-ui/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App' 4 | 5 | import './index.css' 6 | 7 | 8 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( 9 | 10 | 11 | , 12 | ) 13 | 14 | postMessage({payload: 'removeLoading'}, '*') 15 | -------------------------------------------------------------------------------- /skellycam-ui/src/store/types.ts: -------------------------------------------------------------------------------- 1 | import { store } from './store'; 2 | import { ThunkAction, Action } from '@reduxjs/toolkit'; 3 | 4 | export type RootState = ReturnType; 5 | export type AppDispatch = typeof store.dispatch; 6 | export type AppThunk = ThunkAction< 7 | ReturnType, 8 | RootState, 9 | unknown, 10 | Action 11 | >; 12 | -------------------------------------------------------------------------------- /skellycam/api/http/app/health.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from fastapi import APIRouter 4 | 5 | logger = logging.getLogger(__name__) 6 | health_router = APIRouter() 7 | 8 | 9 | @health_router.get("/health", summary="Hello👋", tags=['App']) 10 | def healthcheck_endpoint(): 11 | """ 12 | A simple endpoint to show the server is alive and happy 13 | """ 14 | 15 | return "Hello👋" 16 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ### Description 4 | 5 | 6 | 7 | ### What is the purpose of this pull request? 8 | 9 | - [ ] Bug fix 10 | - [ ] New Feature 11 | - [ ] Documentation update 12 | - [ ] Other 13 | -------------------------------------------------------------------------------- /skellycam-ui/src/store/slices/recording/recording-types.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const RecordingInfoSchema = z.object({ 4 | isRecording: z.boolean(), 5 | recordingDirectory: z.string(), 6 | recordingName: z.string().nullable(), 7 | startedAt: z.string().nullable(), 8 | duration: z.number().nullable() 9 | }); 10 | 11 | export type RecordingInfo = z.infer; 12 | -------------------------------------------------------------------------------- /skellycam-ui/src/store/thunks/server-healthcheck.ts: -------------------------------------------------------------------------------- 1 | import {useAppUrls} from "@/hooks/useAppUrls"; 2 | 3 | export const serverHealthcheck = async () => { 4 | const url = useAppUrls.getHttpEndpointUrls().health; 5 | const response = await fetch(url, {method: 'GET'}); 6 | 7 | if (!response.ok) { 8 | throw new Error(`HTTP error! status: ${response.status}`); 9 | } 10 | 11 | return response; 12 | }; 13 | -------------------------------------------------------------------------------- /skellycam-ui/src/store/thunks/shutdown-server.ts: -------------------------------------------------------------------------------- 1 | import {useAppUrls} from "@/hooks/useAppUrls"; 2 | 3 | export const shutdownServer = async () => { 4 | const url = useAppUrls.getHttpEndpointUrls().shutdown; 5 | const response = await fetch(url, {method: 'GET'}); 6 | 7 | if (!response.ok) { 8 | throw new Error(`HTTP error! status: ${response.status}`); 9 | } 10 | 11 | return response; 12 | }; 13 | -------------------------------------------------------------------------------- /skellycam-ui/e2e/example.spec.ts: -------------------------------------------------------------------------------- 1 | import {_electron as electron, expect, test} from "@playwright/test"; 2 | 3 | test("homepage has title and links to intro page", async () => { 4 | const app = await electron.launch({args: [".", "--no-sandbox"]}); 5 | const page = await app.firstWindow(); 6 | expect(await page.title()).toBe("Electron + Vite + React"); 7 | await page.screenshot({path: "e2e/screenshots/example.png"}); 8 | }); 9 | -------------------------------------------------------------------------------- /skellycam/utilities/rotate_image.py: -------------------------------------------------------------------------------- 1 | import cv2 2 | import numpy as np 3 | 4 | from skellycam.core.camera.config.image_rotation_types import RotationTypes 5 | 6 | 7 | def rotate_image(image:np.ndarray, rotation: RotationTypes): 8 | 9 | # Rotate the image if needed 10 | if rotation.value != -1: 11 | rotated_image = cv2.rotate(image, rotation.value) 12 | else: 13 | rotated_image = image 14 | 15 | return rotated_image 16 | -------------------------------------------------------------------------------- /skellycam-ui/src/services/server/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ServerContextProvider' 2 | export * from '@/services/server/server-helpers/frame-processor/binary-frame-parser' 3 | export * from './server-helpers/canvas-manager' 4 | export * from '@/services/server/server-helpers/frame-processor/frame-processor' 5 | export * from './server-helpers/offscreen-renderer.worker' 6 | export * from './server-helpers/server-urls' 7 | export * from './server-helpers/websocket-connection' 8 | -------------------------------------------------------------------------------- /skellycam-ui/src/store/thunks/pause-unpause-thunk.ts: -------------------------------------------------------------------------------- 1 | import {useAppUrls} from "@/hooks/useAppUrls"; 2 | 3 | export const pauseUnpauseThunk = async () => { 4 | const pauseUnpauseUrl = useAppUrls.getHttpEndpointUrls().pauseUnpauseCameras; 5 | const response = await fetch(pauseUnpauseUrl, {method: 'GET'}); 6 | 7 | if (!response.ok) { 8 | throw new Error(`HTTP error! status: ${response.status}`); 9 | } 10 | 11 | return response; 12 | }; 13 | -------------------------------------------------------------------------------- /skellycam/utilities/setup_windows_app_id.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | 4 | def setup_app_id_for_windows(): 5 | if sys.platform == "win32": 6 | # set up so you can change the taskbar icon - https://stackoverflow.com/a/74531530/14662833 7 | import ctypes 8 | import skellycam 9 | 10 | myappid = f"{skellycam.__package_name__}_{skellycam.__version__}" # arbitrary string 11 | ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid) 12 | -------------------------------------------------------------------------------- /skellycam-ui/src/components/update/update.css: -------------------------------------------------------------------------------- 1 | .modal-slot { 2 | .update-progress { 3 | display: flex; 4 | } 5 | 6 | .new-version__target, 7 | .update__progress { 8 | margin-left: 40px; 9 | } 10 | 11 | .progress__title { 12 | margin-right: 10px; 13 | } 14 | 15 | .progress__bar { 16 | width: 0; 17 | flex-grow: 1; 18 | } 19 | 20 | .can-not-available { 21 | padding: 20px; 22 | text-align: center; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /skellycam/core/camera/config/image_rotation_types.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | import cv2 4 | 5 | 6 | class RotationTypes(Enum): 7 | NO_ROTATION = -1 8 | CLOCKWISE_90 = cv2.ROTATE_90_CLOCKWISE 9 | ROTATE_180 = cv2.ROTATE_180 10 | COUNTERCLOCKWISE_90 = cv2.ROTATE_90_COUNTERCLOCKWISE 11 | 12 | def rotation_int_to_name(rotation_int: int) -> str: 13 | for rotation in RotationTypes: 14 | if rotation.value == rotation_int: 15 | return rotation.name 16 | return "UNKNOWN_ROTATION" 17 | -------------------------------------------------------------------------------- /skellycam-ui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | SkellyCam💀📸 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /skellycam-ui/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Provider} from 'react-redux'; 3 | import {store} from '@/store'; 4 | import {ServerContextProvider} from "@/services/server/ServerContextProvider"; 5 | import {AppContent} from "@/layout/AppContent"; 6 | 7 | 8 | function App() { 9 | return ( 10 | 11 | 12 | 13 | 14 | 15 | ); 16 | } 17 | 18 | export default App; 19 | -------------------------------------------------------------------------------- /skellycam-ui/src/store/index.ts: -------------------------------------------------------------------------------- 1 | export { store } from './store'; 2 | export type { RootState, AppDispatch, AppThunk } from './types'; 3 | export { useAppDispatch, useAppSelector } from './hooks'; 4 | 5 | 6 | // Re-export all slice actions and selectors for a nice 'barrel' design pattern, which makes imports cleaner 7 | export * from './slices/cameras'; 8 | export * from './slices/recording'; 9 | export * from './slices/framerate'; 10 | export * from './slices/log-records'; 11 | export * from './slices/theme'; 12 | export * from './slices/videos'; 13 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "monthly" 12 | -------------------------------------------------------------------------------- /skellycam/utilities/clean_up_empty_directories.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from pathlib import Path 3 | 4 | logger = logging.getLogger(__name__) 5 | 6 | 7 | def recursively_remove_empty_directories(base_path: str) -> None: 8 | logger.debug(f"Cleaning up empty directories...") 9 | for directory in reversed(list(Path(base_path).glob("**"))): 10 | if directory.is_dir(): 11 | try: 12 | directory.rmdir() # Remove directories 13 | except OSError: 14 | pass # Directory not empty, can't be removed 15 | -------------------------------------------------------------------------------- /skellycam-ui/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | dist-electron 14 | release 15 | *.local 16 | 17 | # Editor directories and files 18 | .vscode/.debug.env 19 | .idea 20 | .DS_Store 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | 27 | #lockfile 28 | package-lock.json 29 | pnpm-lock.yaml 30 | yarn.lock 31 | /test-results/ 32 | /playwright-report/ 33 | /playwright/.cache/ 34 | 35 | *.exe 36 | pyapp-* 37 | *.zip -------------------------------------------------------------------------------- /skellycam-ui/src/components/update/Progress/progress.css: -------------------------------------------------------------------------------- 1 | .update-progress { 2 | display: flex; 3 | align-items: center; 4 | 5 | .update-progress-pr { 6 | border: 1px solid #000; 7 | border-radius: 3px; 8 | height: 6px; 9 | width: 300px; 10 | } 11 | 12 | .update-progress-rate { 13 | height: 6px; 14 | border-radius: 3px; 15 | background-image: linear-gradient(to right, rgb(130, 86, 208) 0%, var(--primary-color) 100%) 16 | } 17 | 18 | .update-progress-num { 19 | margin: 0 10px; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /skellycam/system/logging_configuration/filters/delta_time.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from datetime import datetime 3 | 4 | 5 | class DeltaTimeFilter(logging.Filter): 6 | """Adds Δt since last log to records""" 7 | 8 | def __init__(self): 9 | self.prev_time = datetime.now().timestamp() 10 | super().__init__() 11 | 12 | def filter(self, record: logging.LogRecord) -> bool: 13 | current_time = datetime.now().timestamp() 14 | record.delta_t = f"{(current_time - self.prev_time) * 1000:.3f}ms" 15 | self.prev_time = current_time 16 | return True -------------------------------------------------------------------------------- /skellycam-ui/src/store/slices/videos/videos-types.ts: -------------------------------------------------------------------------------- 1 | export interface VideoFile { 2 | name: string; 3 | path: string; 4 | size?: number; 5 | duration?: number; 6 | thumbnail?: string; 7 | } 8 | 9 | export interface VideosState { 10 | folder: string; 11 | files: VideoFile[]; 12 | selectedFile: VideoFile | null; 13 | isLoading: boolean; 14 | error: string | null; 15 | playbackState: { 16 | isPlaying: boolean; 17 | currentTime: number; 18 | duration: number; 19 | volume: number; 20 | playbackRate: number; 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /skellycam/system/logging_configuration/formatters/custom_formatter.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from datetime import datetime 3 | 4 | 5 | class CustomFormatter(logging.Formatter): 6 | """Base formatter with microsecond timestamps and structured formatting""" 7 | 8 | def __init__(self, format_string: str): 9 | super().__init__(fmt=format_string, datefmt="%Y-%m-%dT%H:%M:%S") 10 | self.format_string = format_string 11 | 12 | def formatTime(self, record: logging.LogRecord, datefmt: str = None) -> str: 13 | return datetime.fromtimestamp(record.created).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] -------------------------------------------------------------------------------- /skellycam/system/logging_configuration/handlers/colored_console.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sys 3 | 4 | from ..filters.delta_time import DeltaTimeFilter 5 | from ..formatters.color_formatter import ColorFormatter 6 | from ..log_format_string import COLOR_LOG_FORMAT_STRING 7 | 8 | 9 | class ColoredConsoleHandler(logging.StreamHandler): 10 | """Colorized console output with Δt and process/thread coloring""" 11 | 12 | def __init__(self, stream=sys.stdout): 13 | super().__init__(stream) 14 | self.setFormatter(ColorFormatter(COLOR_LOG_FORMAT_STRING)) 15 | self.addFilter(DeltaTimeFilter()) -------------------------------------------------------------------------------- /skellycam-ui/old_electron/electron-env.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace NodeJS { 2 | interface ProcessEnv { 3 | VSCODE_DEBUG?: 'true' 4 | /** 5 | * The built directory structure 6 | * 7 | * ```tree 8 | * ├─┬ dist-electron 9 | * │ ├─┬ main 10 | * │ │ └── index.js > Electron-Main 11 | * │ └─┬ preload 12 | * │ └── index.mjs > Preload-Scripts 13 | * ├─┬ dist 14 | * │ └── index.html > Electron-Renderer 15 | * ``` 16 | */ 17 | APP_ROOT: string 18 | /** /dist/ or /public/ */ 19 | VITE_PUBLIC: string 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /skellycam/system/logging_configuration/log_format_string.py: -------------------------------------------------------------------------------- 1 | LOG_POINTER_STRING = "└>>" 2 | LOG_FORMAT_STRING_WO_PID_TID = LOG_POINTER_STRING + ( 3 | " %(message)s | " 4 | " %(levelname)s | " 5 | " %(delta_t)s | " 6 | " %(name)s.%(funcName)s():%(lineno)s | " 7 | " %(asctime)s | " 8 | ) 9 | 10 | LOG_FORMAT_STRING = LOG_FORMAT_STRING_WO_PID_TID + ( 11 | " PID:%(process)d:%(processName)s | " 12 | " TID:%(thread)d:%(threadName)s" 13 | ) 14 | 15 | COLOR_LOG_FORMAT_STRING = LOG_FORMAT_STRING_WO_PID_TID + ( 16 | " %(pid_color)sPID:%(process)d:%(processName)s\033[0m | " 17 | " %(tid_color)sTID:%(thread)d:%(threadName)s\033[0m" 18 | ) 19 | -------------------------------------------------------------------------------- /skellycam/utilities/time_unit_conversion.py: -------------------------------------------------------------------------------- 1 | from tzlocal import get_localzone 2 | 3 | def ns_to_ms(ns: int|float) -> float: 4 | """ 5 | Convert nanoseconds to milliseconds. 6 | """ 7 | return ns / 1e6 8 | 9 | def ms_to_ns(ms: float) -> int: 10 | """ 11 | Convert milliseconds to nanoseconds. 12 | """ 13 | return int(ms * 1e6) 14 | 15 | def ns_to_sec(ns: int|float) -> float: 16 | """ 17 | Convert nanoseconds to seconds. 18 | """ 19 | return ns / 1e9 20 | 21 | def ms_to_sec(ms: float) -> float: 22 | """ 23 | Convert milliseconds to seconds. 24 | """ 25 | return ms / 1000.0 26 | LOCAL_TIMEZONE = get_localzone() -------------------------------------------------------------------------------- /skellycam/utilities/get_process_memory_useage.py: -------------------------------------------------------------------------------- 1 | def get_memory_usage(): 2 | """Return current memory usage of this process in a human-readable format.""" 3 | import os 4 | import psutil 5 | 6 | # Get the current process 7 | process = psutil.Process(os.getpid()) 8 | 9 | # Get memory info in bytes 10 | memory_info = process.memory_info() 11 | 12 | # Convert to MB for readability 13 | rss_mb = memory_info.rss / (1024 * 1024) 14 | vms_mb = memory_info.vms / (1024 * 1024) 15 | 16 | return f"RSS: {rss_mb:.2f} MB, VMS: {vms_mb:.2f} MB" 17 | 18 | if __name__ == "__main__": 19 | print("Current process memory usage:") 20 | print(get_memory_usage()) -------------------------------------------------------------------------------- /skellycam-ui/src/layout/BaseContentRouter.tsx: -------------------------------------------------------------------------------- 1 | // skellycam-ui/src/layout/BaseContent.tsx 2 | import React from 'react'; 3 | import {Navigate, Route, Routes} from 'react-router-dom'; 4 | import {CamerasPage} from "@/pages/CamerasPage"; 5 | import VideosPage from "@/pages/VideosPage"; 6 | import WelcomePage from "@/pages/WelcomePage"; 7 | 8 | export const BaseContentRouter: React.FC = () => { 9 | return ( 10 | 11 | } /> 12 | }/> 13 | }/> 14 | }/> 15 | 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /skellycam-ui/src/components/update/Progress/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import './progress.css' 3 | 4 | const Progress: React.FC> = props => { 7 | const {percent = 0} = props 8 | 9 | return ( 10 |
11 |
12 |
16 |
17 | {(percent ?? 0).toString().substring(0, 4)}% 18 |
19 | ) 20 | } 21 | 22 | export default Progress 23 | -------------------------------------------------------------------------------- /skellycam/api/websocket/websocket_connect.py: -------------------------------------------------------------------------------- 1 | 2 | import logging 3 | 4 | from fastapi import APIRouter, WebSocket, Request 5 | 6 | from skellycam.api.websocket.websocket_server import WebsocketServer 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | websocket_router = APIRouter(tags=["Websocket"], prefix="/websocket") 11 | 12 | 13 | @websocket_router.websocket("/connect") 14 | async def websocket_server_connect(websocket: WebSocket): 15 | await websocket.accept() 16 | app = websocket.scope["app"] 17 | logger.success(f"Websocket connection established at url: {websocket.url}") 18 | async with WebsocketServer(websocket=websocket, 19 | app=app) as websocket_server: 20 | await websocket_server.run() 21 | logger.info("Websocket closed") 22 | -------------------------------------------------------------------------------- /skellycam-ui/src/services/electron-ipc/electron-ipc.ts: -------------------------------------------------------------------------------- 1 | 2 | // Check if running in Electron 3 | import {electronIpcClient} from "@/services/electron-ipc/electron-ipc-client"; 4 | 5 | export const isElectron = (): boolean => { 6 | return typeof window !== 'undefined' && !!window.electronAPI; 7 | }; 8 | 9 | // Export the API client or null if not in Electron 10 | export const electronIpc = isElectron() ? electronIpcClient : null; 11 | 12 | // Type-safe wrapper hook for React components 13 | import { useMemo } from 'react'; 14 | 15 | export function useElectronIPC() { 16 | const api = useMemo(() => { 17 | if (!isElectron()) return null; 18 | return electronIpcClient; 19 | }, []); 20 | 21 | return { 22 | isElectron: isElectron(), 23 | api, 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /skellycam/system/logging_configuration/log_levels.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from enum import Enum 3 | 4 | 5 | class LogLevels(Enum): 6 | ALL = logging.NOTSET # 0 # All logs, including those from third-party libraries 7 | LOOP = 3 # For logs that are printed in a loop (ONLY FOR DEBUGGING, REMOVE BEFORE PRODUCTION) 8 | TRACE = 5 # Low level logs for deep debugging 9 | DEBUG = logging.DEBUG # 10 # Detailed information for devs and curious folk 10 | INFO = logging.INFO # 20 # General information about the program 11 | SUCCESS = logging.INFO + 2 # 22 # OMG, something worked! 12 | API = logging.INFO + 5 # 25 # API calls/responses 13 | WARNING = logging.WARNING # 30 # Something unexpected happened, but it's necessarily an error 14 | ERROR = logging.ERROR # 40 # Something went wrong! 15 | -------------------------------------------------------------------------------- /skellycam-ui/src/store/store.ts: -------------------------------------------------------------------------------- 1 | import {configureStore} from "@reduxjs/toolkit"; 2 | import {framerateSlice} from "./slices/framerate/framerate-slice"; 3 | import {cameraSlice} from "./slices/cameras/cameras-slice"; 4 | import {recordingSlice} from "./slices/recording/recording-slice"; 5 | import {themeSlice} from "./slices/theme/theme-slice"; 6 | import {videosSlice} from "./slices/videos/videos-slice"; 7 | import {logRecordsSlice} from "./slices/log-records/log-records-slice"; 8 | 9 | export const store = configureStore({ 10 | reducer: { 11 | cameras: cameraSlice.reducer, 12 | recording: recordingSlice.reducer, 13 | framerate: framerateSlice.reducer, 14 | logs: logRecordsSlice.reducer, 15 | theme: themeSlice.reducer, 16 | videos: videosSlice.reducer, 17 | } 18 | }); 19 | 20 | 21 | -------------------------------------------------------------------------------- /skellycam/utilities/find_available_port.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import socket 3 | 4 | logger = logging.getLogger(__name__) 5 | 6 | 7 | def find_available_port(start_port: int) -> int: 8 | logger.debug(f"Finding available port starting at {start_port}") 9 | port = start_port 10 | while True: 11 | with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: 12 | logger.debug(f"Trying port {port}...") 13 | try: 14 | s.bind(("localhost", port)) 15 | logger.debug(f"Port {port} is available!") 16 | return port 17 | except socket.error as e: 18 | logger.debug(f"Port {port} is not available: `{e}`") 19 | port += 1 20 | if port > 65535: # No more ports available 21 | logger.error("No ports available!") 22 | raise 23 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Use a base image with Python and PowerShell 2 | FROM mcr.microsoft.com/windows/servercore:ltsc2022 3 | 4 | # Install Chocolatey and Rcedit 5 | RUN powershell -Command "Set-ExecutionPolicy Bypass -Scope Process -Force; \ 6 | iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))" 7 | RUN choco install rcedit -y 8 | 9 | # Install Python and Rust 10 | RUN choco install python --version=3.11 -y 11 | RUN choco install rust -y 12 | 13 | # Set the working directory 14 | WORKDIR /app 15 | 16 | # Copy the PowerShell script and your project files into the container 17 | chanCOPY installers/install-windows-pyapp.ps1 /app/ 18 | COPY installers /app/ 19 | 20 | # Run your PowerShell script 21 | RUN powershell -ExecutionPolicy Bypass -File install-windows-pyapp.ps1 22 | 23 | # Entry point to run the built executable 24 | ENTRYPOINT ["./skellycam-server.exe"] 25 | -------------------------------------------------------------------------------- /skellycam/system/logging_configuration/package_log_quieters.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | def suppress_noisy_package_logs(): 4 | # Suppress some external loggers that are too verbose for our context/taste 5 | logging.getLogger("tzlocal").setLevel(logging.WARNING) 6 | logging.getLogger("matplotlib").setLevel(logging.WARNING) 7 | logging.getLogger("httpx").setLevel(logging.WARNING) 8 | logging.getLogger("asyncio").setLevel(logging.WARNING) 9 | logging.getLogger("websockets").setLevel(logging.INFO) 10 | logging.getLogger("websocket").setLevel(logging.INFO) 11 | logging.getLogger("watchfiles").setLevel(logging.WARNING) 12 | logging.getLogger("httpcore").setLevel(logging.WARNING) 13 | logging.getLogger("urllib3").setLevel(logging.WARNING) 14 | logging.getLogger("comtypes").setLevel(logging.WARNING) 15 | logging.getLogger("uvicorn").setLevel(logging.WARNING) 16 | 17 | -------------------------------------------------------------------------------- /skellycam-ui/electron/main/services/logger.ts: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow } from 'electron'; 2 | import { exec } from 'child_process'; 3 | 4 | export class LifecycleLogger { 5 | static logProcessInfo() { 6 | console.log(` 7 | ============================================ 8 | Starting SkellyCam v${app.getVersion()} 9 | Platform: ${process.platform}-${process.arch} 10 | Node: ${process.versions.node} 11 | Chrome: ${process.versions.chrome} 12 | Electron: ${process.versions.electron} 13 | ============================================`); 14 | } 15 | 16 | static logWindowCreation(win: BrowserWindow) { 17 | console.log(`[Window Manager] Created window ID: ${win.id}`); 18 | } 19 | 20 | static logPythonProcess(pythonProcess: ReturnType) { 21 | console.log(`[Python Server] Started process PID: ${pythonProcess.pid}`); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /skellycam-ui/old_electron/main/helpers/logger.ts: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow } from 'electron'; 2 | import { exec } from 'child_process'; 3 | 4 | export class LifecycleLogger { 5 | static logProcessInfo() { 6 | console.log(` 7 | ============================================ 8 | Starting SkellyCam v${app.getVersion()} 9 | Platform: ${process.platform}-${process.arch} 10 | Node: ${process.versions.node} 11 | Chrome: ${process.versions.chrome} 12 | Electron: ${process.versions.electron} 13 | ============================================`); 14 | } 15 | 16 | static logWindowCreation(win: BrowserWindow) { 17 | console.log(`[Window Manager] Created window ID: ${win.id}`); 18 | } 19 | 20 | static logPythonProcess(pythonProcess: ReturnType) { 21 | console.log(`[Python Server] Started process PID: ${pythonProcess.pid}`); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /skellycam-ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": [ 6 | "DOM", 7 | "DOM.Iterable", 8 | "ESNext" 9 | ], 10 | "allowJs": false, 11 | "skipLibCheck": true, 12 | "esModuleInterop": false, 13 | "allowSyntheticDefaultImports": true, 14 | "strict": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "module": "ESNext", 17 | "moduleResolution": "Node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx", 22 | "baseUrl": "./", 23 | "paths": { 24 | "@/*": [ 25 | "src/*" 26 | ] 27 | } 28 | }, 29 | "include": [ 30 | "src", 31 | "old_electron", 32 | "src/lib/**/*" 33 | ], 34 | "references": [ 35 | { 36 | "path": "./tsconfig.node.json" 37 | } 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /skellycam-ui/src/components/ui-components/Footer.tsx: -------------------------------------------------------------------------------- 1 | // Update Copyright.tsx 2 | import Typography from "@mui/material/Typography"; 3 | import Link from "@mui/material/Link"; 4 | import * as React from "react"; 5 | import {useTheme} from "@mui/material"; 6 | 7 | export const Footer = function () { 8 | const theme = useTheme(); 9 | 10 | return ( 11 | 16 | {'w/ '} 17 | 18 | ❤️ 19 | {' from the '} 20 | 21 | FreeMoCap Foundation 22 | {' '} 23 | {new Date().getFullYear()} 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /skellycam/utilities/kill_process_on_port.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import psutil 4 | 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | 9 | def kill_process_on_port(port: int): 10 | for proc in psutil.process_iter(['pid', 'name']): 11 | if proc.info['pid'] == 0: 12 | # Skip kernel processes 13 | continue 14 | try: 15 | for conn in proc.connections(kind='inet'): 16 | if conn.laddr.port == port: 17 | logger.warning( 18 | f"Process already running on port: {port} (PID:{proc.info['pid']}), shutting it down...[TODO - HANDLE THIS BETTER! Figure out why we're leaving behind zombie processes...]") 19 | proc.kill() 20 | except psutil.AccessDenied or psutil.NoSuchProcess: 21 | continue 22 | 23 | 24 | if __name__ == "__main__": 25 | from skellycam.api.server_constants import PORT 26 | kill_process_on_port(PORT) 27 | -------------------------------------------------------------------------------- /skellycam-ui/src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | width: 100%; 3 | height: 100vh; 4 | overflow: hidden; 5 | } 6 | 7 | .logo-box { 8 | position: relative; 9 | height: 9em; 10 | } 11 | 12 | .logo { 13 | position: absolute; 14 | left: calc(50% - 4.5em); 15 | height: 6em; 16 | padding: 1.5em; 17 | will-change: filter; 18 | transition: filter 300ms; 19 | } 20 | 21 | .logo:hover { 22 | filter: drop-shadow(0 0 2em #646cffaa); 23 | } 24 | 25 | @keyframes logo-spin { 26 | from { 27 | transform: rotate(0deg); 28 | } 29 | to { 30 | transform: rotate(360deg); 31 | } 32 | } 33 | 34 | @media (prefers-reduced-motion: no-preference) { 35 | .logo.electron { 36 | animation: logo-spin infinite 20s linear; 37 | } 38 | } 39 | 40 | .card { 41 | padding: 2em; 42 | } 43 | 44 | .read-the-docs { 45 | color: #888; 46 | } 47 | 48 | .flex-center { 49 | display: flex; 50 | align-items: center; 51 | justify-content: center; 52 | } 53 | -------------------------------------------------------------------------------- /skellycam-ui/src/store/slices/log-records/logs-types.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const LogRecordSchema = z.object({ 4 | name: z.string(), 5 | msg: z.string().nullable().default(""), 6 | args: z.array(z.any()), 7 | levelname: z.string(), 8 | levelno: z.number(), 9 | pathname: z.string(), 10 | filename: z.string(), 11 | module: z.string(), 12 | exc_info: z.string().nullable(), 13 | exc_text: z.string().nullable(), 14 | stack_info: z.string().nullable(), 15 | lineno: z.number(), 16 | funcName: z.string(), 17 | created: z.number(), 18 | msecs: z.number(), 19 | relativeCreated: z.number(), 20 | thread: z.number(), 21 | threadName: z.string(), 22 | processName: z.string(), 23 | process: z.number(), 24 | delta_t: z.string(), 25 | message: z.string(), 26 | asctime: z.string(), 27 | formatted_message: z.string(), 28 | type: z.string(), 29 | }); 30 | 31 | export type LogRecord = z.infer; 32 | -------------------------------------------------------------------------------- /skellycam-ui/src/store/thunks/close-cameras-thunks.ts: -------------------------------------------------------------------------------- 1 | import {createAsyncThunk} from '@reduxjs/toolkit'; 2 | import {useAppUrls} from "@/hooks/useAppUrls"; 3 | 4 | 5 | 6 | export const closeCameras = createAsyncThunk( 7 | 'cameras/close', 8 | async () => { 9 | console.log(`Closing cameras...`); 10 | try { 11 | 12 | const closeCamerasURL = useAppUrls.getHttpEndpointUrls().closeAll 13 | 14 | 15 | console.log(`Sending close request to ${closeCamerasURL}`); 16 | const response = await fetch(closeCamerasURL, { 17 | method: 'DELETE', 18 | }); 19 | 20 | if (response.ok) { 21 | console.log('Cameras closed successfully'); 22 | } else { 23 | throw new Error(`Failed to close cameras: ${response.statusText}`); 24 | } 25 | } catch (error) { 26 | console.error('Recording start failed:', error); 27 | throw error; 28 | } 29 | } 30 | ); 31 | -------------------------------------------------------------------------------- /skellycam-ui/src/store/slices/videos/videos-selectors.ts: -------------------------------------------------------------------------------- 1 | import { createSelector } from '@reduxjs/toolkit'; 2 | import { RootState } from '../../types'; 3 | 4 | export const selectVideoFolder = (state: RootState) => state.videos.folder; 5 | export const selectVideoFiles = (state: RootState) => state.videos.files; 6 | export const selectSelectedVideo = (state: RootState) => state.videos.selectedFile; 7 | export const selectVideoPlaybackState = (state: RootState) => state.videos.playbackState; 8 | export const selectVideoLoadingState = (state: RootState) => state.videos.isLoading; 9 | export const selectVideoError = (state: RootState) => state.videos.error; 10 | 11 | export const selectVideosByExtension = createSelector( 12 | [selectVideoFiles, (_: RootState, extension: string) => extension], 13 | (files, extension) => 14 | files.filter((file: { name: string; }) => file.name.toLowerCase().endsWith(extension)) 15 | ); 16 | 17 | export const selectIsVideoPlaying = createSelector( 18 | [selectVideoPlaybackState], 19 | (playback) => playback.isPlaying 20 | ); 21 | -------------------------------------------------------------------------------- /skellycam-ui/src/layout/AppContent.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import {ThemeProvider} from '@mui/material/styles'; 3 | import {HashRouter} from 'react-router-dom'; 4 | import {CssBaseline} from "@mui/material"; 5 | import {BasePanelLayout} from "@/layout/BasePanelLayout"; 6 | import {createExtendedTheme} from "@/layout/paperbase-theme"; 7 | import {useAppSelector} from "@/store"; 8 | import {BaseContentRouter} from "@/layout/BaseContentRouter"; 9 | 10 | export const AppContent = function () { 11 | 12 | const themeMode = useAppSelector(state => state.theme.mode); 13 | // Create theme dynamically based on current mode 14 | 15 | const theme = React.useMemo(() => 16 | createExtendedTheme(themeMode), 17 | [themeMode] 18 | ); 19 | return ( 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /skellycam/utilities/check_main_processs_heartbeat.py: -------------------------------------------------------------------------------- 1 | import multiprocessing 2 | import time 3 | 4 | import logging 5 | from typing import Final 6 | 7 | logger = logging.getLogger(__name__) 8 | HEARTBEAT_INTERVAL_SECONDS: Final[float] = 1.0 9 | HEARTBEAT_TIMEOUT_SECONDS: Final[float] = 300.0 10 | def check_main_process_heartbeat(*, 11 | heartbeat_timestamp: multiprocessing.Value, 12 | global_kill_flag: multiprocessing.Value) -> bool: 13 | """Check if main process is still alive based on heartbeat.""" 14 | current_time: float = time.perf_counter() 15 | last_heartbeat: float = heartbeat_timestamp.value 16 | 17 | time_since_heartbeat: float = current_time - last_heartbeat 18 | 19 | if time_since_heartbeat > HEARTBEAT_TIMEOUT_SECONDS: 20 | logger.error( 21 | f"Main process heartbeat timeout! " 22 | f"Last heartbeat was {time_since_heartbeat:.1f}s ago " 23 | f"(timeout: {HEARTBEAT_TIMEOUT_SECONDS}s)" 24 | ) 25 | global_kill_flag.value = True 26 | return False 27 | 28 | return True -------------------------------------------------------------------------------- /skellycam/core/camera/opencv/opencv_helpers/create_initial_frame_recarray.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from skellycam.core.camera.config.camera_config import CameraConfig 4 | from skellycam.core.camera_group.camera_group_ipc import CameraGroupIPC 5 | from skellycam.core.types.numpy_record_dtypes import create_frame_dtype 6 | 7 | 8 | def create_initial_frame_rec_array(config: CameraConfig, ipc: CameraGroupIPC) -> np.recarray: 9 | # Create initial frame record array 10 | frame_dtype = create_frame_dtype(config) 11 | frame_rec_array = np.recarray(1, dtype=frame_dtype) 12 | # Initialize the frame metadata 13 | 14 | frame_rec_array.frame_metadata.camera_config[0] = config.to_numpy_record_array() 15 | frame_rec_array.frame_metadata.frame_number[0] = -1 16 | frame_rec_array.frame_metadata.timebase_mapping[0] = ipc.timebase_mapping.to_numpy_record_array() 17 | # Initialize the image with zeros 18 | image_shape = (config.resolution.height, config.resolution.width, config.color_channels) 19 | frame_rec_array.image[0] = np.zeros(image_shape, dtype=np.uint8) + config.camera_index 20 | return frame_rec_array 21 | -------------------------------------------------------------------------------- /skellycam-ui/electron/main/ipc.ts: -------------------------------------------------------------------------------- 1 | // electron/main/ipc.ts 2 | import { ipcMain } from 'electron'; 3 | import { api } from './api'; 4 | import superjson from 'superjson'; 5 | 6 | export function setupIPC(): void { 7 | ipcMain.handle('trpc', async (_event, { path, input }) => { 8 | try { 9 | const caller = api.createCaller({}); 10 | 11 | const pathParts = path.split('.'); 12 | let current: any = caller; 13 | 14 | for (let i = 0; i < pathParts.length - 1; i++) { 15 | current = current[pathParts[i]]; 16 | if (!current) { 17 | throw new Error(`Router not found: ${pathParts.slice(0, i + 1).join('.')}`); 18 | } 19 | } 20 | 21 | const procedureName = pathParts[pathParts.length - 1]; 22 | const fn = current[procedureName]; 23 | 24 | if (typeof fn !== 'function') { 25 | throw new Error(`Procedure not found: ${path}`); 26 | } 27 | 28 | const result = await fn(input); 29 | return superjson.serialize(result); 30 | } catch (error) { 31 | console.error(`IPC Error for ${path}:`, error); 32 | throw error; 33 | } 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /skellycam-ui/src/layout/paperbase_theme/PaperbaseContent.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import {ThemeProvider} from '@mui/material/styles'; 3 | import {HashRouter} from 'react-router-dom'; 4 | import {CssBaseline} from "@mui/material"; 5 | import {BasePanelLayout} from "@/layout/BasePanelLayout"; 6 | import {createExtendedTheme} from "@/layout/paperbase_theme/paperbase-theme"; 7 | import {useAppSelector} from "@/store/AppStateStore"; 8 | import {BaseContentRouter} from "@/layout/BaseContentRouter"; 9 | 10 | export const PaperbaseContent = function () { 11 | 12 | const themeMode = useAppSelector(state => state.theme.mode); 13 | // Create theme dynamically based on current mode 14 | 15 | const theme = React.useMemo(() => 16 | createExtendedTheme(themeMode), 17 | [themeMode] 18 | ); 19 | return ( 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /skellycam-ui/src/components/ui-components/Header.tsx: -------------------------------------------------------------------------------- 1 | // Update Header.tsx 2 | import * as React from 'react'; 3 | import AppBar from '@mui/material/AppBar'; 4 | import Toolbar from '@mui/material/Toolbar'; 5 | import Typography from '@mui/material/Typography'; 6 | import {Box, useTheme} from '@mui/material'; 7 | import ThemeToggle from './ThemeToggle'; 8 | 9 | export const Header = function () { 10 | const theme = useTheme(); 11 | 12 | return ( 13 | 22 | 23 | 24 | SkellyCam 💀📸 25 | 26 | 27 | 28 | 29 | 30 | 31 | ); 32 | } 33 | 34 | export default Header; 35 | -------------------------------------------------------------------------------- /skellycam/api/http/ui/ui_router.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | from pathlib import Path 4 | 5 | from fastapi import APIRouter 6 | from skellycam.api.server import PORT 7 | from starlette.responses import HTMLResponse 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | ui_router = APIRouter(tags=['ui'], prefix='/ui') 12 | 13 | 14 | @ui_router.get("/skellycam.__package_name__", response_class=HTMLResponse) 15 | def serve_ui(): 16 | logger.info("Serving UI HTML to `/ui`") 17 | file_path = os.path.join(os.path.dirname(__file__), 'ui.html') 18 | with open(file_path, 'r', encoding='utf-8') as file: 19 | ui_html_string = file.read() 20 | # ui_html_string = ui_html_string.replace("{{HTTP_URL}}", APP_URL) 21 | # ui_html_string = ui_html_string.replace("{{WEBSOCKET_URL}}", APP_URL.replace("http", "ws")) 22 | ui_html_string = ui_html_string.replace("{{SKELLYCAM_DATA_FOLDER}}", str(Path().home()/"skellycam_data")).replace("\\", "/") 23 | return HTMLResponse(content=ui_html_string, status_code=200) 24 | 25 | 26 | if __name__ == "__main__": 27 | import uvicorn 28 | 29 | uvicorn.run(ui_router, host="localhost", port=PORT) 30 | -------------------------------------------------------------------------------- /skellycam-ui/src/components/camera-config-tree-view/NoCamerasPlaceholder.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Box, Typography } from "@mui/material"; 3 | import { TreeItem } from "@mui/x-tree-view/TreeItem"; 4 | 5 | export const NoCamerasPlaceholder: React.FC = () => { 6 | return ( 7 | 17 | 18 | No cameras detected 19 | 20 | 25 | Click refresh to scan for available cameras 26 | 27 | 28 | } 29 | /> 30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /skellycam/utilities/check_ffmpeg_and_h264_codec.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | 3 | 4 | def check_ffmpeg(): 5 | try: 6 | output = subprocess.check_output(['ffmpeg', '-version'], stderr=subprocess.STDOUT) 7 | print("FFmpeg is installed.") 8 | except subprocess.CalledProcessError as e: 9 | print("FFmpeg is not installed or not accessible.") 10 | return False 11 | return True 12 | 13 | 14 | def check_h264_support(): 15 | try: 16 | output = subprocess.check_output(['ffmpeg', '-codecs'], stderr=subprocess.STDOUT) 17 | if b'libx264' in output: 18 | print("H.264 codec (libx264) is available.") 19 | return True 20 | else: 21 | print("H.264 codec (libx264) is not available.") 22 | return False 23 | except subprocess.CalledProcessError as e: 24 | print("Error checking FFmpeg codecs:", e.output.decode()) 25 | return False 26 | 27 | 28 | if __name__ == "__main__": 29 | if check_ffmpeg() and check_h264_support(): 30 | print("System is properly configured for H.264 encoding.") 31 | else: 32 | print("System is not properly configured for H.264 encoding.") 33 | -------------------------------------------------------------------------------- /skellycam-ui/electron-builder.json: -------------------------------------------------------------------------------- 1 | { 2 | "appId": "skellycam.app", 3 | "directories": { 4 | "output": "release/${version}" 5 | }, 6 | "files": [ 7 | "dist-electron", 8 | "dist", 9 | "skellycam_server.exe", 10 | "../shared/skellycam-logo/skellycam-favicon.ico", 11 | "../shared/skellycam-logo/skellycam-logo.png" 12 | ], 13 | "icon": "../shared/skellycam-logo/skellycam-favicon.ico", 14 | "asar": true, 15 | "asarUnpack": [ 16 | "**/skellycam_server.exe", 17 | "**/skellycam-logo.png" 18 | ], 19 | "mac": { 20 | "artifactName": "${productName}_${version}_installer.${ext}", 21 | "target": [ 22 | "dmg", 23 | "zip" 24 | ] 25 | }, 26 | "win": { 27 | "target": [ 28 | { 29 | "target": "nsis", 30 | "arch": [ 31 | "x64" 32 | ] 33 | } 34 | ], 35 | "artifactName": "${productName}_${version}_installer.${ext}" 36 | }, 37 | "nsis": { 38 | "oneClick": false, 39 | "perMachine": false, 40 | "allowToChangeInstallationDirectory": true, 41 | "deleteAppDataOnUninstall": false 42 | }, 43 | "publish": { 44 | "provider": "generic", 45 | "channel": "latest", 46 | "url": "https://github.com/freemocap/freemocap" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /skellycam-ui/src/components/update/README.zh-CN.md: -------------------------------------------------------------------------------- 1 | # electron-auto-update 2 | 3 | [English](README.md) | 简体中文 4 | 5 | 使用`electron-updater`实现electron程序的更新检测、下载和安装等功能。 6 | 7 | ```sh 8 | npm i electron-updater 9 | ``` 10 | 11 | ### 主要逻辑 12 | 13 | 1. ##### 更新地址、更新信息脚本的配置 14 | 15 | 在`electron-builder.json5`添加`publish`字段,用来配置更新地址和使用哪种策略作为更新服务 16 | 17 | ``` json5 18 | { 19 | "publish": { 20 | "provider": "generic", // 提供者、提供商 21 | "channel": "latest", // 生成yml文件的名称 22 | "url": "https://foo.com/" //更新地址 23 | } 24 | } 25 | ``` 26 | 27 | 更多见 : [electron-builder.json5...](xxx) 28 | 29 | 2. ##### Electron更新逻辑 30 | 31 | - 检测更新是否可用; 32 | 33 | - 检测服务端的软件版本; 34 | 35 | - 检测更新是否可用; 36 | 37 | - 下载服务端新版软件(当更新可用); 38 | - 安装方式; 39 | 40 | 更多见 : [update...](https://github.com/electron-vite/electron-vite-react/blob/main/electron/main/update.ts) 41 | 42 | 3. ##### Electron更新UI页面 43 | 44 | 主要功能是:用户触发上述(2.)更新逻辑的UI页面。用户可以通过点击页面触发electron更新的不同功能。 45 | 更多见 : [components/update.ts...](https://github.com/electron-vite/electron-vite-react/tree/main/src/components/update/index.tsx) 46 | 47 | --- 48 | 49 | 这里建议更新触发以用户操作触发(本项目的以用户点击 **更新检测** 后触发electron更新功能) 50 | 51 | 关于更多使用`electron-updater`进行electron更新,见文档:[auto-update](https://www.electron.build/.html) 52 | -------------------------------------------------------------------------------- /skellycam/core/camera_group/timestamps/numpy_timestamps/save_timestamps_statistics_summary.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import numpy as np 4 | 5 | from skellycam.core.camera_group.timestamps.recording_timestamp_stats import RecordingTimestampsStats 6 | from skellycam.core.recorders.videos.recording_info import RecordingInfo 7 | 8 | logger = logging.getLogger(__name__) 9 | def save_timestamp_statistics_summary( 10 | multiframe_rows_recarray: np.recarray, 11 | recording_info: RecordingInfo, 12 | number_of_cameras: int,) -> RecordingTimestampsStats: 13 | stats = RecordingTimestampsStats.from_multiframe_rows( 14 | multiframe_rows=multiframe_rows_recarray, 15 | recording_info=recording_info, 16 | number_of_cameras=number_of_cameras, 17 | ) 18 | 19 | stats_json_path = recording_info.timestamp_stats_json_file_path 20 | 21 | with open(stats_json_path, 'w', encoding='utf-8') as f: 22 | f.write(stats.to_json()) 23 | # logger.debug(f"Saved timestamp statistics to {stats_json_path}") 24 | 25 | stats_text_path = recording_info.timestamp_stats_text_file_path 26 | with open(stats_text_path, 'w', encoding='utf-8') as f: 27 | f.write(str(stats)) 28 | logger.debug(f"Saved timestamp statistics summary to {stats_text_path}") 29 | return stats 30 | 31 | -------------------------------------------------------------------------------- /skellycam/utilities/wait_functions.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import time 3 | 4 | 5 | def wait_1s(): 6 | time.sleep(1.0) 7 | 8 | 9 | def wait_100ms(): 10 | time.sleep(0.1) 11 | 12 | 13 | def wait_10ms(): 14 | time.sleep(1e-2) 15 | 16 | def wait_30ms(): 17 | """ 18 | Once per frame-ish 19 | """ 20 | wait_10ms() 21 | wait_10ms() 22 | wait_10ms() 23 | 24 | 25 | def wait_1ms(): 26 | time.sleep(1e-3) 27 | 28 | def wait_100us(): 29 | time.sleep(1e-4) 30 | 31 | def wait_10us(): 32 | time.sleep(1e-5) 33 | 34 | def wait_1us(): 35 | time.sleep(1e-6) 36 | 37 | 38 | 39 | async def await_1s(): 40 | await asyncio.sleep(1.0) 41 | 42 | 43 | async def await_100ms(): 44 | await asyncio.sleep(1e-1) 45 | 46 | async def await_10ms(): 47 | await asyncio.sleep(1e-2) 48 | 49 | 50 | async def await_1ms(): 51 | await asyncio.sleep(1e-3) 52 | 53 | 54 | 55 | if __name__ == "__main__": 56 | print("Testing wait functions") 57 | 58 | tic = time.perf_counter_ns() 59 | for i in range(1000): 60 | wait_1ms() 61 | toc = time.perf_counter_ns() 62 | print(f"WAITED 1ms {1000} times in {(toc - tic)/1e9} s") 63 | 64 | tic = time.perf_counter_ns() 65 | for i in range(1000): 66 | wait_10ms() 67 | toc = time.perf_counter_ns() 68 | print(f"WAITED 10ms {1000} times in {(toc - tic)/1e9} s") 69 | 70 | 71 | -------------------------------------------------------------------------------- /skellycam-ui/src/components/ui-components/ThemeToggle.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { IconButton, Tooltip, useTheme } from '@mui/material'; 3 | import Brightness4Icon from '@mui/icons-material/Brightness4'; // Dark mode icon 4 | import Brightness7Icon from '@mui/icons-material/Brightness7'; // Light mode icon 5 | import { useAppDispatch, useAppSelector } from '@/store'; 6 | import { themeModeToggled } from '@/store/slices/theme'; 7 | 8 | export const ThemeToggle: React.FC = () => { 9 | const dispatch = useAppDispatch(); 10 | const theme = useTheme(); 11 | const themeMode = useAppSelector(state => state.theme.mode); 12 | const isDarkMode = themeMode === 'dark'; 13 | 14 | return ( 15 | 16 | dispatch(themeModeToggled())} 18 | color="inherit" 19 | aria-label="toggle theme" 20 | edge="end" 21 | sx={{ 22 | '&:hover': { 23 | backgroundColor: theme.palette.action.hover, 24 | }, 25 | }} 26 | > 27 | {isDarkMode ? : } 28 | 29 | 30 | ); 31 | }; 32 | 33 | export default ThemeToggle; 34 | -------------------------------------------------------------------------------- /skellycam/core/types/type_overloads.py: -------------------------------------------------------------------------------- 1 | import enum 2 | import multiprocessing 3 | import threading 4 | 5 | import numpy as np 6 | from pydantic import SkipValidation 7 | 8 | CameraIdString = str 9 | CameraGroupIdString = str 10 | CameraNameString = str 11 | SharedMemoryName = str 12 | CameraIndexInt = int 13 | CameraBackendInt = int # OpenCV backend ID, e.g., cv2.CAP_ANY, cv2.CAP_MSMF, cv2.CAP_DSHOW, etc. 14 | CameraBackendNameString = str # Name of the backend, e.g., "MSMF", "DShow", etc. 15 | CameraVendorIdInt = int # Vendor ID of the camera, e.g., 0x046D for Logitech 16 | CameraProductIdInt = int # Product ID of the camera, e.g., 0x0825 for Logitech C920 17 | CameraDevicePathString = str # Path to the camera device, e.g., "/dev/video0" on Linux or "COM3"/ on Windows 18 | FrameNumberInt = int 19 | MultiframeTimestampFloat = float|np.floating # Mean timestamp of the frames in a multiframe (converted to milliseconds) 20 | Base64JPEGImage = str # Base64 encoded JPEG image 21 | RecordingManagerIdString = str 22 | TopicSubscriptionQueue = SkipValidation[multiprocessing.Queue] 23 | TopicPublicationQueue = SkipValidation[multiprocessing.Queue] 24 | 25 | WorkerType = SkipValidation[threading.Thread | multiprocessing.Process] 26 | 27 | IMAGE_DATA_DTYPE = np.uint8 28 | BYTES_PER_MONO_PIXEL = np.dtype(IMAGE_DATA_DTYPE).itemsize 29 | 30 | class WorkerStrategy(enum.Enum): 31 | THREAD = threading.Thread 32 | PROCESS = multiprocessing.Process 33 | 34 | -------------------------------------------------------------------------------- /skellycam/system/logging_configuration/formatters/color_formatter.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from copy import deepcopy 3 | 4 | from .custom_formatter import CustomFormatter 5 | from ..log_format_string import LOG_POINTER_STRING 6 | from ..logging_color_helpers import get_hashed_color 7 | 8 | LOG_COLOR_CODES = { 9 | "LOOP": "\033[90m", # Grey 10 | "TRACE": "\033[37m", # White 11 | "DEBUG": "\033[34m", # Blue 12 | "INFO": "\033[96m", # Cyan 13 | "SUCCESS": "\033[95m", # Magenta 14 | "API": "\033[92m", # Green 15 | "WARNING": "\033[33m", # Yellow 16 | "ERROR": "\033[41m", # Red background 17 | } 18 | class ColorFormatter(CustomFormatter): 19 | """Adds ANSI colors to PID, TID, and log messages""" 20 | 21 | def format(self, record: logging.LogRecord) -> str: 22 | # Apply PID/TID colors 23 | record = deepcopy(record) 24 | record.pid_color = get_hashed_color(record.process) 25 | record.tid_color = get_hashed_color(record.thread) 26 | 27 | # Apply level color to message 28 | level_color = LOG_COLOR_CODES.get(record.levelname, "") 29 | record.msg = f"{level_color}{record.msg}\033[0m" 30 | 31 | # Format with parent class 32 | formatted = super().format(record) 33 | formatted = f"{level_color}{formatted}\033[0m" 34 | # Add color to structural elements 35 | return formatted.replace(LOG_POINTER_STRING, f"{level_color}{LOG_POINTER_STRING}\033[0m") -------------------------------------------------------------------------------- /installers/nuitka_scripts/nuitka_installer_notes.md: -------------------------------------------------------------------------------- 1 | # Nuitka Installer Commands 2 | 3 | Copy-paste these commands into your terminal to build the SkellyCam server with Nuitka. 4 | 5 | > run `nuitka --help` for more information on the options used here. 6 | 7 | run from /skellycam-ui folder: 8 | 9 | ## Windows 10 | 11 | ``` 12 | nuitka --onefile --windows-icon-from-ico=../shared/skellycam-logo/skellycam-favicon.ico --user-package-configuration-file=../installers/nuitka_scripts/skellycam-nuitka.config.yml --remove-output --output-filename=skellycam_server.exe ../skellycam/__main__.py 13 | ``` 14 | 15 | ## Mac 16 | ``` 17 | nuitka --onefile --macos-create-app-bundle=1 --macos-app-icon=shared/skellycam-logo/skellycam-favicon.ico --user-package-configuration-file=installers/skellycam-nuitka.config.yml --output-filename=skellycam-ui/skellycam_server.exe skellycam/__main__.py 18 | ``` 19 | ## Linux 20 | ``` 21 | nuitka --onefile --linux-icon=shared/skellycam-logo/skellycam-favicon.ico --user-package-configuration-file=installers/skellycam-nuitka.config.yml skellycam/__main__.py 22 | ``` 23 | ___ 24 | 25 | ## DEBUG OPTIONS 26 | 27 | # --report=compilation-report.xml 28 | 29 | ## not working yet 30 | 31 | ### --onefile-windows-splash-screen-image=shared/skellycam-logo/skellycam-logo.png 32 | 33 | - makes a giant splash screen that covers the whole screen and never goes away lol 34 | - The docs have instructions on how to turn off the splash screen when the app starts up 35 | -------------------------------------------------------------------------------- /skellycam/api/http/app/shutdown.py: -------------------------------------------------------------------------------- 1 | """ 2 | Clean shutdown endpoint with proper async handling. 3 | """ 4 | import logging 5 | import os 6 | import signal 7 | 8 | from fastapi import APIRouter, Request 9 | from fastapi.responses import JSONResponse 10 | 11 | from skellycam.utilities.wait_functions import await_100ms 12 | 13 | logger = logging.getLogger(__name__) 14 | 15 | shutdown_router = APIRouter(tags=["App"]) 16 | 17 | 18 | @shutdown_router.get( 19 | "/shutdown", 20 | summary="Gracefully shutdown the server", 21 | response_model=dict[str, str] 22 | ) 23 | async def shutdown_server( 24 | request: Request, 25 | ) -> JSONResponse: 26 | """ 27 | Initiate graceful server shutdown. 28 | 29 | This endpoint triggers a graceful shutdown of the entire SkellyCam system, 30 | including all camera groups and the server itself. 31 | 32 | Returns: 33 | JSON response confirming shutdown initiation 34 | """ 35 | logger.api(f"Shutdown requested via API - {request.url}") 36 | 37 | # Send SIGTERM to ourselves - this triggers the existing shutdown flow 38 | request.app.state.global_kill_flag.value = True 39 | await await_100ms() 40 | os.kill(os.getpid(), signal.SIGTERM) 41 | logger.info("Sent SIGTERM signal to initiate shutdown") 42 | return JSONResponse( 43 | content={ 44 | "status": "shutdown_initiated", 45 | "message": "Server shutting down. Goodbye! 👋" 46 | }, 47 | status_code=200 48 | ) 49 | -------------------------------------------------------------------------------- /skellycam/core/camera/opencv/opencv_helpers/determine_backend.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from platform import platform 3 | 4 | import cv2 5 | from cv2.videoio_registry import getBackendName 6 | from cv2_enumerate_cameras import supported_backends 7 | from pydantic import BaseModel 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | class OpenCVBackend(BaseModel): 13 | id: int 14 | name: str 15 | 16 | @classmethod 17 | def from_backend_id(cls, backend_id: int) -> 'OpenCVBackend': 18 | name = getBackendName(backend_id) 19 | if name is None: 20 | logger.warning(f"Unknown OpenCV backend ID: {backend_id}. Defaulting to cv2.CAP_ANY.") 21 | backend_id = cv2.CAP_ANY 22 | name = getBackendName(backend_id) 23 | return cls(id=backend_id, name=name) 24 | 25 | 26 | def determine_opencv_camera_backend() -> OpenCVBackend: 27 | if "windows" in platform().lower(): 28 | # TODO - Try MSMF? We've used CAP_DSHOW for a long time, but MSMF is the default on Windows 10+ so may be worth trying. 29 | backend = OpenCVBackend.from_backend_id(cv2.CAP_DSHOW) 30 | else: 31 | backend = OpenCVBackend.from_backend_id(supported_backends[0] if len(supported_backends) > 0 else cv2.CAP_ANY) 32 | logger.debug(f"Determined OpenCV backend: {backend.name} (ID: {backend.id})") 33 | return backend 34 | 35 | 36 | if __name__ == "__main__": 37 | b = determine_opencv_camera_backend() 38 | print(f"OpenCV Backend: {b.name} (ID: {b.id})") 39 | -------------------------------------------------------------------------------- /skellycam-ui/src/components/camera-config-tree-view/CameraConfigTreeSection.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { TreeItem } from "@mui/x-tree-view/TreeItem"; 3 | import { useAppDispatch } from "@/store"; 4 | import { cameraDesiredConfigUpdated } from "@/store/slices/cameras/cameras-slice"; 5 | import { Camera, CameraConfig } from "@/store/slices/cameras/cameras-types"; 6 | import {CameraConfigPanel} from "@/components/camera-config-panel/CameraConfigPanel"; 7 | 8 | interface CameraConfigTreeSectionProps { 9 | camera: Camera; 10 | } 11 | 12 | export const CameraConfigTreeSection: React.FC = ({ 13 | camera, 14 | }) => { 15 | const dispatch = useAppDispatch(); 16 | 17 | const handleConfigChange = (newConfig: CameraConfig): void => { 18 | dispatch( 19 | cameraDesiredConfigUpdated({ 20 | cameraId: camera.id, 21 | config: newConfig, 22 | }) 23 | ); 24 | }; 25 | 26 | return ( 27 | 35 | } 36 | /> 37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /skellycam/system/logging_configuration/log_test_messages.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | 4 | def log_test_messages(logger: logging.Logger): 5 | logger.loop("This is a LOOP message, value `4` -> For logs that are printed in a loop") 6 | logger.trace("This is a TRACE message, value `5` -> Low level logs for deep debugging") 7 | logger.debug("This is a DEBUG message, value `10` -> Detailed information for devs and curious folk") 8 | logger.info("This is an INFO message, value `20` -> General information about the program") 9 | logger.success("This is a SUCCESS message, value `22` -> OMG, something worked!") 10 | logger.api("This is an IMPORTANT message, value `25` -> API calls/responses") 11 | logger.warning( 12 | "This is a WARNING message, value `30` -> Something unexpected happened, but it's necessarily an error" 13 | ) 14 | logger.error("This is an ERROR message, value `40` -> Something went wrong!") 15 | 16 | print("----------This is a regular ol' print message.------------------") 17 | 18 | import time 19 | 20 | iters = 10 21 | for iter in range(1, iters + 1): 22 | wait_time = iter / 10 23 | logger.loop("Starting timers loop (Δt should probably be near 0, unless you've got other stuff going on)") 24 | tic = time.perf_counter_ns() 25 | time.sleep(wait_time) 26 | toc = time.perf_counter_ns() 27 | elapsed_time = (toc - tic) / 1e9 28 | logger.trace(f"Done {wait_time} sec timer - elapsed time:{elapsed_time}s (Δt should be ~{wait_time}s)") 29 | -------------------------------------------------------------------------------- /skellycam-ui/src/store/slices/framerate/framerate-types.ts: -------------------------------------------------------------------------------- 1 | // framerate-types.ts 2 | import { z } from 'zod'; 3 | 4 | // The actual framerate data structure used in the Redux store 5 | export const FramerateDataSchema = z.object({ 6 | mean: z.number(), 7 | std: z.number(), 8 | current: z.number(), 9 | }); 10 | 11 | export type FramerateData = z.infer; 12 | 13 | // If you have a different schema from the backend with more detailed stats, 14 | // you can keep it separate for parsing/validation but transform it to FramerateData 15 | export const DetailedFramerateSchema = z.object({ 16 | mean_frame_duration_ms: z.number(), 17 | mean_frames_per_second: z.number(), 18 | frame_duration_max: z.number(), 19 | frame_duration_min: z.number(), 20 | frame_duration_mean: z.number(), 21 | frame_duration_stddev: z.number(), 22 | frame_duration_median: z.number(), 23 | frame_duration_coefficient_of_variation: z.number(), 24 | calculation_window_size: z.number(), 25 | framerate_source: z.string(), 26 | }); 27 | 28 | export type DetailedFramerate = z.infer; 29 | 30 | // Helper function to convert detailed framerate to simple framerate data 31 | export function detailedToSimpleFramerate(detailed: DetailedFramerate): FramerateData { 32 | return { 33 | mean: detailed.mean_frames_per_second, 34 | std: detailed.frame_duration_stddev, 35 | current: detailed.mean_frames_per_second, // or calculate from mean_frame_duration_ms 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /skellycam/core/recorders/recording_manager_status.py: -------------------------------------------------------------------------------- 1 | import multiprocessing 2 | 3 | from pydantic import BaseModel, ConfigDict, SkipValidation, Field 4 | 5 | 6 | class RecordingManagerStatus(BaseModel): 7 | model_config = ConfigDict(arbitrary_types_allowed=True) 8 | is_recording_frames_flag: SkipValidation[multiprocessing.Value] = Field( 9 | default_factory=lambda: multiprocessing.Value('b', False)) 10 | should_record: SkipValidation[multiprocessing.Value] = Field( 11 | default_factory=lambda: multiprocessing.Value('b', False)) 12 | is_running_flag: SkipValidation[multiprocessing.Value] = Field( 13 | default_factory=lambda: multiprocessing.Value('b', False)) 14 | should_pause: SkipValidation[multiprocessing.Value] = Field(default_factory=lambda: multiprocessing.Value('b', False)) 15 | is_paused: SkipValidation[multiprocessing.Value] = Field(default_factory=lambda: multiprocessing.Value('b', False)) 16 | finishing: SkipValidation[multiprocessing.Value] = Field(default_factory=lambda: multiprocessing.Value('b', False)) 17 | updating: SkipValidation[multiprocessing.Value] = Field(default_factory=lambda: multiprocessing.Value('b', False)) 18 | closed: SkipValidation[multiprocessing.Value] = Field(default_factory=lambda: multiprocessing.Value('b', False)) 19 | error: SkipValidation[multiprocessing.Value] = Field(default_factory=lambda: multiprocessing.Value('b', False)) 20 | 21 | @property 22 | def recording(self) -> bool: 23 | return self.is_recording_frames_flag.value and self.should_record.value 24 | -------------------------------------------------------------------------------- /skellycam-ui/old_electron/main/index.ts: -------------------------------------------------------------------------------- 1 | import {app, BrowserWindow} from 'electron'; 2 | import {update} from './update'; 3 | import {IpcManager} from "./helpers/ipc-manager"; 4 | import {WindowManager} from "./helpers/window-manager"; 5 | import {PythonServer} from "./helpers/python-server"; 6 | import {LifecycleLogger} from "./helpers/logger"; 7 | import os from "node:os"; 8 | 9 | 10 | // Environment variables that `python` server will use for its lifecycle management 11 | process.env.SKELLYCAM_RUNNING_IN_ELECTRON = 'true'; 12 | 13 | // Platform config 14 | // Disable GPU Acceleration for Windows 7 15 | if (os.release().startsWith('6.1')) app.disableHardwareAcceleration() 16 | if (process.platform === 'win32') app.setAppUserModelId(app.getName()); 17 | 18 | // Initialization Sequence 19 | function startApplication() { 20 | LifecycleLogger.logProcessInfo(); 21 | IpcManager.initialize(); 22 | 23 | app.whenReady() 24 | .then(() => { 25 | console.log('App is ready') 26 | 27 | 28 | const mainWindow = WindowManager.createMainWindow(); 29 | 30 | update(mainWindow); 31 | }); 32 | } 33 | 34 | // Lifecycle Handlers 35 | app.on('window-all-closed', async () => { 36 | await PythonServer.shutdown(); 37 | app.quit(); 38 | LifecycleLogger.logShutdownSequence(); 39 | }); 40 | 41 | app.on('activate', () => { 42 | if (BrowserWindow.getAllWindows().length === 0) { 43 | WindowManager.createMainWindow(); 44 | } 45 | }); 46 | 47 | // Start App 48 | console.log('Starting application'); 49 | startApplication(); 50 | -------------------------------------------------------------------------------- /skellycam-ui/src/services/server/server-helpers/server-urls.ts: -------------------------------------------------------------------------------- 1 | class ServerUrls { 2 | private readonly host = 'localhost'; 3 | private readonly port = 53117; 4 | 5 | /** 6 | * Get HTTP base URL 7 | */ 8 | getHttpUrl(): string { 9 | return `http://${this.host}:${this.port}`; 10 | } 11 | 12 | /** 13 | * Get WebSocket base URL 14 | */ 15 | getWebSocketUrl(): string { 16 | return `ws://${this.host}:${this.port}/skellycam/websocket/connect`; 17 | } 18 | get endpoints() { 19 | const baseUrl = this.getHttpUrl(); 20 | 21 | return { 22 | // Server management 23 | health: `${baseUrl}/health`, 24 | shutdown: `${baseUrl}/shutdown`, 25 | 26 | // Camera endpoints 27 | detectCameras: `${baseUrl}/skellycam/camera/detect`, 28 | camerasConnectOrUpdate: `${baseUrl}/skellycam/camera/group/apply`, 29 | closeAll: `${baseUrl}/skellycam/camera/group/close/all`, 30 | updateConfigs: `${baseUrl}/skellycam/camera/update`, 31 | pauseUnpauseCameras: `${baseUrl}/skellycam/camera/group/all/pause_unpause`, 32 | 33 | // Recording endpoints 34 | startRecording: `${baseUrl}/skellycam/camera/group/all/record/start`, 35 | stopRecording: `${baseUrl}/skellycam/camera/group/all/record/stop`, 36 | 37 | // WebSocket 38 | websocket: this.getWebSocketUrl(), 39 | }; 40 | } 41 | } 42 | 43 | // Export singleton instance 44 | export const serverUrls = new ServerUrls(); 45 | -------------------------------------------------------------------------------- /skellycam-ui/src/hooks/useAppUrls.ts: -------------------------------------------------------------------------------- 1 | export interface DefaultUrlConfig { 2 | host: string; 3 | port: number; 4 | } 5 | 6 | // Default URL configuration 7 | const defaultUrlConfig: DefaultUrlConfig = { 8 | host: 'localhost', 9 | port: 8006, 10 | }; 11 | 12 | 13 | // Get the base HTTP URL 14 | const getBaseHttpUrl = () => { 15 | const {host, port} = defaultUrlConfig; 16 | return `http://${host}:${port}`; 17 | }; 18 | 19 | // Get a specific API URL 20 | const getApiUrl = (path: string) => { 21 | return `${getBaseHttpUrl()}${path}`; 22 | }; 23 | 24 | // Get WebSocket URL 25 | const getWebSocketUrl = () => { 26 | const {host, port} = defaultUrlConfig; 27 | return `ws://${host}:${port}/skellycam/websocket/connect`; 28 | }; 29 | 30 | // Get all HTTP endpoint URLs 31 | const getHttpEndpointUrls = () => { 32 | return { 33 | health: getApiUrl('/health'), 34 | shutdown: getApiUrl('/shutdown'), 35 | detectCameras: getApiUrl('/skellycam/camera/detect'), 36 | createGroup: getApiUrl('/skellycam/camera/group/apply'), 37 | closeAll: getApiUrl('/skellycam/camera/group/close/all'), 38 | updateConfigs: getApiUrl('/skellycam/camera/update'), 39 | startRecording: getApiUrl('/skellycam/camera/group/all/record/start'), 40 | stopRecording: getApiUrl('/skellycam/camera/group/all/record/stop'), 41 | pauseUnpauseCameras: getApiUrl('/skellycam/camera/group/all/pause_unpause'), 42 | }; 43 | }; 44 | 45 | export const useAppUrls = { 46 | getBaseHttpUrl, 47 | getApiUrl, 48 | getWebSocketUrl, 49 | getHttpEndpointUrls, 50 | }; 51 | -------------------------------------------------------------------------------- /skellycam-ui/src/components/recording-info-panel/recording-subcomponents/RecordingNamePreview.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {TextField, Typography} from '@mui/material'; 3 | 4 | interface RecordingNamePreviewProps { 5 | name: string; 6 | tag: string; 7 | isRecording: boolean; 8 | onTagChange: (tag: string) => void; 9 | } 10 | 11 | export const RecordingNamePreview: React.FC = ({ 12 | name, 13 | tag, 14 | isRecording, 15 | onTagChange 16 | }) => { 17 | return ( 18 | <> 19 | 20 | Recording Name: {name} 21 | 22 | {!isRecording && ( 23 | onTagChange(e.target.value)} 27 | onKeyDown={(e) => { 28 | // Stop the TreeView from intercepting keyboard navigation 29 | e.stopPropagation();}} 30 | size="small" 31 | fullWidth 32 | placeholder="Optional tag" 33 | /> 34 | )} 35 | 36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /skellycam/core/camera/config/image_resolution.py: -------------------------------------------------------------------------------- 1 | from typing import Hashable 2 | 3 | from pydantic import BaseModel 4 | 5 | 6 | class ImageResolution(BaseModel): 7 | height: int 8 | width: int 9 | 10 | @classmethod 11 | def from_string(cls, tuple_str: str, delimiter: str = "x") -> "ImageResolution": 12 | """ 13 | Create a `VideoResolution` from a string like "(720x1280)" or "(1080x1920)" consistent with rows-by-columns 14 | """ 15 | height, width = tuple_str.replace("(", "").replace(")", "").split(delimiter) 16 | return cls(height=int(height), width=int(width)) 17 | 18 | 19 | @property 20 | def aspect_ratio(self) -> float: 21 | return self.width / self.height 22 | 23 | 24 | 25 | @property 26 | def as_tuple(self) -> tuple: 27 | return self.width, self.height 28 | 29 | def __hash__(self) -> Hashable: 30 | return hash((self.height, self.width)) 31 | 32 | def __lt__(self, other: object) -> bool: 33 | """ 34 | Define this so we can sort a list of `VideoResolution`s 35 | """ 36 | if not isinstance(other, ImageResolution): 37 | return False 38 | return self.width * self.height < other.width * other.height 39 | 40 | def __eq__(self, other: object) -> bool: 41 | """ 42 | Define this so we can compare `VideoResolution`s 43 | """ 44 | if not isinstance(other, ImageResolution): 45 | return False 46 | return self.width == other.width and self.height == other.height 47 | 48 | def __str__(self) -> str: 49 | return f"({self.height}x{self.width})" 50 | 51 | -------------------------------------------------------------------------------- /skellycam-ui/src/store/slices/theme/theme-slice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit'; 2 | import { ThemeMode } from './theme-types'; 3 | 4 | const getInitialTheme = (): ThemeMode => { 5 | if (typeof window !== 'undefined') { 6 | const saved = localStorage.getItem('themeMode'); 7 | if (saved === 'light' || saved === 'dark') { 8 | return saved; 9 | } 10 | // Check system preference 11 | if (window.matchMedia?.('(prefers-color-scheme: dark)').matches) { 12 | return 'dark'; 13 | } 14 | } 15 | return 'dark'; 16 | }; 17 | 18 | interface ThemeState { 19 | mode: ThemeMode; 20 | systemPreference: ThemeMode | null; 21 | } 22 | 23 | const initialState: ThemeState = { 24 | mode: getInitialTheme(), 25 | systemPreference: null, 26 | }; 27 | 28 | export const themeSlice = createSlice({ 29 | name: 'theme', 30 | initialState, 31 | reducers: { 32 | themeModeSet: (state, action: PayloadAction) => { 33 | state.mode = action.payload; 34 | localStorage.setItem('themeMode', action.payload); 35 | }, 36 | themeModeToggled: (state) => { 37 | state.mode = state.mode === 'dark' ? 'light' : 'dark'; 38 | localStorage.setItem('themeMode', state.mode); 39 | }, 40 | systemPreferenceUpdated: (state, action: PayloadAction) => { 41 | state.systemPreference = action.payload; 42 | }, 43 | }, 44 | }); 45 | 46 | export const { 47 | themeModeSet, 48 | themeModeToggled, 49 | systemPreferenceUpdated, 50 | } = themeSlice.actions; 51 | -------------------------------------------------------------------------------- /skellycam/core/camera_group/timestamps/numpy_timestamps/process_recording_timestamps.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from skellycam.core.camera_group.timestamps.numpy_timestamps.calculate_timestamps_numpy import calculate_durations 4 | from skellycam.core.types.numpy_record_dtypes import FRAME_LIFECYCLE_TIMESTAMPS_DTYPE, TimestampsArray, DurationArray 5 | from skellycam.core.types.type_overloads import CameraIdString 6 | 7 | 8 | async def process_recording_timestamps( 9 | frame_metadatas_by_camera: dict[CameraIdString, list[np.recarray]], 10 | frame_numbers: list[int] 11 | ) -> tuple[TimestampsArray, DurationArray]: 12 | 13 | # Get dimensions 14 | camera_ids = list(frame_metadatas_by_camera.keys()) 15 | num_cameras = len(camera_ids) 16 | num_frames = len(frame_numbers) 17 | 18 | # Create combined array for all cameras 19 | all_timestamps = np.recarray((num_cameras, num_frames), dtype=FRAME_LIFECYCLE_TIMESTAMPS_DTYPE) 20 | 21 | # Fill the array with data from each camera on each frame 22 | for frame_number in range(num_frames): 23 | for camera_index, camera_id in enumerate(camera_ids): 24 | all_timestamps[camera_index, frame_number] = frame_metadatas_by_camera[camera_id][frame_number].timestamps[0] 25 | 26 | # Calculate durations 27 | all_durations = await calculate_durations(all_timestamps=all_timestamps) 28 | 29 | # # Calculate statistics 30 | # timestamp_statistics = calculate_frame_timestamps_statistics(all_timestamps=all_timestamps, 31 | # all_durations=all_durations) 32 | 33 | return all_timestamps, all_durations 34 | -------------------------------------------------------------------------------- /skellycam/system/logging_configuration/logging_color_helpers.py: -------------------------------------------------------------------------------- 1 | def ensure_min_brightness(value: int, threshold=100): 2 | """Ensure the RGB value is above a certain threshold.""" 3 | return max(value, threshold) 4 | 5 | 6 | def ensure_not_grey(r: int, g: int, b: int, threshold_diff: int = 100): 7 | """Ensure that the color isn't desaturated grey by making one color component dominant.""" 8 | max_val = max(r, g, b) 9 | if abs(r - g) < threshold_diff and abs(r - b) < threshold_diff and abs(g - b) < threshold_diff: 10 | if max_val == r: 11 | r = 255 12 | elif max_val == g: 13 | g = 255 14 | else: 15 | b = 255 16 | return r, g, b 17 | 18 | 19 | def ensure_not_red(r: int, g: int, b: int, threshold_diff: int = 100): 20 | """Ensure that the color isn't too red (which looks like an error).""" 21 | if r - g > threshold_diff and r - b > threshold_diff: 22 | r = int(r / 2) 23 | if g > b: 24 | g = 255 25 | else: 26 | b = 255 27 | return r, g, b 28 | 29 | 30 | def get_hashed_color(value: int): 31 | """Generate a consistent random color for the given value.""" 32 | # Use modulo to ensure it's within the range of normal terminal colors. 33 | hashed = hash(value) % 0xFFFFFF # Keep within RGB 24-bit color 34 | red = ensure_min_brightness(hashed >> 16 & 255) 35 | green = ensure_min_brightness(hashed >> 8 & 255) 36 | blue = ensure_min_brightness(hashed & 255) 37 | 38 | red, green, blue = ensure_not_grey(red, green, blue) 39 | red, green, blue = ensure_not_red(red, green, blue) 40 | 41 | return "\033[38;2;{};{};{}m".format(red, green, blue) 42 | -------------------------------------------------------------------------------- /skellycam-ui/src/components/ui-components/BottomPanelContent.tsx: -------------------------------------------------------------------------------- 1 | // skellycam-ui/src/components/ui-components/BottomPanelContent.tsx 2 | import * as React from 'react'; 3 | import Box from '@mui/material/Box'; 4 | import {LogTerminal} from "@/components/LogTerminal"; 5 | import {Panel, PanelGroup, PanelResizeHandle} from "react-resizable-panels"; 6 | import {useTheme} from "@mui/material/styles"; 7 | import FramerateViewerPanel from "@/components/framerate-viewer/FrameRateViewer"; 8 | 9 | export default function BottomPanelContent() { 10 | const theme = useTheme(); 11 | 12 | return ( 13 | 14 | 15 | {/* Framerate Viewer Panel */} 16 | 17 | 18 | 19 | Framerate Viewer 20 | 21 | 22 | 23 | {/* Resize Handle */} 24 | 31 | 32 | {/* Logs Terminal Panel */} 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /skellycam-ui/src/components/camera-config-tree-view/CameraGroupTreeItem.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Box, Typography } from "@mui/material"; 3 | import { TreeItem } from "@mui/x-tree-view/TreeItem"; 4 | import { CameraTreeItem } from "./CameraTreeItem"; 5 | import { Camera } from "@/store/slices/cameras/cameras-types"; 6 | 7 | interface CameraGroupTreeItemProps { 8 | groupId: string; 9 | title: string; 10 | cameras: Camera[]; 11 | icon?: React.ReactNode; 12 | expandedItems?: string[]; 13 | } 14 | 15 | export const CameraGroupTreeItem: React.FC = ({ 16 | groupId, 17 | title, 18 | cameras, 19 | icon, 20 | expandedItems, 21 | }) => { 22 | return ( 23 | 27 | {icon} 28 | 29 | {title} ({cameras.length}) 30 | 31 | 32 | } 33 | > 34 | {cameras.map((camera: Camera) => ( 35 | 40 | ))} 41 | 42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /skellycam/core/device_detection/detect_microphone_devices.py: -------------------------------------------------------------------------------- 1 | import pyaudio 2 | 3 | 4 | def get_available_microphones(): 5 | # https://stackoverflow.com/questions/70886225/get-camera-device-name-and-port-for-opencv-videostream-python 6 | available_microphones = {} 7 | pyduo = pyaudio.PyAudio() 8 | devices_info = pyduo.get_host_api_info_by_index(0) 9 | number_of_devices = devices_info.get('deviceCount') 10 | for device_index in range(0, number_of_devices): 11 | if (pyduo.get_device_info_by_host_api_device_index(0, device_index).get('maxInputChannels')) > 0: 12 | available_microphones[device_index] = pyduo.get_device_info_by_host_api_device_index(0, device_index).get( 13 | 'name') 14 | 15 | return available_microphones 16 | 17 | 18 | # def get_available_microphone_devices() -> Dict[int, str]: 19 | # """ 20 | # List available unique microphone devices. 21 | # 22 | # Returns 23 | # ------- 24 | # list 25 | # A list of device indices for available microphones. 26 | # """ 27 | # audio = pyaudio.PyAudio() 28 | # device_count = audio.get_device_count() 29 | # unique_names = set() 30 | # microphones = {} 31 | # 32 | # for i in range(device_count): 33 | # device_info = audio.get_device_info_by_index(i) 34 | # device_name = device_info['name'] 35 | # if device_info['maxInputChannels'] > 0 and device_name not in unique_names: 36 | # # unique_names.add(device_name) 37 | # # print(f"\nDevice ID {i}: {device_name}") 38 | # # print(pprint(device_info)) 39 | # if device_name not in microphones.values(): 40 | # microphones[i] = device_name 41 | # 42 | # audio.terminate() 43 | # return microphones 44 | 45 | 46 | if __name__ == "__main__": 47 | from pprint import pprint 48 | 49 | pprint(get_available_microphones()) 50 | -------------------------------------------------------------------------------- /skellycam/system/device_detection/detect_microphone_devices.py: -------------------------------------------------------------------------------- 1 | import pyaudio 2 | 3 | 4 | def get_available_microphones(): 5 | # https://stackoverflow.com/questions/70886225/get-camera-device-name-and-port-for-opencv-videostream-python 6 | available_microphones = {} 7 | pyduo = pyaudio.PyAudio() 8 | devices_info = pyduo.get_host_api_info_by_index(0) 9 | number_of_devices = devices_info.get('deviceCount') 10 | for device_index in range(0, number_of_devices): 11 | if (pyduo.get_device_info_by_host_api_device_index(0, device_index).get('maxInputChannels')) > 0: 12 | available_microphones[device_index] = pyduo.get_device_info_by_host_api_device_index(0, device_index).get( 13 | 'name') 14 | 15 | return available_microphones 16 | 17 | 18 | # def get_available_microphone_devices() -> Dict[int, str]: 19 | # """ 20 | # List available unique microphone devices. 21 | # 22 | # Returns 23 | # ------- 24 | # list 25 | # A list of device indices for available microphones. 26 | # """ 27 | # audio = pyaudio.PyAudio() 28 | # device_count = audio.get_device_count() 29 | # unique_names = set() 30 | # microphones = {} 31 | # 32 | # for i in range(device_count): 33 | # device_info = audio.get_device_info_by_index(i) 34 | # device_name = device_info['name'] 35 | # if device_info['maxInputChannels'] > 0 and device_name not in unique_names: 36 | # # unique_names.add(device_name) 37 | # # print(f"\nDevice ID {i}: {device_name}") 38 | # # print(pprint(device_info)) 39 | # if device_name not in microphones.values(): 40 | # microphones[i] = device_name 41 | # 42 | # audio.terminate() 43 | # return microphones 44 | 45 | 46 | if __name__ == "__main__": 47 | from pprint import pprint 48 | 49 | pprint(get_available_microphones()) 50 | -------------------------------------------------------------------------------- /skellycam-ui/src/components/common/ErrorBoundary.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | // Define the types for props and state 4 | interface ErrorBoundaryProps { 5 | children: React.ReactNode; 6 | } 7 | 8 | interface ErrorBoundaryState { 9 | hasError: boolean; 10 | error: Error | null; 11 | errorInfo: React.ErrorInfo | null; 12 | } 13 | 14 | class ErrorBoundary extends React.Component { 15 | constructor(props: ErrorBoundaryProps) { 16 | super(props); 17 | this.state = {hasError: false, error: null, errorInfo: null}; 18 | } 19 | 20 | static getDerivedStateFromError(error: Error): Partial { 21 | // Update state so the next render shows the fallback UI. 22 | return {hasError: true}; 23 | } 24 | 25 | componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { 26 | // You can also log the error to an error reporting service 27 | console.error('Error caught by ErrorBoundary', error, errorInfo); 28 | // Store the error and errorInfo in state 29 | this.setState({error, errorInfo}); 30 | } 31 | 32 | render() { 33 | if (this.state.hasError) { 34 | // Render a custom fallback UI with error details 35 | return ( 36 |
37 |

Something went wrong :(

38 |
39 | {this.state.error && this.state.error.toString()} 40 |
41 | {this.state.errorInfo && this.state.errorInfo.componentStack} 42 |
43 |
44 | ); 45 | } 46 | 47 | return this.props.children; 48 | } 49 | } 50 | 51 | export default ErrorBoundary; 52 | // // Usage 53 | // 54 | // 55 | // 56 | -------------------------------------------------------------------------------- /skellycam-ui/src/store/slices/recording/recording-slice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit'; 2 | import { RecordingInfo } from './recording-types'; 3 | import { startRecording, stopRecording } from './recording-thunks'; 4 | 5 | const initialState: RecordingInfo = { 6 | isRecording: false, 7 | recordingDirectory: '~/skellycam_data/recordings', 8 | recordingName: null, 9 | startedAt: null, 10 | duration: 0, 11 | }; 12 | 13 | export const recordingSlice = createSlice({ 14 | name: 'recording', 15 | initialState, 16 | reducers: { 17 | recordingInfoUpdated: (state, action: PayloadAction>) => { 18 | return { ...state, ...action.payload }; 19 | }, 20 | recordingDirectoryChanged: (state, action: PayloadAction) => { 21 | state.recordingDirectory = action.payload; 22 | }, 23 | recordingDurationUpdated: (state, action: PayloadAction) => { 24 | state.duration = action.payload; 25 | }, 26 | }, 27 | extraReducers: (builder) => { 28 | builder 29 | .addCase(startRecording.fulfilled, (state, action) => { 30 | state.isRecording = true; 31 | state.recordingName = action.meta.arg.recordingName; 32 | state.recordingDirectory = action.meta.arg.recordingDirectory; 33 | state.startedAt = new Date().toISOString(); 34 | state.duration = 0; 35 | }) 36 | .addCase(stopRecording.fulfilled, (state) => { 37 | state.isRecording = false; 38 | state.recordingName = null; 39 | state.startedAt = null; 40 | state.duration = 0; 41 | }); 42 | }, 43 | }); 44 | 45 | export const { 46 | recordingInfoUpdated, 47 | recordingDirectoryChanged, 48 | recordingDurationUpdated, 49 | } = recordingSlice.actions; 50 | -------------------------------------------------------------------------------- /skellycam-ui/src/components/recording-info-panel/recording-subcomponents/DelayRecordingStartControl.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Box, Checkbox, FormControlLabel, TextField, useTheme} from '@mui/material'; 3 | 4 | interface DelayStartControlProps { 5 | useDelay: boolean; 6 | delaySeconds: number; 7 | onDelayToggle: (checked: boolean) => void; 8 | onDelayChange: (seconds: number) => void; 9 | } 10 | 11 | export const DelayRecordingStartControl: React.FC = ({ 12 | useDelay, 13 | delaySeconds, 14 | onDelayToggle, 15 | onDelayChange 16 | }) => { 17 | const theme = useTheme(); 18 | return ( 19 | 20 | onDelayToggle(e.target.checked)} 25 | color="primary" 26 | /> 27 | } 28 | label="Delay Start" 29 | /> 30 | {useDelay && ( 31 | onDelayChange(Math.max(1, parseInt(e.target.value) || 1))} 36 | inputProps={{min: 1, step: 1}} 37 | size="small" 38 | sx={{width: 100}} 39 | /> 40 | )} 41 | 42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /skellycam/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | """Top-level package for skellycam.""" 3 | 4 | __author__ = """Skelly FreeMoCap""" 5 | __email__ = "info@freemocap.org" 6 | __version__ = "v2023.09.1086" 7 | 8 | __description__ = "A simple python API for efficiently connecting to and recording synchronized videos from one or multiple cameras 💀📸" 9 | __package_name__ = "skellycam" 10 | __repo_url__ = f"https://github.com/freemocap/{__package_name__}" 11 | __repo_issues_url__ = f"{__repo_url__}/issues" 12 | __pypi_url__ = f"https://pypi.org/project/{__package_name__}" 13 | 14 | __package_root__ = __file__.replace("/__init__.py", "") 15 | 16 | import multiprocessing 17 | multiprocessing.freeze_support() 18 | 19 | # from skellycam.api.routers import SKELLYCAM_ROUTERS 20 | # from skellycam.core.camera.config.camera_config import CameraConfig, CameraConfigs 21 | # from skellycam.core.shared_memory.multi_frame_payload_ring_buffer import \ 22 | # MultiFrameSharedMemoryRingBuffer 23 | # from skellycam.core.types.type_overloads import CameraIndex, CameraName 24 | # from skellycam.skellycam_app.skellycam_app import SkellycamApplication 25 | # from skellycam.skellycam_app.skellycam_app_ipc.ipc_manager import InterProcessCommunicationManager 26 | # from skellycam.system.logging_configuration.handlers.websocket_log_queue_handler import create_websocket_log_queue 27 | from skellycam.system.logging_configuration.configure_logging import configure_logging 28 | from skellycam.system.logging_configuration.log_levels import LogLevels 29 | 30 | LOG_LEVEL = LogLevels.TRACE 31 | configure_logging(LOG_LEVEL) 32 | 33 | 34 | __all__ = [ 35 | "__author__", 36 | "__email__", 37 | "__version__", 38 | "__description__", 39 | "__package_name__", 40 | "__repo_url__", 41 | "__repo_issues_url__", 42 | "__pypi_url__", 43 | # 'SKELLYCAM_ROUTERS', 44 | # 'SkellycamApplication', 45 | # 'MultiFrameSharedMemoryRingBuffer', 46 | # 'CameraConfig', 47 | # 'InterProcessCommunicationManager', 48 | 'LOG_LEVEL' 49 | ] -------------------------------------------------------------------------------- /skellycam/system/logging_configuration/configure_logging.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import multiprocessing 3 | from typing import Optional 4 | 5 | from skellycam.system.logging_configuration.handlers.websocket_log_queue_handler import create_websocket_log_queue 6 | from skellycam.system.logging_configuration.log_levels import LogLevels 7 | from skellycam.system.logging_configuration.package_log_quieters import suppress_noisy_package_logs 8 | from .log_test_messages import log_test_messages 9 | from .logger_builder import LoggerBuilder 10 | 11 | suppress_noisy_package_logs() 12 | # Add custom log levels 13 | logging.addLevelName(LogLevels.LOOP.value, "LOOP") 14 | logging.addLevelName(LogLevels.TRACE.value, "TRACE") 15 | logging.addLevelName(LogLevels.SUCCESS.value, "SUCCESS") 16 | logging.addLevelName(LogLevels.API.value, "API") 17 | 18 | 19 | def add_log_method(level: LogLevels, name: str): 20 | def log_method(self, message, *args, **kws): 21 | if self.isEnabledFor(level.value): 22 | self._log(level.value, message, args, **kws, stacklevel=2) 23 | 24 | setattr(logging.Logger, name, log_method) 25 | 26 | 27 | def configure_logging(level: LogLevels, ws_queue: Optional[multiprocessing.Queue] = None): 28 | if ws_queue is None: 29 | # do not create new queue if not main process 30 | if not multiprocessing.current_process().name.lower() == 'mainprocess': 31 | return 32 | 33 | ws_queue = create_websocket_log_queue() 34 | add_log_method(LogLevels.LOOP, 'loop') 35 | add_log_method(LogLevels.TRACE, 'trace') 36 | add_log_method(LogLevels.API, 'api') 37 | add_log_method(LogLevels.SUCCESS, 'success') 38 | 39 | builder = LoggerBuilder(level, ws_queue) 40 | builder.configure() 41 | 42 | 43 | if __name__ == "__main__": 44 | logger_test = logging.getLogger(__name__) 45 | log_test_messages(logger_test) 46 | logger_test.success( 47 | "Logging setup and tests completed. Check the console output and the log file." 48 | ) 49 | -------------------------------------------------------------------------------- /skellycam-ui/src/store/slices/recording/recording-thunks.ts: -------------------------------------------------------------------------------- 1 | import { createAsyncThunk } from '@reduxjs/toolkit'; 2 | import { z } from 'zod'; 3 | import { RootState } from '@/store/types'; 4 | import {serverUrls} from "@/services"; 5 | 6 | const RecordStartRequestSchema = z.object({ 7 | recording_name: z.string(), 8 | recording_directory: z.string(), 9 | mic_device_index: z.number().default(-1), 10 | }); 11 | 12 | interface StartRecordingParams { 13 | recordingName: string; 14 | recordingDirectory: string; 15 | micDeviceIndex?: number; 16 | } 17 | 18 | export const startRecording = createAsyncThunk< 19 | void, 20 | StartRecordingParams, 21 | { state: RootState } 22 | >( 23 | 'recording/start', 24 | async ({ recordingName, recordingDirectory, micDeviceIndex = -1 }, { getState }) => { 25 | const state = getState(); 26 | 27 | const payload = RecordStartRequestSchema.parse({ 28 | recording_name: recordingName, 29 | recording_directory: recordingDirectory, 30 | mic_device_index: micDeviceIndex, 31 | }); 32 | 33 | const response = await fetch(serverUrls.endpoints.startRecording, { 34 | method: 'POST', 35 | headers: { 'Content-Type': 'application/json' }, 36 | body: JSON.stringify(payload), 37 | }); 38 | 39 | if (!response.ok) { 40 | throw new Error(`Failed to start recording: ${response.statusText}`); 41 | } 42 | } 43 | ); 44 | 45 | export const stopRecording = createAsyncThunk< 46 | void, 47 | void, 48 | { state: RootState } 49 | >( 50 | 'recording/stop', 51 | async (_, { getState }) => { 52 | const state = getState(); 53 | 54 | const response = await fetch(serverUrls.endpoints.stopRecording, { 55 | method: 'GET', 56 | }); 57 | 58 | if (!response.ok) { 59 | throw new Error(`Failed to stop recording: ${response.statusText}`); 60 | } 61 | } 62 | ); 63 | -------------------------------------------------------------------------------- /skellycam-ui/src/pages/CamerasPage.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import Box from "@mui/material/Box"; 3 | import ErrorBoundary from "@/components/common/ErrorBoundary"; 4 | import { Footer } from "@/components/ui-components/Footer"; 5 | import { useTheme } from "@mui/material/styles"; 6 | import { CameraViewsGrid } from "@/components/camera-views/CameraViewsGrid"; 7 | import {CamerasViewSettingsOverlay} from "@/components/camera-view-settings-overlay/CamerasViewSettingsOverlay"; 8 | 9 | interface CameraSettings { 10 | columns: number | null; 11 | } 12 | 13 | export const CamerasPage = () => { 14 | const theme = useTheme(); 15 | const [settings, setSettings] = useState({ 16 | columns: null, // auto 17 | }); 18 | 19 | const handleSettingsChange = (newSettings: CameraSettings) => { 20 | setSettings(newSettings); 21 | }; 22 | 23 | return ( 24 | 25 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 |