├── savant_etcd ├── assets │ └── certs │ │ └── keep ├── README.md ├── utils │ ├── run_etcd.sh │ ├── gen_keys.sh │ └── run_etcd_with_tls.sh ├── docker-compose.yml ├── src │ └── lib.rs └── Cargo.toml ├── savant_python ├── python │ └── savant_rs │ │ ├── py.typed │ │ ├── py │ │ ├── __init__.py │ │ ├── api │ │ │ ├── __init__.py │ │ │ ├── constants.py │ │ │ └── enums.py │ │ ├── utils │ │ │ ├── __init__.py │ │ │ └── re_patterns.py │ │ ├── client │ │ │ ├── builder │ │ │ │ └── __init__.py │ │ │ ├── runner │ │ │ │ ├── __init__.py │ │ │ │ └── log_result.py │ │ │ ├── image_source │ │ │ │ ├── __init__.py │ │ │ │ └── img_header_parse.py │ │ │ ├── utils │ │ │ │ └── __init__.py │ │ │ ├── __init__.py │ │ │ └── frame_source │ │ │ │ └── __init__.py │ │ └── log │ │ │ ├── const.py │ │ │ ├── __init__.py │ │ │ ├── logger_mixin.py │ │ │ └── savant_rs_handler.py │ │ ├── draw_spec │ │ └── __init__.py │ │ ├── zmq │ │ └── __init__.py │ │ ├── __init__.py │ │ ├── webserver │ │ ├── kvs │ │ │ └── __init__.py │ │ ├── __init__.py │ │ └── webserver.pyi │ │ ├── logging │ │ ├── __init__.py │ │ └── logging.pyi │ │ ├── metrics │ │ ├── __init__.py │ │ └── metrics.pyi │ │ ├── gstreamer │ │ ├── __init__.py │ │ └── gstreamer.pyi │ │ ├── telemetry │ │ ├── __init__.py │ │ └── telemetry.pyi │ │ ├── match_query │ │ └── __init__.py │ │ ├── primitives │ │ ├── geometry │ │ │ └── __init__.py │ │ ├── shutdown.pyi │ │ ├── end_of_stream.pyi │ │ ├── __init__.py │ │ └── attribute.pyi │ │ ├── atomic_counter │ │ ├── __init__.py │ │ └── atomic_counter.pyi │ │ ├── utils │ │ ├── serialization │ │ │ └── __init__.py │ │ ├── symbol_mapper │ │ │ ├── __init__.py │ │ │ └── symbol_mapper.pyi │ │ ├── __init__.py │ │ └── atomic_counter.pyi │ │ ├── test │ │ ├── __init__.py │ │ └── test.pyi │ │ ├── pipeline │ │ └── __init__.py │ │ └── savant_rs.pyi ├── build.rs ├── README.md ├── Cargo.toml └── pyproject.toml ├── savant_core ├── src │ ├── transport.rs │ ├── webserver │ │ └── kvs_subscription.rs │ ├── json_api.rs │ ├── primitives │ │ ├── eos.rs │ │ ├── point.rs │ │ ├── shutdown.rs │ │ ├── segment.rs │ │ ├── any_object.rs │ │ ├── userdata.rs │ │ └── attribute_set.rs │ ├── utils.rs │ ├── transport │ │ └── zeromq │ │ │ ├── sync_reader.rs │ │ │ └── sync_writer.rs │ ├── protobuf │ │ └── serialize │ │ │ ├── video_frame_batch.rs │ │ │ └── video_frame_transcoding_method.rs │ ├── pipeline │ │ ├── stage_function_loader.rs │ │ └── stage_plugin_sample.rs │ ├── deadlock_detection.rs │ ├── macros.rs │ ├── utils │ │ └── default_once.rs │ ├── metrics │ │ └── metric_collector.rs │ ├── primitives.rs │ └── rwlock.rs ├── build.rs ├── benches │ ├── bench_frame_save_load_pb.rs │ ├── bench_label_filter.rs │ └── bench_bbox_utils.rs └── examples │ └── rtp_pts_mapper.rs ├── .gitattributes ├── python ├── requirements.txt ├── utils │ ├── atomic_counter.py │ ├── inc_uuid_v7.py │ ├── log.py │ ├── eval.py │ └── relative_uuid_v7.py ├── primitives │ ├── attribute_value_type.py │ ├── shutdown.py │ ├── eos.py │ ├── bbox │ │ ├── bug_091.py │ │ ├── bug_096.py │ │ ├── cmp.py │ │ └── utils.py │ ├── object_track_id_range.py │ ├── frame_geometric_transformations.py │ ├── user_data.py │ ├── vector_view_ops.py │ ├── buf_copy.py │ ├── video_frame_update.py │ ├── load_save_various_approaches.py │ └── bench_obj_add_vs_create.py ├── run_all.sh ├── etcd.py ├── zmq │ ├── zmq_reqrep.py │ └── zmq_native.py └── match_query │ ├── simple_queries.py │ └── query.py ├── .cargo └── config.toml ├── savant_gstreamer_elements ├── build.rs ├── src │ ├── utils.rs │ └── lib.rs └── Cargo.toml ├── requirements.txt ├── services ├── replay │ ├── replay │ │ ├── scripts │ │ │ ├── rest_api │ │ │ │ ├── list_job.sh │ │ │ │ ├── list_jobs.sh │ │ │ │ ├── del_job.sh │ │ │ │ ├── list_stopped_jobs.sh │ │ │ │ ├── update_stop_condition.sh │ │ │ │ ├── find_keyframes.sh │ │ │ │ └── new_job.sh │ │ │ ├── ao-rtsp.sh │ │ │ └── video-loop.sh │ │ ├── assets │ │ │ ├── stub_imgs │ │ │ │ └── smpte100_640x360.jpeg │ │ │ └── test.json │ │ ├── src │ │ │ ├── web_service │ │ │ │ ├── shutdown.rs │ │ │ │ ├── new_job.rs │ │ │ │ ├── status.rs │ │ │ │ ├── find_keyframes.rs │ │ │ │ ├── del_job.rs │ │ │ │ └── update_stop_condition.rs │ │ │ └── web_service.rs │ │ └── Cargo.toml │ ├── samples │ │ └── file_restreaming │ │ │ ├── assets │ │ │ └── stub_imgs │ │ │ │ └── smpte100_1280x720.jpeg │ │ │ └── replay_config.json │ ├── .cargo │ │ └── config.toml │ ├── replaydb │ │ ├── src │ │ │ ├── lib.rs │ │ │ └── service.rs │ │ ├── assets │ │ │ ├── rocksdb_opt_out.json │ │ │ └── rocksdb.json │ │ └── Cargo.toml │ └── README.md ├── buffer_ng │ ├── README.md │ ├── src │ │ └── rocksdb │ │ │ ├── utilities.rs │ │ │ └── fs.rs │ ├── assets │ │ ├── configuration.json │ │ └── python │ │ │ ├── zmq_consumer.py │ │ │ └── module.py │ └── Cargo.toml ├── router │ ├── README.md │ ├── Cargo.toml │ └── assets │ │ └── python │ │ └── zmq_consumer.py ├── common │ ├── src │ │ ├── lib.rs │ │ └── fps_meter.rs │ └── Cargo.toml └── retina_rtsp │ ├── README.md │ ├── assets │ ├── empty_configuration.json │ ├── configuration_mediamtx.json │ ├── configuration_no_ntp.json │ ├── configuration_3groups_no_ntp.json │ └── configuration.json │ └── Cargo.toml ├── savant_protobuf ├── src │ └── lib.rs ├── Cargo.toml └── build.rs ├── .dockerignore ├── savant_core_py ├── src │ ├── zmq.rs │ ├── gst.rs │ ├── test.rs │ ├── atomic_counter.rs │ ├── capi.rs │ ├── primitives │ │ ├── point.rs │ │ ├── eos.rs │ │ ├── bbox │ │ │ └── utils.rs │ │ ├── shutdown.rs │ │ ├── pyobject.rs │ │ └── object │ │ │ └── object_tree.rs │ ├── primitives.rs │ ├── utils │ │ └── bigint.rs │ ├── lib.rs │ └── webserver.rs ├── build.rs ├── README.md └── Cargo.toml ├── docs ├── source │ ├── savant_rs │ │ ├── utils.rst │ │ ├── zmq.rst │ │ ├── pipeline.rst │ │ ├── logging.rst │ │ ├── metrics.rst │ │ ├── primitives.rst │ │ ├── draw_spec.rst │ │ ├── webserver.rst │ │ ├── match_query.rst │ │ ├── webserver_kvs.rst │ │ ├── primitives_geometry.rst │ │ ├── utils_serialization.rst │ │ ├── utils_symbol_mapper.rst │ │ └── index.rst │ ├── services │ │ ├── replay │ │ │ ├── _static │ │ │ │ └── replay_usage_diagram.png │ │ │ ├── 1_platforms.rst │ │ │ └── index.rst │ │ ├── retina_rtsp │ │ │ ├── index.rst │ │ │ └── 1_platforms.rst │ │ └── router │ │ │ ├── index.rst │ │ │ └── 1_platforms.rst │ ├── index.rst │ ├── _static │ │ └── css │ │ │ └── custom.css │ └── conf.py ├── Makefile └── make.bat ├── savant_deepstream ├── deepstream-sys │ ├── gstnvdsmeta_rs.h │ ├── src │ │ └── lib.rs │ └── Cargo.toml ├── deepstream_nvinfer │ ├── assets │ │ ├── identity.onnx │ │ ├── adaface_ir50_webface4m.onnx │ │ └── age_gender_mobilenet_v2_dynBatch.onnx │ ├── Cargo.toml │ └── src │ │ ├── infer_context │ │ └── output.rs │ │ └── lib.rs └── deepstream │ ├── Cargo.toml │ └── src │ └── lib.rs ├── savant_launcher ├── README.md └── Cargo.toml ├── .devcontainer ├── install-dev-env.sh ├── Dockerfile └── devcontainer.json ├── README.md ├── docker ├── Dockerfile.manylinux ├── Dockerfile.docs ├── services │ ├── Dockerfile.retina_rtsp │ ├── Dockerfile.replay │ ├── Dockerfile.router │ └── Dockerfile.buffer_ng ├── Dockerfile.py313 └── Dockerfile.py314 ├── utils ├── python313 │ └── copy-deps.sh ├── python314 │ └── copy-deps.sh └── services │ ├── docker-deps.sh │ ├── protoc.sh │ ├── replay │ └── copy-deps.sh │ └── retina_rtsp │ └── copy-deps.sh ├── savant_info └── Cargo.toml ├── savant_gstreamer └── Cargo.toml ├── .gitignore ├── Makefile └── .github └── workflows └── docs.yml /savant_etcd/assets/certs/keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /savant_python/python/savant_rs/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /savant_core/src/transport.rs: -------------------------------------------------------------------------------- 1 | pub mod zeromq; 2 | -------------------------------------------------------------------------------- /savant_python/python/savant_rs/py/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /savant_core/src/webserver/kvs_subscription.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /savant_python/python/savant_rs/py/api/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /savant_python/python/savant_rs/py/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.onnx filter=lfs diff=lfs merge=lfs -text 2 | -------------------------------------------------------------------------------- /savant_python/python/savant_rs/py/client/builder/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /savant_python/python/savant_rs/py/client/runner/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /savant_python/python/savant_rs/draw_spec/__init__.py: -------------------------------------------------------------------------------- 1 | from .draw_spec import * 2 | -------------------------------------------------------------------------------- /python/requirements.txt: -------------------------------------------------------------------------------- 1 | requests ~= 2.32 2 | numpy ~= 2.2 3 | pyzmq ~= 26.2 4 | maturin ~= 1.8 5 | -------------------------------------------------------------------------------- /savant_core/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("cargo:rustc-link-lib=dylib=zmq"); 3 | } 4 | -------------------------------------------------------------------------------- /savant_python/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | pyo3_build_config::add_extension_module_link_args(); 3 | } 4 | -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [registries.crates-io] 2 | protocol = "sparse" 3 | 4 | rustflags = "-C prefer-dynamic" 5 | -------------------------------------------------------------------------------- /savant_gstreamer_elements/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | pyo3_build_config::add_extension_module_link_args(); 3 | } 4 | -------------------------------------------------------------------------------- /savant_python/python/savant_rs/py/log/const.py: -------------------------------------------------------------------------------- 1 | LOGGING_PREFIX = 'insight.savant' 2 | DEFAULT_LOGLEVEL = 'info' 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | zmq 2 | numpy 3 | maturin[patchelf] ~= 1.9 4 | sphinx 5 | sphinx-rtd-theme 6 | sphinxcontrib-napoleon 7 | -------------------------------------------------------------------------------- /services/replay/replay/scripts/rest_api/list_job.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | curl http://127.0.0.1:8080/api/v1/job/$1 | json_pp -------------------------------------------------------------------------------- /savant_core/src/json_api.rs: -------------------------------------------------------------------------------- 1 | pub trait ToSerdeJsonValue { 2 | fn to_serde_json_value(&self) -> serde_json::Value; 3 | } 4 | -------------------------------------------------------------------------------- /services/replay/replay/scripts/rest_api/list_jobs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | curl http://127.0.0.1:8080/api/v1/job | json_pp 4 | -------------------------------------------------------------------------------- /savant_protobuf/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod generated; 2 | 3 | pub fn version() -> &'static str { 4 | env!("CARGO_PKG_VERSION") 5 | } 6 | -------------------------------------------------------------------------------- /savant_python/python/savant_rs/zmq/__init__.py: -------------------------------------------------------------------------------- 1 | from .zmq import * # type: ignore 2 | 3 | __all__ = zmq.__all__ # type: ignore 4 | -------------------------------------------------------------------------------- /services/replay/replay/scripts/rest_api/del_job.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | curl -X DELETE http://127.0.0.1:8080/api/v1/job/$1 | json_pp -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | target 2 | python 3 | .github 4 | .idea 5 | .zed 6 | .vscode 7 | .venv 8 | venv* 9 | dist 10 | docs-artifact.tar 11 | -------------------------------------------------------------------------------- /savant_core_py/src/zmq.rs: -------------------------------------------------------------------------------- 1 | pub mod basic_types; 2 | pub mod blocking; 3 | pub mod configs; 4 | pub mod nonblocking; 5 | pub mod results; 6 | -------------------------------------------------------------------------------- /savant_python/python/savant_rs/__init__.py: -------------------------------------------------------------------------------- 1 | from .savant_rs import * # type: ignore 2 | 3 | __all__ = savant_rs.__all__ # type: ignore 4 | -------------------------------------------------------------------------------- /savant_python/python/savant_rs/webserver/kvs/__init__.py: -------------------------------------------------------------------------------- 1 | from .kvs import * # type: ignore 2 | 3 | __all__ = kvs.__all__ # type: ignore 4 | -------------------------------------------------------------------------------- /services/replay/replay/scripts/rest_api/list_stopped_jobs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | curl http://127.0.0.1:8080/api/v1/job/stopped | json_pp 4 | -------------------------------------------------------------------------------- /savant_python/python/savant_rs/logging/__init__.py: -------------------------------------------------------------------------------- 1 | from .logging import * # type: ignore 2 | 3 | __all__ = logging.__all__ # type: ignore 4 | -------------------------------------------------------------------------------- /savant_python/python/savant_rs/metrics/__init__.py: -------------------------------------------------------------------------------- 1 | from .metrics import * # type: ignore 2 | 3 | __all__ = metrics.__all__ # type: ignore 4 | -------------------------------------------------------------------------------- /savant_python/python/savant_rs/py/utils/re_patterns.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | socket_uri_pattern = re.compile('([a-z]+\\+[a-z]+:)?([a-z]+://.*)') 4 | -------------------------------------------------------------------------------- /docs/source/savant_rs/utils.rst: -------------------------------------------------------------------------------- 1 | savant_rs.utils 2 | --------------- 3 | 4 | .. automodule:: savant_rs.utils 5 | :members: 6 | :undoc-members: 7 | -------------------------------------------------------------------------------- /savant_python/python/savant_rs/gstreamer/__init__.py: -------------------------------------------------------------------------------- 1 | from .gstreamer import * # type: ignore 2 | 3 | __all__ = gstreamer.__all__ # type: ignore 4 | -------------------------------------------------------------------------------- /savant_python/python/savant_rs/telemetry/__init__.py: -------------------------------------------------------------------------------- 1 | from .telemetry import * # type: ignore 2 | 3 | __all__ = telemetry.__all__ # type: ignore 4 | -------------------------------------------------------------------------------- /savant_python/python/savant_rs/webserver/__init__.py: -------------------------------------------------------------------------------- 1 | from .webserver import * # type: ignore 2 | 3 | __all__ = webserver.__all__ # type: ignore 4 | -------------------------------------------------------------------------------- /savant_python/python/savant_rs/match_query/__init__.py: -------------------------------------------------------------------------------- 1 | from .match_query import * # type: ignore 2 | 3 | __all__ = match_query.__all__ # type: ignore 4 | -------------------------------------------------------------------------------- /savant_python/python/savant_rs/primitives/geometry/__init__.py: -------------------------------------------------------------------------------- 1 | from .geometry import * # type: ignore 2 | 3 | __all__ = geometry.__all__ # type: ignore 4 | -------------------------------------------------------------------------------- /docs/source/savant_rs/zmq.rst: -------------------------------------------------------------------------------- 1 | savant_rs.zmq 2 | ---------------------------- 3 | 4 | .. automodule:: savant_rs.zmq 5 | :members: 6 | :undoc-members: 7 | -------------------------------------------------------------------------------- /docs/source/savant_rs/pipeline.rst: -------------------------------------------------------------------------------- 1 | savant_rs.pipeline 2 | -------------------- 3 | 4 | .. automodule:: savant_rs.pipeline 5 | :members: 6 | :undoc-members: 7 | -------------------------------------------------------------------------------- /savant_deepstream/deepstream-sys/gstnvdsmeta_rs.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include -------------------------------------------------------------------------------- /savant_python/python/savant_rs/atomic_counter/__init__.py: -------------------------------------------------------------------------------- 1 | from .atomic_counter import * # type: ignore 2 | 3 | __all__ = atomic_counter.__all__ # type: ignore 4 | -------------------------------------------------------------------------------- /savant_python/python/savant_rs/py/api/constants.py: -------------------------------------------------------------------------------- 1 | DEFAULT_NAMESPACE = 'default' 2 | DEFAULT_TIME_BASE = (1, 10**9) # nanosecond 3 | DEFAULT_FRAMERATE = '30/1' 4 | -------------------------------------------------------------------------------- /savant_python/python/savant_rs/py/api/enums.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class ExternalFrameType(Enum): 5 | ZEROMQ = 'zeromq' 6 | REDIS = 'redis' 7 | -------------------------------------------------------------------------------- /savant_python/python/savant_rs/py/client/image_source/__init__.py: -------------------------------------------------------------------------------- 1 | from .image_source import JpegSource, PngSource 2 | 3 | __all__ = ['JpegSource', 'PngSource'] 4 | -------------------------------------------------------------------------------- /savant_python/python/savant_rs/utils/serialization/__init__.py: -------------------------------------------------------------------------------- 1 | from .serialization import * # type: ignore 2 | 3 | __all__ = serialization.__all__ # type: ignore 4 | -------------------------------------------------------------------------------- /savant_python/python/savant_rs/utils/symbol_mapper/__init__.py: -------------------------------------------------------------------------------- 1 | from .symbol_mapper import * # type: ignore 2 | 3 | __all__ = symbol_mapper.__all__ # type: ignore 4 | -------------------------------------------------------------------------------- /docs/source/savant_rs/logging.rst: -------------------------------------------------------------------------------- 1 | savant_rs.logging 2 | ----------------------------- 3 | 4 | .. automodule:: savant_rs.logging 5 | :members: 6 | :undoc-members: 7 | -------------------------------------------------------------------------------- /docs/source/savant_rs/metrics.rst: -------------------------------------------------------------------------------- 1 | savant_rs.metrics 2 | ----------------------------- 3 | 4 | .. automodule:: savant_rs.metrics 5 | :members: 6 | :undoc-members: 7 | -------------------------------------------------------------------------------- /docs/source/savant_rs/primitives.rst: -------------------------------------------------------------------------------- 1 | savant_rs.primitives 2 | -------------------- 3 | 4 | .. automodule:: savant_rs.primitives 5 | :members: 6 | :undoc-members: 7 | -------------------------------------------------------------------------------- /docs/source/savant_rs/draw_spec.rst: -------------------------------------------------------------------------------- 1 | savant_rs.draw_spec 2 | ----------------------------- 3 | 4 | .. automodule:: savant_rs.draw_spec 5 | :members: 6 | :undoc-members: 7 | -------------------------------------------------------------------------------- /docs/source/savant_rs/webserver.rst: -------------------------------------------------------------------------------- 1 | savant_rs.webserver 2 | ----------------------------- 3 | 4 | .. automodule:: savant_rs.webserver 5 | :members: 6 | :undoc-members: 7 | -------------------------------------------------------------------------------- /docs/source/savant_rs/match_query.rst: -------------------------------------------------------------------------------- 1 | savant_rs.match_query 2 | ---------------------------- 3 | 4 | .. automodule:: savant_rs.match_query 5 | :members: 6 | :undoc-members: 7 | -------------------------------------------------------------------------------- /docs/source/services/replay/_static/replay_usage_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insight-platform/savant-rs/HEAD/docs/source/services/replay/_static/replay_usage_diagram.png -------------------------------------------------------------------------------- /services/replay/replay/assets/stub_imgs/smpte100_640x360.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insight-platform/savant-rs/HEAD/services/replay/replay/assets/stub_imgs/smpte100_640x360.jpeg -------------------------------------------------------------------------------- /docs/source/savant_rs/webserver_kvs.rst: -------------------------------------------------------------------------------- 1 | savant_rs.webserver.kvs 2 | ----------------------------- 3 | 4 | .. automodule:: savant_rs.webserver.kvs 5 | :members: 6 | :undoc-members: 7 | -------------------------------------------------------------------------------- /savant_python/python/savant_rs/test/__init__.py: -------------------------------------------------------------------------------- 1 | """Test utilities for generating test data.""" 2 | 3 | from .test import * # type: ignore 4 | 5 | __all__ = test.__all__ # type: ignore 6 | -------------------------------------------------------------------------------- /savant_deepstream/deepstream_nvinfer/assets/identity.onnx: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:ea8ea79fa84ca078e3d2bce5f75bfd573279615900e021b887dad08862a5c6c1 3 | size 175 4 | -------------------------------------------------------------------------------- /savant_launcher/README.md: -------------------------------------------------------------------------------- 1 | cd dist/build_artifacts 2 | LD_LIBRARY_PATH=. ./savant_launcher --python-root ../../savant_launcher/assets \ 3 | --module-name entrypoint \ 4 | --function-name main 5 | -------------------------------------------------------------------------------- /savant_python/python/savant_rs/py/log/__init__.py: -------------------------------------------------------------------------------- 1 | """Logging utils package.""" 2 | 3 | from .log_setup import get_logger, init_logging, update_logging 4 | from .logger_mixin import LoggerMixin 5 | -------------------------------------------------------------------------------- /savant_python/python/savant_rs/pipeline/__init__.py: -------------------------------------------------------------------------------- 1 | """Pipeline module for video processing.""" 2 | 3 | from .pipeline import * # type: ignore 4 | 5 | __all__ = pipeline.__all__ # type: ignore 6 | -------------------------------------------------------------------------------- /savant_python/python/savant_rs/py/client/utils/__init__.py: -------------------------------------------------------------------------------- 1 | """Savant Client SDK utilities.""" 2 | 3 | from .img_resize import resize_preserving_aspect 4 | 5 | __all__ = ['resize_preserving_aspect'] 6 | -------------------------------------------------------------------------------- /.devcontainer/install-dev-env.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | curl -o rustup.sh --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs 4 | sh rustup.sh -y 5 | source $HOME/.cargo/env 6 | rustup update 7 | rustc -V 8 | -------------------------------------------------------------------------------- /docs/source/savant_rs/primitives_geometry.rst: -------------------------------------------------------------------------------- 1 | savant_rs.primitives.geometry 2 | ----------------------------- 3 | 4 | .. automodule:: savant_rs.primitives.geometry 5 | :members: 6 | :undoc-members: 7 | -------------------------------------------------------------------------------- /docs/source/savant_rs/utils_serialization.rst: -------------------------------------------------------------------------------- 1 | savant_rs.utils.serialization 2 | ----------------------------- 3 | 4 | .. automodule:: savant_rs.utils.serialization 5 | :members: 6 | :undoc-members: 7 | -------------------------------------------------------------------------------- /docs/source/savant_rs/utils_symbol_mapper.rst: -------------------------------------------------------------------------------- 1 | savant_rs.utils.symbol_mapper 2 | ----------------------------- 3 | 4 | .. automodule:: savant_rs.utils.symbol_mapper 5 | :members: 6 | :undoc-members: 7 | -------------------------------------------------------------------------------- /savant_deepstream/deepstream_nvinfer/assets/adaface_ir50_webface4m.onnx: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:5346b3f6615858e1bb1089e718562c73587d9eff3ac66529ff69abadc03d0a50 3 | size 174746887 4 | -------------------------------------------------------------------------------- /services/replay/samples/file_restreaming/assets/stub_imgs/smpte100_1280x720.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insight-platform/savant-rs/HEAD/services/replay/samples/file_restreaming/assets/stub_imgs/smpte100_1280x720.jpeg -------------------------------------------------------------------------------- /savant_deepstream/deepstream_nvinfer/assets/age_gender_mobilenet_v2_dynBatch.onnx: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:586f5848b9495c8815899764c5b5715bf8f0f651fd705b2c598452bd9d80942c 3 | size 30283039 4 | -------------------------------------------------------------------------------- /python/utils/atomic_counter.py: -------------------------------------------------------------------------------- 1 | from savant_rs.utils import AtomicCounter 2 | 3 | base = 100000 4 | counter = AtomicCounter(base) 5 | 6 | print(counter.next) 7 | print(counter.next) 8 | print(counter.get) 9 | counter.set(1000) 10 | -------------------------------------------------------------------------------- /python/primitives/attribute_value_type.py: -------------------------------------------------------------------------------- 1 | from savant_rs.primitives import AttributeValueType 2 | 3 | # not AttributeValueType is hashable 4 | # 5 | d = { 6 | AttributeValueType.Bytes: "Hello", 7 | AttributeValueType.Integer: 1, 8 | } 9 | -------------------------------------------------------------------------------- /services/replay/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [registries.crates-io] 2 | protocol = "sparse" 3 | 4 | # enable x86 optimizations V3 5 | [target.x86_64-unknown-linux-gnu] 6 | rustflags = "-C target-cpu=x86-64-v3" 7 | 8 | [env] 9 | CXXFLAGS = "-mpclmul -msse2" -------------------------------------------------------------------------------- /savant_python/python/savant_rs/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .atomic_counter import * # type: ignore 2 | from .utils import * # type: ignore 3 | 4 | __all__ = ( 5 | utils.__all__ # type: ignore 6 | + atomic_counter.__all__ # type: ignore 7 | ) 8 | -------------------------------------------------------------------------------- /services/replay/replay/scripts/rest_api/update_stop_condition.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | curl --header "Content-Type: application/json" -X PATCH \ 4 | --data '{"frame_count": 10000}' \ 5 | http://127.0.0.1:8080/api/v1/job/$1/stop-condition | json_pp -------------------------------------------------------------------------------- /savant_python/python/savant_rs/savant_rs.pyi: -------------------------------------------------------------------------------- 1 | __all__ = ["version", "register_handler", "unregister_handler"] 2 | 3 | def version() -> str: ... 4 | def register_handler(element_name: str, h: object) -> None: ... 5 | def unregister_handler(name: str) -> None: ... 6 | -------------------------------------------------------------------------------- /python/utils/inc_uuid_v7.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | import savant_rs.utils as utils 4 | 5 | for i in range(10): 6 | print(utils.incremental_uuid_v7()) 7 | 8 | time.sleep(1 / 1000) 9 | 10 | for i in range(10): 11 | print(utils.incremental_uuid_v7()) 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Savant Rust Optimized Algorithms, Data Structures and Services 2 | 3 | Documentation is available at: https://insight-platform.github.io/savant-rs/ 4 | 5 | # Installation options 6 | 7 | To install a standalone version with clientsdk dependencies use `[clientsdk]`. -------------------------------------------------------------------------------- /services/replay/replay/scripts/rest_api/find_keyframes.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | curl --header "Content-Type: application/json" -X POST \ 4 | --data '{"source_id": "in-video", "from": null, "to": null, "limit": 1}' \ 5 | http://127.0.0.1:8080/api/v1/keyframes/find | json_pp -------------------------------------------------------------------------------- /docker/Dockerfile.manylinux: -------------------------------------------------------------------------------- 1 | FROM ghcr.io/insight-platform/manylinux_2_28:v1.3.0 AS builder 2 | 3 | COPY . . 4 | ARG PYTHON_INTERPRETER 5 | RUN BUILD_ENVIRONMENT=manylinux make release 6 | RUN rm -rf target 7 | 8 | FROM alpine:3.18 AS dist 9 | COPY --from=builder /opt/dist /opt/dist 10 | -------------------------------------------------------------------------------- /savant_python/python/savant_rs/test/test.pyi: -------------------------------------------------------------------------------- 1 | from savant_rs.primitives import VideoFrame, VideoObject 2 | 3 | __all__ = ["gen_empty_frame", "gen_frame", "gen_object"] 4 | 5 | def gen_empty_frame() -> VideoFrame: ... 6 | def gen_frame() -> VideoFrame: ... 7 | def gen_object(id: int) -> VideoObject: ... 8 | -------------------------------------------------------------------------------- /savant_core/src/primitives/eos.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize)] 2 | pub struct EndOfStream { 3 | pub source_id: String, 4 | } 5 | 6 | impl EndOfStream { 7 | pub fn new(source_id: String) -> Self { 8 | Self { source_id } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /savant_etcd/README.md: -------------------------------------------------------------------------------- 1 | # Etcd Dynamic State 2 | 3 | A simple library representing the state dynamically updated from Etcd watched endpoint. Mostly used for retrieval data 4 | from Etcd and keeping it up to date. 5 | 6 | Requires Docker and sudo-less access to it for the current users for running tests. 7 | 8 | -------------------------------------------------------------------------------- /python/utils/log.py: -------------------------------------------------------------------------------- 1 | from savant_rs.py.log import get_logger, init_logging 2 | 3 | # run LOGLEVEL=info python python/utils/log.py 4 | # or LOGLEVEL=debug python python/utils/log.py 5 | 6 | init_logging() 7 | 8 | logger = get_logger(__name__) 9 | 10 | logger.debug("Hello, world!") 11 | logger.info("Hello, world!") 12 | -------------------------------------------------------------------------------- /savant_etcd/utils/run_etcd.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | docker run -it --rm \ 4 | -p 2379:2379 \ 5 | -e ALLOW_NONE_AUTHENTICATION=yes \ 6 | -e ETCD_LISTEN_CLIENT_URLS=http://0.0.0.0:2379 \ 7 | -e ETCD_ADVERTISE_CLIENT_URLS=http://0.0.0.0:2379 \ 8 | --name remote-etcd \ 9 | bitnamilegacy/etcd:3.6.4-debian-12-r4 10 | -------------------------------------------------------------------------------- /savant_python/python/savant_rs/utils/atomic_counter.pyi: -------------------------------------------------------------------------------- 1 | __all__ = ["AtomicCounter"] 2 | 3 | class AtomicCounter: 4 | def __init__(self, initial_value: int): ... 5 | @property 6 | def next(self) -> int: ... 7 | def set(self, value: int) -> int: ... 8 | @property 9 | def get(self) -> int: ... 10 | -------------------------------------------------------------------------------- /services/buffer_ng/README.md: -------------------------------------------------------------------------------- 1 | # Buffer NG 2 | 3 | Buffer NG is a Python-extendable service that can process/modify, and buffer messages on disk to prevent their loss when downstream cannot keep up or experience outage. This service is crucial for complex streaming applications working under high load. 4 | 5 | **License**: Apache 2 6 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | ************************ 2 | Savant Rust Components 3 | ************************ 4 | 5 | .. toctree:: 6 | :maxdepth: 2 7 | :caption: Contents 8 | 9 | savant_rs/index 10 | services/replay/index 11 | services/retina_rtsp/index 12 | services/router/index 13 | services/buffer_ng/index 14 | -------------------------------------------------------------------------------- /savant_core/src/utils.rs: -------------------------------------------------------------------------------- 1 | pub mod default_once; 2 | pub mod iter; 3 | pub mod rtp_pts_mapper; 4 | pub mod uuid_v7; 5 | use std::fmt::Write; 6 | 7 | pub fn bytes_to_hex_string(bytes: &[u8]) -> String { 8 | bytes.iter().fold(String::new(), |mut output, b| { 9 | let _ = write!(output, "{b:02X}"); 10 | output 11 | }) 12 | } 13 | -------------------------------------------------------------------------------- /savant_python/python/savant_rs/primitives/shutdown.pyi: -------------------------------------------------------------------------------- 1 | from savant_rs.utils.serialization import Message 2 | 3 | __all__ = [ 4 | "Shutdown", 5 | ] 6 | 7 | class Shutdown: 8 | def __init__(self, auth: str): ... 9 | @property 10 | def auth(self) -> str: ... 11 | @property 12 | def json(self) -> str: ... 13 | def to_message(self) -> Message: ... 14 | -------------------------------------------------------------------------------- /savant_deepstream/deepstream-sys/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_snake_case)] 2 | #![allow(non_upper_case_globals)] 3 | #![allow(dead_code)] 4 | #![allow(unused_variables)] 5 | #![allow(unused_imports)] 6 | #![allow(clippy::all)] 7 | #![allow(non_camel_case_types)] 8 | #![allow(warnings)] 9 | 10 | pub mod gstnvdsmeta; 11 | 12 | // Re-export main types 13 | pub use gstnvdsmeta::*; 14 | -------------------------------------------------------------------------------- /python/run_all.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | pip install --upgrade pip 4 | pip install -r requirements.txt 5 | 6 | CURRENT_DIR=$(pwd) 7 | find . -name '*.py' | while read -r file; do 8 | DIR=$(dirname "$file") 9 | FILE=$(basename "$file") 10 | cd "$DIR" || exit 11 | echo -ne "Run '$FILE' ... " 12 | python "$FILE" >/dev/null 2>&1 13 | echo "$?" 14 | cd "$CURRENT_DIR" || exit 15 | done 16 | -------------------------------------------------------------------------------- /savant_python/python/savant_rs/atomic_counter/atomic_counter.pyi: -------------------------------------------------------------------------------- 1 | __all__ = [ 2 | "AtomicCounter", 3 | ] 4 | 5 | class AtomicCounter: 6 | """A thread-safe atomic counter.""" 7 | 8 | def __init__(self, initial: int) -> None: ... 9 | def set(self, value: int) -> None: ... 10 | @property 11 | def next(self) -> int: ... 12 | @property 13 | def get(self) -> int: ... 14 | -------------------------------------------------------------------------------- /savant_python/python/savant_rs/primitives/end_of_stream.pyi: -------------------------------------------------------------------------------- 1 | from savant_rs.utils.serialization import Message 2 | 3 | __all__ = [ 4 | "EndOfStream", 5 | ] 6 | 7 | class EndOfStream: 8 | def __init__(self, source_id: str): ... 9 | @property 10 | def source_id(self) -> str: ... 11 | @property 12 | def json(self) -> str: ... 13 | def to_message(self) -> Message: ... 14 | -------------------------------------------------------------------------------- /services/replay/replay/src/web_service/shutdown.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{post, web, Responder}; 2 | use log::info; 3 | 4 | use crate::web_service::{JobService, ResponseMessage}; 5 | 6 | #[post("/shutdown")] 7 | async fn shutdown(js: web::Data) -> impl Responder { 8 | let mut js_bind = js.shutdown.lock().await; 9 | info!("Shutting down"); 10 | *js_bind = true; 11 | ResponseMessage::Ok 12 | } 13 | -------------------------------------------------------------------------------- /utils/python313/copy-deps.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -euo pipefail # Strict error handling 3 | 4 | # Define target directories 5 | readonly TARGET_BIN_DIR="/opt/bin" 6 | install -d -m 755 "${TARGET_BIN_DIR}" 7 | # Copy binary with executable permissions 8 | if ! install -m 755 /tmp/build/release/savant_info "${TARGET_BIN_DIR}/"; then 9 | echo "Error: Failed to copy savant_info binary" >&2 10 | exit 1 11 | fi 12 | -------------------------------------------------------------------------------- /utils/python314/copy-deps.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -euo pipefail # Strict error handling 3 | 4 | # Define target directories 5 | readonly TARGET_BIN_DIR="/opt/bin" 6 | install -d -m 755 "${TARGET_BIN_DIR}" 7 | # Copy binary with executable permissions 8 | if ! install -m 755 /tmp/build/release/savant_info "${TARGET_BIN_DIR}/"; then 9 | echo "Error: Failed to copy savant_info binary" >&2 10 | exit 1 11 | fi 12 | -------------------------------------------------------------------------------- /savant_info/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "savant_info" 3 | version.workspace = true 4 | edition.workspace = true 5 | authors.workspace = true 6 | description.workspace = true 7 | homepage.workspace = true 8 | repository.workspace = true 9 | readme.workspace = true 10 | keywords.workspace = true 11 | categories.workspace = true 12 | license.workspace = true 13 | rust-version.workspace = true 14 | 15 | [dependencies] 16 | -------------------------------------------------------------------------------- /services/router/README.md: -------------------------------------------------------------------------------- 1 | # Router 2 | 3 | Router is a Python-extendable service that can process/modify, and route Savant messages based on their labels. With the router, you can process multiple ingress streams coming from multiple sockets and route them to multiple egress streams. This service is crucial for complex streaming applications requiring conditional processing of streams with many circuits. 4 | 5 | **License**: Apache 2 6 | -------------------------------------------------------------------------------- /utils/services/docker-deps.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -euo pipefail # Stricter error handling 3 | 4 | # Update and install dependencies with minimal attack surface 5 | apt-get update && \ 6 | DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ 7 | clang \ 8 | ca-certificates \ 9 | libva-dev \ 10 | curl \ 11 | unzip && \ 12 | apt-get clean && \ 13 | rm -rf /var/lib/apt/lists/* 14 | -------------------------------------------------------------------------------- /python/utils/eval.py: -------------------------------------------------------------------------------- 1 | from savant_rs.match_query import (register_env_resolver, 2 | register_utility_resolver) 3 | from savant_rs.utils import eval_expr 4 | 5 | register_env_resolver() 6 | register_utility_resolver() 7 | 8 | print(eval_expr("1 + 1")) 9 | 10 | print(eval_expr("""p = env("PATH", ""); (is_string(p), p)""")) # uncached 11 | print(eval_expr("""p = env("PATH", ""); (is_string(p), p)""")) # cached 12 | -------------------------------------------------------------------------------- /python/primitives/shutdown.py: -------------------------------------------------------------------------------- 1 | from savant_rs.primitives import Shutdown 2 | from savant_rs.utils.serialization import (Message, load_message_from_bytes, 3 | save_message_to_bytes) 4 | 5 | e = Shutdown("abc") 6 | 7 | m = Message.shutdown(e) 8 | s = save_message_to_bytes(m) 9 | new_m = load_message_from_bytes(s) 10 | assert new_m.is_shutdown() 11 | 12 | e = new_m.as_shutdown() 13 | assert e.auth == "abc" 14 | -------------------------------------------------------------------------------- /docs/source/savant_rs/index.rst: -------------------------------------------------------------------------------- 1 | Savant Rust Library 2 | ==================== 3 | 4 | .. automodule:: savant_rs 5 | :members: 6 | :undoc-members: 7 | 8 | .. toctree:: 9 | :maxdepth: 2 10 | 11 | draw_spec 12 | logging 13 | metrics 14 | pipeline 15 | primitives 16 | primitives_geometry 17 | utils 18 | utils_symbol_mapper 19 | utils_serialization 20 | match_query 21 | zmq 22 | webserver 23 | webserver_kvs 24 | -------------------------------------------------------------------------------- /savant_gstreamer_elements/src/utils.rs: -------------------------------------------------------------------------------- 1 | const DEFAULT_TIME_BASE: (i32, i32) = (1, 1_000_000_000); 2 | 3 | pub fn convert_ts(ts: i64, time_base: (i32, i32)) -> i64 { 4 | if time_base == DEFAULT_TIME_BASE { 5 | ts 6 | } else { 7 | let (tb_num, tb_denum) = time_base; 8 | let (target_num, target_denum) = DEFAULT_TIME_BASE; 9 | (ts * target_num as i64 * tb_denum as i64) / (target_denum as i64 * tb_num as i64) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /savant_etcd/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | etcd: 4 | image: docker.io/bitnamilegacy/etcd:3.6.4-debian-12-r4 5 | restart: on-failure 6 | environment: 7 | - ETCD_ADVERTISE_CLIENT_URLS=http://etcd:2379 8 | - ETCD_LISTEN_CLIENT_URLS=http://0.0.0.0:2379 9 | - ALLOW_NONE_AUTHENTICATION=yes 10 | ports: 11 | - "2379:2379" 12 | - "2380:2380" 13 | extra_hosts: 14 | - "host.docker.internal:host-gateway" 15 | -------------------------------------------------------------------------------- /savant_core_py/build.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | 3 | fn main() { 4 | pyo3_build_config::add_extension_module_link_args(); 5 | 6 | let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); 7 | let config = cbindgen::Config { 8 | language: cbindgen::Language::C, 9 | ..Default::default() 10 | }; 11 | cbindgen::generate_with_config(crate_dir, config) 12 | .unwrap() 13 | .write_to_file("../savant_python/python/savant_rs/include/savant_rs.h"); 14 | } 15 | -------------------------------------------------------------------------------- /savant_core/src/primitives/point.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize)] 2 | pub struct Point { 3 | pub x: f32, 4 | pub y: f32, 5 | } 6 | 7 | impl Point { 8 | pub fn new(x: f32, y: f32) -> Self { 9 | Self { x, y } 10 | } 11 | } 12 | 13 | #[cfg(test)] 14 | mod tests { 15 | #[test] 16 | fn test_point() { 17 | let p = super::Point::new(1.0, 2.0); 18 | assert_eq!(p.x, 1.0); 19 | assert_eq!(p.y, 2.0); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /services/replay/replay/scripts/ao-rtsp.sh: -------------------------------------------------------------------------------- 1 | docker run --rm -it --name sink-always-on-rtsp \ 2 | --gpus=all \ 3 | --network="host" \ 4 | -v ./assets/stub_imgs:/stub_imgs \ 5 | -e ZMQ_ENDPOINT=sub+bind:tcp://127.0.0.1:6666 \ 6 | -e SOURCE_ID=vod-video-1 \ 7 | -e FRAMERATE=25/1 \ 8 | -e STUB_FILE_LOCATION=/stub_imgs/smpte100_640x360.jpeg \ 9 | -e DEV_MODE=True \ 10 | ghcr.io/insight-platform/savant-adapters-deepstream:latest \ 11 | python -m adapters.ds.sinks.always_on_rtsp 12 | 13 | -------------------------------------------------------------------------------- /savant_python/python/savant_rs/webserver/webserver.pyi: -------------------------------------------------------------------------------- 1 | __all__ = [ 2 | "init_webserver", 3 | "stop_webserver", 4 | "set_shutdown_token", 5 | "is_shutdown_set", 6 | "set_status_running", 7 | "set_shutdown_signal", 8 | ] 9 | 10 | def init_webserver(port: int) -> None: ... 11 | def stop_webserver() -> None: ... 12 | def set_shutdown_token(token: str) -> None: ... 13 | def is_shutdown_set() -> bool: ... 14 | def set_status_running() -> None: ... 15 | def set_shutdown_signal(signal: int) -> None: ... 16 | -------------------------------------------------------------------------------- /python/primitives/eos.py: -------------------------------------------------------------------------------- 1 | from savant_rs import version 2 | from savant_rs.primitives import EndOfStream 3 | from savant_rs.utils.serialization import (load_message_from_bytes, 4 | save_message_to_bytes) 5 | 6 | print("Savant version:", version()) 7 | 8 | e = EndOfStream("abc") 9 | 10 | m = e.to_message() 11 | s = save_message_to_bytes(m) 12 | new_m = load_message_from_bytes(s) 13 | assert new_m.is_end_of_stream() 14 | 15 | e = new_m.as_end_of_stream() 16 | assert e.source_id == "abc" 17 | -------------------------------------------------------------------------------- /savant_protobuf/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "savant_protobuf" 3 | version.workspace = true 4 | edition.workspace = true 5 | authors.workspace = true 6 | description.workspace = true 7 | homepage.workspace = true 8 | repository.workspace = true 9 | readme.workspace = true 10 | keywords.workspace = true 11 | categories.workspace = true 12 | license.workspace = true 13 | rust-version.workspace = true 14 | 15 | [dependencies] 16 | prost = { workspace = true } 17 | 18 | [build-dependencies] 19 | prost-build = { workspace = true } 20 | -------------------------------------------------------------------------------- /python/primitives/bbox/bug_091.py: -------------------------------------------------------------------------------- 1 | from savant_rs.draw_spec import PaddingDraw 2 | from savant_rs.primitives.geometry import BBox 3 | 4 | padding = PaddingDraw(0, 0, 0, 0) 5 | 6 | max_col = 1279 7 | max_row = 719 8 | 9 | bbox = BBox(1279, 719, 2, 2) 10 | 11 | vis_bbox = bbox.get_visual_box(padding, 0, max_col, max_row) 12 | print(vis_bbox) 13 | left, top, right, bottom = vis_bbox.as_ltrb_int() 14 | print(left, top, right, bottom) 15 | 16 | assert left >= 0 17 | assert right <= max_col 18 | assert top >= 0 19 | assert bottom <= max_row 20 | -------------------------------------------------------------------------------- /python/primitives/object_track_id_range.py: -------------------------------------------------------------------------------- 1 | from savant_rs.logging import LogLevel, set_log_level 2 | from savant_rs.primitives import VideoObject 3 | from savant_rs.primitives.geometry import BBox 4 | 5 | set_log_level(LogLevel.Trace) 6 | 7 | obj = VideoObject( 8 | id=1, 9 | namespace="some", 10 | label="person", 11 | detection_box=BBox(0.1, 0.2, 0.3, 0.4).as_rbbox(), 12 | confidence=0.5, 13 | attributes=[], 14 | track_id=2**66, # must be cropped 15 | track_box=BBox(0.1, 0.2, 0.3, 0.4).as_rbbox(), 16 | ) 17 | -------------------------------------------------------------------------------- /savant_deepstream/deepstream-sys/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "deepstream-sys" 3 | version.workspace = true 4 | edition.workspace = true 5 | authors.workspace = true 6 | description.workspace = true 7 | homepage.workspace = true 8 | repository.workspace = true 9 | readme.workspace = true 10 | keywords.workspace = true 11 | categories.workspace = true 12 | license.workspace = true 13 | rust-version.workspace = true 14 | 15 | links = "nvdsgst_meta" 16 | 17 | [dependencies] 18 | 19 | [build-dependencies] 20 | bindgen = { workspace = true } 21 | -------------------------------------------------------------------------------- /services/buffer_ng/src/rocksdb/utilities.rs: -------------------------------------------------------------------------------- 1 | use crate::rocksdb::{MAX_ALLOWED_INDEX, U64_BYTE_LEN}; 2 | 3 | pub fn u64_from_byte_vec(v: &[u8]) -> u64 { 4 | let mut buf = [0u8; U64_BYTE_LEN]; 5 | buf.copy_from_slice(v); 6 | u64::from_le_bytes(buf) 7 | } 8 | 9 | pub fn index_to_key(index: u64) -> [u8; U64_BYTE_LEN] { 10 | index.to_le_bytes() 11 | } 12 | 13 | pub fn next_index(index: u64) -> u64 { 14 | let mut next = index + 1; 15 | if next == MAX_ALLOWED_INDEX { 16 | next = 0; 17 | } 18 | next 19 | } 20 | -------------------------------------------------------------------------------- /python/primitives/frame_geometric_transformations.py: -------------------------------------------------------------------------------- 1 | from savant_rs.utils import VideoObjectBBoxTransformation, gen_frame 2 | 3 | f = gen_frame() 4 | obj = f.get_object(0) 5 | obj.detection_box.xc = 25 6 | obj.detection_box.yc = 50 7 | obj.detection_box.height = 10 8 | obj.detection_box.width = 20 9 | print(obj) 10 | 11 | transformations = [ 12 | VideoObjectBBoxTransformation.scale(2.0, 2.0), 13 | VideoObjectBBoxTransformation.shift(10, 10), 14 | ] 15 | 16 | f.transform_geometry(transformations) 17 | obj = f.get_object(0) 18 | print(obj) 19 | -------------------------------------------------------------------------------- /savant_core/src/primitives/shutdown.rs: -------------------------------------------------------------------------------- 1 | use crate::json_api::ToSerdeJsonValue; 2 | use serde_json::Value; 3 | 4 | #[derive(Debug, PartialEq, Clone, serde::Serialize)] 5 | pub struct Shutdown(String); 6 | 7 | impl Shutdown { 8 | pub fn new(auth: &str) -> Self { 9 | Self(auth.to_string()) 10 | } 11 | 12 | pub fn get_auth(&self) -> &str { 13 | &self.0 14 | } 15 | } 16 | 17 | impl ToSerdeJsonValue for Shutdown { 18 | fn to_serde_json_value(&self) -> Value { 19 | serde_json::json!(self) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /services/replay/replay/scripts/video-loop.sh: -------------------------------------------------------------------------------- 1 | docker run -it --rm --network="host" \ 2 | -v /tmp/video-loop-source-downloads:/tmp/video-loop-source-downloads \ 3 | -e LOCATION=https://eu-central-1.linodeobjects.com/savant-data/demo/shuffle_dance.mp4 \ 4 | -e DOWNLOAD_PATH=/tmp/video-loop-source-downloads \ 5 | -e ZMQ_ENDPOINT=dealer+connect:tcp://127.0.0.1:5555 \ 6 | -e SOURCE_ID=video \ 7 | -e SYNC_OUTPUT=True \ 8 | --entrypoint /opt/savant/adapters/gst/sources/video_loop.sh \ 9 | ghcr.io/insight-platform/savant-adapters-gstreamer:latest 10 | -------------------------------------------------------------------------------- /python/primitives/bbox/bug_096.py: -------------------------------------------------------------------------------- 1 | from savant_rs.draw_spec import PaddingDraw 2 | from savant_rs.primitives.geometry import BBox 3 | 4 | padding = PaddingDraw(0, 0, 0, 0) 5 | 6 | max_col = 1279 7 | max_row = 719 8 | 9 | bboxes = [ 10 | BBox(380.201, 603.9101, 2.3868103, 4.445815), 11 | BBox(490.4524, 603.80145, 2.9029846, 3.4778595), 12 | ] 13 | 14 | for bbox in bboxes: 15 | vis_bbox = bbox.get_visual_box(padding, 0, max_col, max_row) 16 | left, top, width, height = vis_bbox.as_ltwh_int() 17 | 18 | assert width >= 1 19 | assert height >= 1 20 | -------------------------------------------------------------------------------- /savant_python/python/savant_rs/py/client/__init__.py: -------------------------------------------------------------------------------- 1 | """Source/Sink framework for development and QA purposes.""" 2 | 3 | from .builder.sink import SinkBuilder 4 | from .builder.source import SourceBuilder 5 | from .frame_source import FrameSource 6 | from .image_source import JpegSource, PngSource 7 | from .log_provider import LogProvider 8 | from .log_provider.jaeger import JaegerLogProvider 9 | 10 | __all__ = [ 11 | 'SinkBuilder', 12 | 'SourceBuilder', 13 | 'LogProvider', 14 | 'FrameSource', 15 | 'JaegerLogProvider', 16 | 'JpegSource', 17 | 'PngSource', 18 | ] 19 | -------------------------------------------------------------------------------- /services/buffer_ng/src/rocksdb/fs.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | use std::{fs, io}; 3 | pub fn dir_size(path: &String) -> io::Result { 4 | fn dir_size(mut dir: fs::ReadDir) -> io::Result { 5 | dir.try_fold(0, |acc, file| { 6 | let file = file?; 7 | let size = match file.metadata()? { 8 | data if data.is_dir() => dir_size(fs::read_dir(file.path())?)?, 9 | data => data.len() as usize, 10 | }; 11 | Ok(acc + size) 12 | }) 13 | } 14 | dir_size(fs::read_dir(Path::new(path))?) 15 | } 16 | -------------------------------------------------------------------------------- /docs/source/_static/css/custom.css: -------------------------------------------------------------------------------- 1 | /* Increase content width */ 2 | .wy-nav-content { 3 | max-width: 80% !important; 4 | } 5 | 6 | /* Ensure code blocks don't overflow */ 7 | .wy-nav-content pre { 8 | max-width: 100%; 9 | overflow-x: auto; 10 | } 11 | 12 | /* Adjust table width */ 13 | .wy-nav-content table.docutils { 14 | max-width: 100%; 15 | overflow-x: auto; 16 | display: block; 17 | } 18 | 19 | /* Limit table column widths */ 20 | .wy-nav-content table.docutils th, 21 | .wy-nav-content table.docutils td { 22 | max-width: 500px; 23 | white-space: normal; 24 | word-wrap: break-word; 25 | } -------------------------------------------------------------------------------- /services/common/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod fps_meter; 2 | pub mod job_writer; 3 | pub mod source; 4 | use savant_core::utils::bytes_to_hex_string; 5 | use std::str::from_utf8; 6 | use std::time::{SystemTime, UNIX_EPOCH}; 7 | 8 | pub fn topic_to_string(topic: &[u8]) -> String { 9 | from_utf8(topic) 10 | .map(String::from) 11 | .unwrap_or(bytes_to_hex_string(topic)) 12 | } 13 | 14 | pub fn systime_ms() -> u128 { 15 | let start = SystemTime::now(); 16 | let since_the_epoch = start 17 | .duration_since(UNIX_EPOCH) 18 | .expect("Time went backwards"); 19 | since_the_epoch.as_millis() 20 | } 21 | -------------------------------------------------------------------------------- /savant_gstreamer/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "savant_gstreamer" 3 | version.workspace = true 4 | edition.workspace = true 5 | authors.workspace = true 6 | description.workspace = true 7 | homepage.workspace = true 8 | repository.workspace = true 9 | readme.workspace = true 10 | keywords.workspace = true 11 | categories.workspace = true 12 | license.workspace = true 13 | rust-version.workspace = true 14 | 15 | [lib] 16 | crate-type = ["rlib"] 17 | 18 | [dependencies] 19 | anyhow = { workspace = true } 20 | gstreamer = { workspace = true } 21 | lazy_static = { workspace = true } 22 | parking_lot = { workspace = true } 23 | 24 | -------------------------------------------------------------------------------- /savant_core_py/src/gst.rs: -------------------------------------------------------------------------------- 1 | use pyo3::prelude::*; 2 | 3 | #[pyclass(eq, eq_int)] 4 | #[derive(Clone, PartialEq, Debug)] 5 | pub enum FlowResult { 6 | CustomSuccess2, 7 | CustomSuccess1, 8 | CustomSuccess, 9 | Ok, 10 | NotLinked, 11 | Flushing, 12 | Eos, 13 | NotNegotiated, 14 | Error, 15 | NotSupported, 16 | CustomError, 17 | CustomError1, 18 | CustomError2, 19 | } 20 | 21 | #[pyclass(eq, eq_int)] 22 | #[derive(Clone, PartialEq, Debug)] 23 | pub enum InvocationReason { 24 | Buffer, 25 | SinkEvent, 26 | SourceEvent, 27 | StateChange, 28 | IngressMessageTransformer, 29 | } 30 | -------------------------------------------------------------------------------- /services/retina_rtsp/README.md: -------------------------------------------------------------------------------- 1 | # Retina RTSP Adapter 2 | 3 | Retina RTSP is a new generation Savant RTSP adapter built around Scott Lamb's [Retina](https://github.com/scottlamb/retina) project. This adapter supports multiple RTSP streams and RTSP synchronization with RTCP sender reports. It is not as capable as the FFmpeg-based RTSP adapter which potentially supports more cameras but provides its own unique features. 4 | 5 | * License: Apache 2 6 | * Documentation: https://insight-platform.github.io/savant-rs/services/retina_rtsp/index.html 7 | * Usage Sample: https://github.com/insight-platform/Savant/tree/develop/samples/retina_rtsp_rtcp_sr 8 | 9 | -------------------------------------------------------------------------------- /services/common/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "savant_services_common" 3 | version.workspace = true 4 | edition.workspace = true 5 | authors.workspace = true 6 | description.workspace = true 7 | homepage.workspace = true 8 | repository.workspace = true 9 | readme.workspace = true 10 | keywords.workspace = true 11 | categories.workspace = true 12 | license.workspace = true 13 | rust-version.workspace = true 14 | 15 | [dependencies] 16 | anyhow = { workspace = true } 17 | log = { workspace = true } 18 | mini-moka = { workspace = true } 19 | savant_core = { workspace = true } 20 | serde = { workspace = true } 21 | tokio = { workspace = true } 22 | 23 | -------------------------------------------------------------------------------- /savant_core_py/README.md: -------------------------------------------------------------------------------- 1 | # Savant Primitives And Optimized Algorithms 2 | 3 | Savant Library with new generation primitives re-implemented in Rust. 4 | 5 | Run tests: 6 | 7 | ```bash 8 | cargo test --no-default-features 9 | ``` 10 | 11 | Run benchmarks: 12 | ```bash 13 | cargo bench --no-default-features 14 | ``` 15 | 16 | Build Wheel: 17 | 18 | ```bash 19 | RUSTFLAGS=" -C target-cpu=x86-64-v3 -C opt-level=3" maturin build --release -o dist 20 | ``` 21 | 22 | Install Wheel: 23 | 24 | ```bash 25 | pip3 install --force-reinstall dist/savant_primitives-0.1.2-cp38-cp38-manylinux2014_x86_64.whl 26 | ``` 27 | 28 | ## License 29 | 30 | [Apache License 2.0](../LICENSE) -------------------------------------------------------------------------------- /savant_python/README.md: -------------------------------------------------------------------------------- 1 | # Savant Primitives And Optimized Algorithms 2 | 3 | Savant Library with new generation primitives re-implemented in Rust. 4 | 5 | Run tests: 6 | 7 | ```bash 8 | cargo test --no-default-features 9 | ``` 10 | 11 | Run benchmarks: 12 | ```bash 13 | cargo bench --no-default-features 14 | ``` 15 | 16 | Build Wheel: 17 | 18 | ```bash 19 | RUSTFLAGS=" -C target-cpu=x86-64-v3 -C opt-level=3" maturin build --release -o dist 20 | ``` 21 | 22 | Install Wheel: 23 | 24 | ```bash 25 | pip3 install --force-reinstall dist/savant_primitives-0.1.2-cp38-cp38-manylinux2014_x86_64.whl 26 | ``` 27 | 28 | ## License 29 | 30 | [Apache License 2.0](../LICENSE) -------------------------------------------------------------------------------- /savant_python/python/savant_rs/py/client/runner/log_result.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Optional 3 | 4 | from ..log_provider import LogProvider, Logs 5 | 6 | 7 | @dataclass 8 | class LogResult: 9 | trace_id: Optional[str] 10 | """OpenTelemetry trace ID of the message.""" 11 | log_provider: Optional[LogProvider] 12 | """Log provider for to fetch the logs.""" 13 | 14 | def logs(self) -> Logs: 15 | """Fetch logs from log provider for this result.""" 16 | 17 | if self.log_provider is not None and self.trace_id is not None: 18 | return self.log_provider.logs(self.trace_id) 19 | return Logs([]) 20 | -------------------------------------------------------------------------------- /services/replay/replaydb/src/lib.rs: -------------------------------------------------------------------------------- 1 | use savant_core::primitives::frame::VideoFrameProxy; 2 | use uuid::{NoContext, Timestamp, Uuid}; 3 | 4 | pub mod job; 5 | pub mod service; 6 | pub mod store; 7 | pub mod stream_processor; 8 | 9 | pub type ParkingLotMutex = parking_lot::Mutex; 10 | 11 | pub fn get_keyframe_boundary(v: Option, default: u64) -> Uuid { 12 | let ts = v.unwrap_or(default); 13 | Uuid::new_v7(Timestamp::from_unix(NoContext, ts, 0)) 14 | } 15 | 16 | pub(crate) fn best_ts(f: &VideoFrameProxy) -> i64 { 17 | let dts_opt = f.get_dts(); 18 | if let Some(dts) = dts_opt { 19 | dts 20 | } else { 21 | f.get_pts() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /savant_python/python/savant_rs/py/client/frame_source/__init__.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import Tuple, TypeVar 3 | 4 | from savant_rs.primitives import VideoFrame, VideoFrameUpdate 5 | 6 | T = TypeVar('T', bound='FrameSource') 7 | 8 | 9 | class FrameSource(ABC): 10 | """Interface for frame sources.""" 11 | 12 | @abstractmethod 13 | def with_update(self: T, update: VideoFrameUpdate) -> T: 14 | """Apply an update to a frame.""" 15 | 16 | @abstractmethod 17 | def build_frame(self) -> Tuple[VideoFrame, bytes]: 18 | """Build a frame. 19 | 20 | :return: A tuple of a frame metadata and its content. 21 | """ 22 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ghcr.io/insight-platform/savant_rust:latest AS base 2 | 3 | FROM base AS devcontainer 4 | 5 | RUN apt-get update && apt-get install -y clang build-essential \ 6 | libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev 7 | 8 | ARG REMOTE_USER 9 | ARG REMOTE_UID 10 | ARG REMOTE_GID 11 | RUN < VideoFrame { 8 | VideoFrame(savant_core::test::gen_empty_frame()) 9 | } 10 | 11 | #[pyfunction] 12 | pub fn gen_frame() -> VideoFrame { 13 | VideoFrame(savant_core::test::gen_frame()) 14 | } 15 | 16 | pub fn gen_object(id: i64) -> VideoObject { 17 | VideoObject(savant_core::test::gen_object(id)) 18 | } 19 | 20 | #[inline(always)] 21 | pub fn s(a: &str) -> String { 22 | a.to_string() 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /services/replay/replay/src/web_service/new_job.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{put, web, Responder}; 2 | use log::info; 3 | 4 | use replaydb::job::query::JobQuery; 5 | use replaydb::service::JobManager; 6 | 7 | use crate::web_service::{JobService, ResponseMessage}; 8 | 9 | #[put("/job")] 10 | async fn new_job(js: web::Data, query: web::Json) -> impl Responder { 11 | info!("Received New Job Query: {:?}", query); 12 | let mut js_bind = js.service.lock().await; 13 | let job = js_bind.add_job(query.into_inner()).await; 14 | match job { 15 | Ok(job) => ResponseMessage::NewJob(job.to_string()), 16 | Err(e) => ResponseMessage::Error(e.to_string()), 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /savant_core_py/src/atomic_counter.rs: -------------------------------------------------------------------------------- 1 | use pyo3::prelude::*; 2 | use std::sync::atomic::AtomicU64; 3 | 4 | #[pyclass] 5 | pub struct AtomicCounter(AtomicU64); 6 | 7 | #[pymethods] 8 | impl AtomicCounter { 9 | #[new] 10 | fn new(initial: u64) -> Self { 11 | AtomicCounter(AtomicU64::new(initial)) 12 | } 13 | 14 | fn set(&self, value: u64) { 15 | self.0.store(value, std::sync::atomic::Ordering::SeqCst); 16 | } 17 | 18 | #[getter] 19 | fn next(&self) -> u64 { 20 | self.0.fetch_add(1, std::sync::atomic::Ordering::SeqCst) 21 | } 22 | 23 | #[getter] 24 | fn get(&self) -> u64 { 25 | self.0.load(std::sync::atomic::Ordering::SeqCst) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /services/retina_rtsp/assets/empty_configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "sink": { 3 | "url": "pub+connect:tcp://127.0.0.1:3333", 4 | "options": { 5 | "send_timeout": { 6 | "secs": 1, 7 | "nanos": 0 8 | }, 9 | "send_retries": 3, 10 | "receive_timeout": { 11 | "secs": 1, 12 | "nanos": 0 13 | }, 14 | "receive_retries": 3, 15 | "send_hwm": 1000, 16 | "receive_hwm": 1000, 17 | "inflight_ops": 100 18 | } 19 | }, 20 | "rtsp_sources": {}, 21 | "reconnect_interval": { 22 | "secs": 5, 23 | "nanos": 0 24 | } 25 | } -------------------------------------------------------------------------------- /savant_python/python/savant_rs/primitives/__init__.py: -------------------------------------------------------------------------------- 1 | from .attribute import * # type: ignore 2 | from .attribute_value import * # type: ignore 3 | from .end_of_stream import * # type: ignore 4 | from .shutdown import * # type: ignore 5 | from .user_data import * # type: ignore 6 | from .video_frame import * # type: ignore 7 | from .video_object import * # type: ignore 8 | 9 | __all__ = ( 10 | video_frame.__all__ # type: ignore 11 | + attribute_value.__all__ # type: ignore 12 | + attribute.__all__ # type: ignore 13 | + end_of_stream.__all__ # type: ignore 14 | + shutdown.__all__ # type: ignore 15 | + user_data.__all__ # type: ignore 16 | + video_object.__all__ # type: ignore 17 | ) 18 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /savant_python/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "savant_rs" 3 | version.workspace = true 4 | edition.workspace = true 5 | authors.workspace = true 6 | description.workspace = true 7 | homepage.workspace = true 8 | repository.workspace = true 9 | readme.workspace = true 10 | keywords.workspace = true 11 | categories.workspace = true 12 | license.workspace = true 13 | rust-version.workspace = true 14 | 15 | [lib] 16 | crate-type = ["dylib"] 17 | 18 | [dependencies] 19 | savant_core_py = { workspace = true } 20 | pretty_env_logger = { workspace = true } 21 | pyo3 = { workspace = true } 22 | 23 | [build-dependencies] 24 | pyo3-build-config = { workspace = true } 25 | 26 | [package.metadata.maturin] 27 | python-source = "python" 28 | -------------------------------------------------------------------------------- /savant_python/python/savant_rs/logging/logging.pyi: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import Dict, Optional 3 | 4 | __all__ = [ 5 | "LogLevel", 6 | "set_log_level", 7 | "get_log_level", 8 | "log_level_enabled", 9 | "log", 10 | ] 11 | 12 | class LogLevel(Enum): 13 | Trace: int 14 | Debug: int 15 | Info: int 16 | Warning: int 17 | Error: int 18 | Off: int 19 | 20 | def set_log_level(level: LogLevel) -> LogLevel: ... 21 | def get_log_level() -> LogLevel: ... 22 | def log_level_enabled(level: LogLevel) -> bool: ... 23 | def log( 24 | level: LogLevel, 25 | target: str, 26 | message: str, 27 | params: Optional[Dict[str, str]] = None, 28 | no_gil: bool = True, 29 | ) -> None: ... 30 | -------------------------------------------------------------------------------- /savant_etcd/src/lib.rs: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Ivan Kudriavtsev 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | pub mod etcd_api; 17 | pub mod parameter_storage; 18 | -------------------------------------------------------------------------------- /savant_python/python/savant_rs/gstreamer/gstreamer.pyi: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import List, Optional 3 | 4 | from savant_rs.primitives import Attribute 5 | 6 | __all__ = [ 7 | "FlowResult", 8 | "InvocationReason", 9 | ] 10 | 11 | class FlowResult(Enum): 12 | CustomSuccess2: ... 13 | CustomSuccess1: ... 14 | CustomSuccess: ... 15 | Ok: ... 16 | NotLinked: ... 17 | Flushing: ... 18 | Eos: ... 19 | NotNegotiated: ... 20 | Error: ... 21 | NotSupported: ... 22 | CustomError: ... 23 | CustomError1: ... 24 | CustomError2: ... 25 | 26 | class InvocationReason(Enum): 27 | Buffer: ... 28 | SinkEvent: ... 29 | SourceEvent: ... 30 | StateChange: ... 31 | IngressMessageTransformer: ... 32 | -------------------------------------------------------------------------------- /savant_launcher/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "savant_launcher" 3 | version.workspace = true 4 | edition.workspace = true 5 | authors.workspace = true 6 | description.workspace = true 7 | homepage.workspace = true 8 | repository.workspace = true 9 | readme.workspace = true 10 | keywords.workspace = true 11 | categories.workspace = true 12 | license.workspace = true 13 | rust-version.workspace = true 14 | 15 | [dependencies] 16 | anyhow = { workspace = true } 17 | clap = { workspace = true } 18 | ctrlc = { workspace = true } 19 | log = { workspace = true } 20 | pyo3 = { workspace = true, features = ["auto-initialize"] } 21 | savant_rs = { workspace = true } 22 | gstreamer = { workspace = true } 23 | gstreamer-base = { workspace = true } 24 | glib = { workspace = true } 25 | -------------------------------------------------------------------------------- /services/common/src/fps_meter.rs: -------------------------------------------------------------------------------- 1 | use std::time::SystemTime; 2 | 3 | pub struct FpsMeter { 4 | counter: u64, 5 | last_time: SystemTime, 6 | } 7 | 8 | impl Default for FpsMeter { 9 | fn default() -> Self { 10 | Self { 11 | counter: 0, 12 | last_time: SystemTime::now(), 13 | } 14 | } 15 | } 16 | 17 | impl FpsMeter { 18 | pub fn increment(&mut self) { 19 | self.counter += 1; 20 | } 21 | 22 | pub fn get_fps(&mut self) -> f64 { 23 | let now = SystemTime::now(); 24 | let duration = now.duration_since(self.last_time).unwrap(); 25 | let fps = self.counter as f64 / duration.as_secs_f64(); 26 | self.last_time = now; 27 | self.counter = 0; 28 | fps 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /python/utils/relative_uuid_v7.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | from time import sleep 3 | 4 | from savant_rs.utils import incremental_uuid_v7, relative_time_uuid_v7 5 | 6 | now_uuid = incremental_uuid_v7() 7 | parsed_now_uuid = uuid.UUID(now_uuid) 8 | print("now_uuid: ", now_uuid) 9 | print("parsed_now_uuid: ", parsed_now_uuid) 10 | 11 | future_uuid = relative_time_uuid_v7(now_uuid, 1000) 12 | parsed_future_uuid = uuid.UUID(future_uuid) 13 | print("future_uuid: ", future_uuid) 14 | print("parsed_future_uuid: ", parsed_future_uuid) 15 | 16 | int_uuid = parsed_now_uuid.int 17 | int_future_uuid = parsed_future_uuid.int 18 | 19 | assert int_future_uuid > int_uuid 20 | 21 | sleep(1) 22 | 23 | int_new_now_uuid = uuid.UUID(incremental_uuid_v7()).int 24 | 25 | assert int_new_now_uuid > int_future_uuid 26 | -------------------------------------------------------------------------------- /services/replay/replay/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "replay" 3 | description = "Replay Service" 4 | license = "../LICENSE" 5 | readme = "../README.md" 6 | 7 | version.workspace = true 8 | edition.workspace = true 9 | authors.workspace = true 10 | homepage.workspace = true 11 | repository.workspace = true 12 | keywords.workspace = true 13 | categories.workspace = true 14 | rust-version.workspace = true 15 | 16 | [dependencies] 17 | anyhow = { workspace = true } 18 | env_logger = { workspace = true } 19 | log = { workspace = true } 20 | replaydb = { workspace = true } 21 | savant_core = { workspace = true } 22 | tokio = { workspace = true } 23 | serde = { workspace = true } 24 | serde_json = { workspace = true } 25 | uuid = { workspace = true } 26 | actix-web = { workspace = true } 27 | -------------------------------------------------------------------------------- /docs/source/services/retina_rtsp/index.rst: -------------------------------------------------------------------------------- 1 | Retina RTSP Service Documentation 2 | ================================= 3 | 4 | Retina RTSP is a specialized service for handling RTSP video streams with advanced synchronization capabilities: 5 | 6 | - connects to multiple RTSP sources and streams them to Savant sinks or modules; 7 | - supports time-synchronized streaming from multiple sources; 8 | - provides NTP and RTCP SR synchronization mechanisms; 9 | - handles authentication for protected RTSP sources; 10 | - automatically reconnects to sources in case of disconnection; 11 | - can work as a sidecar or intermediary service in Savant pipelines. 12 | 13 | .. toctree:: 14 | :maxdepth: 2 15 | :caption: Contents 16 | 17 | 0_introduction 18 | 1_platforms 19 | 2_installation 20 | 3_configuration 21 | -------------------------------------------------------------------------------- /services/replay/replay/src/web_service/status.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{get, web, Responder}; 2 | use log::error; 3 | 4 | use replaydb::service::JobManager; 5 | 6 | use crate::web_service::{JobService, ResponseMessage}; 7 | 8 | #[get("/status")] 9 | async fn status(js: web::Data) -> impl Responder { 10 | let mut js_bind = js.service.lock().await; 11 | match js_bind.check_stream_processor_finished().await { 12 | Ok(finished) => { 13 | if finished { 14 | ResponseMessage::StatusFinished 15 | } else { 16 | ResponseMessage::StatusRunning 17 | } 18 | } 19 | Err(e) => { 20 | error!("Stream processor finished with error: {}", e); 21 | ResponseMessage::Error(e.to_string()) 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /savant_core/benches/bench_frame_save_load_pb.rs: -------------------------------------------------------------------------------- 1 | use criterion::{criterion_group, criterion_main, Criterion}; 2 | use savant_core::primitives::rust::VideoFrameProxy; 3 | use savant_core::protobuf::{from_pb, ToProtobuf}; 4 | use savant_core::test::gen_frame; 5 | use std::hint::black_box; 6 | 7 | fn frame_pb_benchmarks(c: &mut Criterion) { 8 | let mut group = c.benchmark_group("frame_protobuf"); 9 | 10 | group.bench_function("save_load_video_frame_pb", |b| { 11 | let frame = gen_frame(); 12 | b.iter(|| { 13 | let res = black_box(frame.to_pb().unwrap()); 14 | black_box(from_pb::(&res).unwrap()); 15 | }) 16 | }); 17 | 18 | group.finish(); 19 | } 20 | 21 | criterion_group!(benches, frame_pb_benchmarks); 22 | criterion_main!(benches); 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 7 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 8 | Cargo.lock 9 | 10 | # These are backup files generated by rustfmt 11 | **/*.rs.bk 12 | 13 | # MSVC Windows builds of rustc generate these, which store debugging information 14 | **/*.pdb 15 | **/*.gz 16 | **/*.zip 17 | **/*.so 18 | **/*.tar 19 | **/*.engine 20 | # Added by cargo 21 | 22 | /.idea 23 | /dist 24 | /target 25 | /Cargo.lock 26 | /docs/build 27 | /docs/source/generated 28 | 29 | /venv 30 | /venv312 31 | 32 | **/*.pyc 33 | 34 | .zed 35 | .vscode 36 | 37 | **/*.crt 38 | **/*.key 39 | **/*.csr 40 | 41 | services/replay/samples/file_restreaming/data -------------------------------------------------------------------------------- /python/primitives/user_data.py: -------------------------------------------------------------------------------- 1 | from savant_rs.primitives import AttributeValue, UserData 2 | from savant_rs.utils.serialization import (Message, load_message_from_bytes, 3 | save_message_to_bytes) 4 | 5 | t = UserData("abc") 6 | t.set_persistent_attribute( 7 | namespace="some", 8 | name="attr", 9 | hint="x", 10 | is_hidden=False, 11 | values=[AttributeValue.float(1.0, confidence=0.5)], 12 | ) 13 | 14 | pb = t.to_protobuf() 15 | restored = UserData.from_protobuf(pb) 16 | assert t.json == restored.json 17 | 18 | print("Before") 19 | print(t.json_pretty) 20 | 21 | m = Message.user_data(t) 22 | s = save_message_to_bytes(m) 23 | new_m = load_message_from_bytes(s) 24 | assert new_m.is_user_data() 25 | 26 | t = new_m.as_user_data() 27 | assert t.source_id == "abc" 28 | 29 | print("After") 30 | print(t.json_pretty) 31 | -------------------------------------------------------------------------------- /docs/source/services/retina_rtsp/1_platforms.rst: -------------------------------------------------------------------------------- 1 | Hardware Requirements 2 | ===================== 3 | 4 | CPU 5 | --- 6 | 7 | Currently, we support: 8 | 9 | - ARM64 (Nvidia Jetson, Raspberry Pi); 10 | - X86-64 (Intel/AMD CPUs). 11 | 12 | We mostly build and test ARM64 support on Jetson Orin Nano and NX platforms. If you find any problems with Raspberry Pi or another ARM64 platform, please let us know. 13 | 14 | RAM 15 | --- 16 | 17 | The system needs a very small amount of RAM. We recommend having at least 1GB of RAM. However, you should be able to run Replay even with 512MB of RAM without problems. Depending on the number of streams, bitrates, and synchronization buffer size, you may need more RAM. 18 | 19 | Storage 20 | ------- 21 | 22 | The service does not require storage aside of the space required to store docker images and container layers. 2 GB of storage is enough. 23 | -------------------------------------------------------------------------------- /savant_core/benches/bench_label_filter.rs: -------------------------------------------------------------------------------- 1 | use criterion::{criterion_group, criterion_main, Criterion}; 2 | use std::hint::black_box; 3 | 4 | fn label_filter_benchmarks(c: &mut Criterion) { 5 | let mut group = c.benchmark_group("label_filter"); 6 | 7 | group.bench_function("label_filter", |b| { 8 | use savant_core::message::label_filter::LabelFilterRule::*; 9 | let rule = Or(vec![ 10 | Set("test".to_string()), 11 | Not(Box::new(Or(vec![ 12 | Set("test2".to_string()), 13 | Set("test3".to_string()), 14 | ]))), 15 | ]); 16 | 17 | b.iter(|| { 18 | black_box(rule.matches(&["test".to_string(), "test2".to_string()])); 19 | }) 20 | }); 21 | 22 | group.finish(); 23 | } 24 | 25 | criterion_group!(benches, label_filter_benchmarks); 26 | criterion_main!(benches); 27 | -------------------------------------------------------------------------------- /savant_deepstream/deepstream_nvinfer/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "deepstream_nvinfer" 3 | version.workspace = true 4 | edition.workspace = true 5 | authors.workspace = true 6 | description = "Safe Rust API for NVIDIA DeepStream NvInfer engine" 7 | homepage.workspace = true 8 | repository.workspace = true 9 | readme.workspace = true 10 | keywords.workspace = true 11 | categories.workspace = true 12 | license.workspace = true 13 | rust-version.workspace = true 14 | 15 | [dependencies] 16 | anyhow = { workspace = true } 17 | deepstream-sys = { path = "../deepstream-sys" } 18 | thiserror = { workspace = true } 19 | log = { workspace = true } 20 | 21 | [dev-dependencies] 22 | cudarc = { version = "0.17", features = ["cuda-12020"] } 23 | criterion = { workspace = true } 24 | env_logger = "0.10" 25 | 26 | [[bench]] 27 | name = "infer_context" 28 | path = "benchmarks/infer_context.rs" 29 | harness = false 30 | -------------------------------------------------------------------------------- /savant_python/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["maturin>=1.8"] 3 | build-backend = "maturin" 4 | 5 | [project] 6 | dependencies = [ 7 | "pretty-traceback==2024.1021" 8 | ] 9 | dynamic = ['version'] 10 | name = "savant_rs" 11 | requires-python = ">=3.10" 12 | classifiers = [ 13 | "Programming Language :: Rust", 14 | "Programming Language :: Python :: Implementation :: CPython", 15 | "Programming Language :: Python :: Implementation :: PyPy", 16 | ] 17 | 18 | [project.optional-dependencies] 19 | clientsdk = [ 20 | "python-magic~=0.4.27", 21 | "requests~=2.32.5", 22 | "numpy>=1.26", 23 | "opencv-python~=4.12.0" 24 | ] 25 | 26 | [tool.black] 27 | skip-string-normalization = true 28 | 29 | [tool.pylint.messages_control] 30 | max-line-length = 88 31 | 32 | [tool.maturin] 33 | python-source = "python" 34 | include = ["*"] 35 | features = ["pyo3/extension-module"] 36 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | %SPHINXBUILD% >NUL 2>NUL 14 | if errorlevel 9009 ( 15 | echo. 16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 17 | echo.installed, then set the SPHINXBUILD environment variable to point 18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 19 | echo.may add the Sphinx directory to PATH. 20 | echo. 21 | echo.If you don't have Sphinx installed, grab it from 22 | echo.https://www.sphinx-doc.org/ 23 | exit /b 1 24 | ) 25 | 26 | if "%1" == "" goto help 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /savant_core_py/src/capi.rs: -------------------------------------------------------------------------------- 1 | pub mod frame; 2 | pub mod object; 3 | pub mod pipeline; 4 | 5 | use std::ffi::{c_char, CStr}; 6 | 7 | /// # Safety 8 | /// 9 | /// The function is intended for invocation from C/C++, so it is unsafe by design. 10 | #[no_mangle] 11 | pub unsafe extern "C" fn check_version(external_version: *const c_char) -> bool { 12 | let external_version = CStr::from_ptr(external_version); 13 | savant_core::version() 14 | == *external_version.to_str().expect( 15 | "Failed to convert external version to string. This is a bug. Please report it.", 16 | ) 17 | } 18 | 19 | #[cfg(test)] 20 | mod tests { 21 | use std::ffi::CString; 22 | 23 | #[test] 24 | fn test_check_version() { 25 | unsafe { 26 | let ver = CString::new(savant_core::version()).unwrap(); 27 | assert!(crate::capi::check_version(ver.as_ptr())); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /savant_etcd/utils/gen_keys.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | DIR=$(dirname $0)/../assets/certs 4 | 5 | # CA 6 | openssl genpkey -algorithm RSA -out $DIR/ca.key 7 | openssl req -new -x509 -days 365000 -key $DIR/ca.key -out $DIR/ca.crt -subj "/CN=local-etcd" 8 | 9 | # SERVER 10 | openssl genpkey -algorithm RSA -out $DIR/server.key 11 | openssl req -new -key $DIR/server.key -out $DIR/server.csr -subj "/CN=localhost" 12 | openssl x509 -req -days 365000 -in $DIR/server.csr -CA $DIR/ca.crt -CAkey $DIR/ca.key -CAcreateserial -out $DIR/server.crt -extfile <(echo "subjectAltName=IP:127.0.0.1") 13 | 14 | # CLIENT 15 | openssl genpkey -algorithm RSA -out $DIR/client.key 16 | openssl req -new -key $DIR/client.key -out $DIR/client.csr -subj "/CN=localhost" 17 | openssl x509 -req -days 365000 -in $DIR/client.csr -CA $DIR/ca.crt -CAkey $DIR/ca.key -CAcreateserial -out $DIR/client.crt -extfile <(echo "subjectAltName=IP:127.0.0.1") 18 | 19 | chmod 0666 $DIR/* -------------------------------------------------------------------------------- /savant_deepstream/deepstream_nvinfer/src/infer_context/output.rs: -------------------------------------------------------------------------------- 1 | use super::DataType; 2 | use crate::infer_tensor_meta::InferDims; 3 | 4 | #[derive(Debug)] 5 | pub struct OutputLayer { 6 | pub layer_name: String, 7 | pub dimensions: InferDims, 8 | pub data_type: DataType, 9 | pub byte_length: usize, 10 | pub device_address: *mut std::ffi::c_void, 11 | pub host_address: *mut std::ffi::c_void, 12 | } 13 | 14 | #[derive(Debug, Default)] 15 | pub struct FrameOutput { 16 | output_layers: Vec, 17 | } 18 | 19 | impl FrameOutput { 20 | pub fn new() -> Self { 21 | Self { 22 | output_layers: Vec::new(), 23 | } 24 | } 25 | 26 | pub fn add_output_layer(&mut self, output_layer: OutputLayer) { 27 | self.output_layers.push(output_layer); 28 | } 29 | 30 | pub fn output_layers(&self) -> &Vec { 31 | &self.output_layers 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /savant_deepstream/deepstream/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "deepstream" 3 | version.workspace = true 4 | edition.workspace = true 5 | authors.workspace = true 6 | description.workspace = true 7 | homepage.workspace = true 8 | repository.workspace = true 9 | readme.workspace = true 10 | keywords.workspace = true 11 | categories.workspace = true 12 | license.workspace = true 13 | rust-version.workspace = true 14 | 15 | [dependencies] 16 | anyhow = { workspace = true } 17 | crossbeam = { workspace = true } 18 | deepstream-sys = { path = "../deepstream-sys" } 19 | deepstream_nvinfer = { path = "../deepstream_nvinfer" } 20 | glib = { workspace = true } 21 | gstreamer = { workspace = true } 22 | thiserror = { workspace = true } 23 | log = { workspace = true } 24 | parking_lot = { workspace = true } 25 | 26 | [dev-dependencies] 27 | cudarc = { version = "0.17", features = ["cuda-12020"] } 28 | criterion = { workspace = true } 29 | env_logger = "0.10" 30 | -------------------------------------------------------------------------------- /savant_etcd/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "savant_etcd" 3 | version.workspace = true 4 | edition.workspace = true 5 | authors.workspace = true 6 | description.workspace = true 7 | homepage.workspace = true 8 | repository.workspace = true 9 | readme.workspace = true 10 | keywords.workspace = true 11 | categories.workspace = true 12 | license.workspace = true 13 | rust-version.workspace = true 14 | 15 | [dependencies] 16 | log = { workspace = true } 17 | anyhow = { workspace = true } 18 | async-trait = { workspace = true } # "0.1" 19 | crc32fast = { workspace = true } 20 | env_logger = { workspace = true } 21 | etcd-client = { workspace = true, features = ["tls", "tls-roots"] } 22 | futures = { workspace = true } 23 | glob = { workspace = true } 24 | hashbrown = { workspace = true } 25 | thiserror = { workspace = true } 26 | parking_lot = { workspace = true } 27 | tokio = { workspace = true } 28 | 29 | [dev-dependencies] 30 | bollard = { workspace = true } 31 | -------------------------------------------------------------------------------- /savant_protobuf/build.rs: -------------------------------------------------------------------------------- 1 | extern crate prost_build; 2 | 3 | use std::path::PathBuf; 4 | use std::{env, fs}; 5 | 6 | fn main() { 7 | let proto_path = PathBuf::from("src/savant_rs.proto"); 8 | let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); 9 | let src_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()).join("src"); 10 | let out_path = out_dir.join("protocol.rs"); 11 | let module_path = src_dir.join("generated.rs"); 12 | 13 | let mut config = prost_build::Config::new(); 14 | config.protoc_arg("--experimental_allow_proto3_optional"); 15 | config.enum_attribute(".", "#[allow(clippy::large_enum_variant)]"); 16 | config 17 | .compile_protos( 18 | &[proto_path.to_str().unwrap()], 19 | &[proto_path.parent().unwrap().to_str().unwrap()], 20 | ) 21 | .expect("Failed to compile protobuf definitions"); 22 | 23 | fs::copy(out_path, module_path).unwrap(); 24 | } 25 | -------------------------------------------------------------------------------- /docs/source/services/replay/1_platforms.rst: -------------------------------------------------------------------------------- 1 | Hardware Requirements 2 | ========================== 3 | 4 | CPU 5 | --- 6 | 7 | Currently, we support two platforms: 8 | 9 | - ARM64 (Nvidia Jetson, Raspberry Pi 4/5, AWS Graviton, etc); 10 | - X86_64 (Intel/AMD CPUs). 11 | 12 | RAM 13 | --- 14 | 15 | The system uses RocksDB as a storage engine, which benefit from having more RAM. We recommend having at least 4GB of RAM. However, you should be able to run Replay even with 512MB of RAM without problems. 16 | 17 | Storage 18 | ------- 19 | 20 | Replay uses RocksDB as a storage engine, which is designed for best operation on SSDs. However, it can work on HDDs as well. We recommend using SSD or HDD, but not SD cards due to a low IOPS and fast wear-out. 21 | 22 | If you are using Replay to collect data and run long-running jobs, HDDs are fine. If you are using Replay for quick living, fast video retrievals and other low-latency real-time tasks, SSDs are recommended. 23 | -------------------------------------------------------------------------------- /services/router/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "router" 3 | version.workspace = true 4 | edition.workspace = true 5 | authors.workspace = true 6 | description.workspace = true 7 | homepage.workspace = true 8 | repository.workspace = true 9 | readme.workspace = true 10 | keywords.workspace = true 11 | categories.workspace = true 12 | license.workspace = true 13 | rust-version.workspace = true 14 | 15 | [dependencies] 16 | anyhow = { workspace = true } 17 | ctrlc = { workspace = true } 18 | env_logger = { workspace = true } 19 | hashbrown = { workspace = true } 20 | log = { workspace = true } 21 | lru = { workspace = true } 22 | pyo3 = { workspace = true, features = ["auto-initialize"] } 23 | savant_core = { workspace = true } 24 | savant_core_py = { workspace = true } 25 | savant_rs = { workspace = true } 26 | savant_services_common = { path = "../common" } 27 | serde = { workspace = true } 28 | serde_json = { workspace = true } 29 | twelf = { workspace = true } 30 | -------------------------------------------------------------------------------- /docker/Dockerfile.docs: -------------------------------------------------------------------------------- 1 | FROM ghcr.io/insight-platform/py313_rust:v1.3.0 AS builder 2 | 3 | RUN --mount=type=cache,target=/var/cache/apt \ 4 | apt-get update && apt install -y \ 5 | jq \ 6 | libgstreamer1.0-dev \ 7 | libunwind-dev \ 8 | libgstreamer-plugins-base1.0-dev \ 9 | gstreamer1.0-plugins-base gstreamer1.0-plugins-good \ 10 | gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly \ 11 | gstreamer1.0-libav libgstrtspserver-1.0-dev libges-1.0-dev \ 12 | libpython3-dev 13 | 14 | RUN --mount=type=bind,source=.,target=/opt/savant-rs \ 15 | pip install -r /opt/savant-rs/requirements.txt 16 | 17 | # add rust path to PATH 18 | ENV PATH="/root/.cargo/bin:$PATH" 19 | ENV CARGO_TARGET_DIR=/tmp/build 20 | 21 | RUN --mount=type=cache,target=/root/.cargo/registry \ 22 | --mount=type=cache,target=/tmp/build \ 23 | --mount=type=bind,rw,source=.,target=/opt/savant-rs \ 24 | cd /opt/savant-rs && PYTHON_INTERPRETER=python3.13 make docs && cp docs-artifact.tar ../ 25 | -------------------------------------------------------------------------------- /savant_deepstream/deepstream_nvinfer/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod infer_context; 2 | pub mod infer_tensor_meta; 3 | 4 | // Re-export main types for convenience 5 | pub use infer_context::{ 6 | BatchInput, BatchOutput, Context, DataType, InferContextInitParams, InferFormat, 7 | InferNetworkMode, InferTensorOrder, LayerInfo, LogLevel, NetworkInfo, NetworkType, 8 | }; 9 | pub use infer_tensor_meta::{InferDims, InferTensorMeta}; 10 | 11 | /// Error type for DeepStream nvinfer operations 12 | #[derive(Debug, thiserror::Error)] 13 | pub enum NvInferError { 14 | #[error("Invalid operation: {0}")] 15 | InvalidOperation(String), 16 | #[error("Null pointer error in {0}")] 17 | NullPointer(String), 18 | #[error("IO error: {0}")] 19 | Io(#[from] std::io::Error), 20 | #[error("String conversion error: {0}")] 21 | StringConversion(#[from] std::ffi::NulError), 22 | } 23 | 24 | /// Result type for DeepStream nvinfer operations 25 | pub type Result = std::result::Result; 26 | -------------------------------------------------------------------------------- /services/buffer_ng/assets/configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "ingress": { 3 | "socket": { 4 | "url": "${ZMQ_SRC_ENDPOINT}" 5 | } 6 | }, 7 | "egress": { 8 | "socket": { 9 | "url": "${ZMQ_SINK_ENDPOINT}" 10 | } 11 | }, 12 | "common": { 13 | "idle_sleep": { 14 | "secs": 0, 15 | "nanos": 1000 16 | }, 17 | "telemetry": { 18 | "port": 8080, 19 | "stats_log_interval": { 20 | "secs": ${STATS_LOG_INTERVAL:-60}, 21 | "nanos": 0 22 | }, 23 | "metrics_extra_labels": ${METRICS_EXTRA_LABELS:-null} 24 | }, 25 | "buffer": { 26 | "path": "${BUFFER_PATH:-/tmp/buffer}", 27 | "max_length": ${BUFFER_LEN:-1000000}, 28 | "full_threshold_percentage": ${BUFFER_THRESHOLD_PERCENTAGE:-90}, 29 | "reset_on_start": ${BUFFER_RESET_ON_RESTART:-true} 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /docs/source/services/replay/index.rst: -------------------------------------------------------------------------------- 1 | Replay Service Documentation 2 | ============================ 3 | 4 | Replay is an advanced storage providing features required for non-linear computer vision and video analytics: 5 | 6 | - collects video from multiple streams (archiving with TTL eviction); 7 | - provides a REST API for video re-streaming to Savant sinks or modules; 8 | - supports time-synchronized and fast video re-streaming; 9 | - supports configurable video re-streaming stop conditions; 10 | - supports setting minimum and maximum frame duration to increase or decrease the video playback speed; 11 | - can fix incorrect TS in re-streaming video streams; 12 | - can look backward when video stream re-streamed; 13 | - can set additional attributes to retrieved video streams; 14 | - can work as a sidecar or intermediary service in Savant pipelines. 15 | 16 | .. toctree:: 17 | :maxdepth: 2 18 | :caption: Contents 19 | 20 | 0_introduction 21 | 1_platforms 22 | 2_installation 23 | 3_jobs 24 | 4_api -------------------------------------------------------------------------------- /savant_core_py/src/primitives/point.rs: -------------------------------------------------------------------------------- 1 | use pyo3::{pyclass, pymethods, Py, PyAny}; 2 | use savant_core::primitives::rust; 3 | 4 | #[pyclass] 5 | #[derive(Debug, PartialEq, Clone)] 6 | pub struct Point(pub(crate) rust::Point); 7 | 8 | #[pymethods] 9 | impl Point { 10 | #[classattr] 11 | const __hash__: Option> = None; 12 | 13 | fn __repr__(&self) -> String { 14 | format!("{:?}", &self.0) 15 | } 16 | 17 | fn __str__(&self) -> String { 18 | self.__repr__() 19 | } 20 | 21 | #[new] 22 | pub fn new(x: f32, y: f32) -> Self { 23 | Self(rust::Point::new(x, y)) 24 | } 25 | 26 | #[getter] 27 | fn get_x(&self) -> f32 { 28 | self.0.x 29 | } 30 | 31 | #[setter] 32 | fn set_x(&mut self, x: f32) { 33 | self.0.x = x; 34 | } 35 | 36 | #[getter] 37 | fn get_y(&self) -> f32 { 38 | self.0.y 39 | } 40 | 41 | #[setter] 42 | fn set_y(&mut self, y: f32) { 43 | self.0.y = y; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Savant Rust Container", 3 | "features": {}, 4 | "build": { 5 | "dockerfile": "Dockerfile", 6 | "args": { 7 | "REMOTE_USER": "${localEnv:USER}", 8 | "REMOTE_UID": "${localEnv:REMOTE_UID:1000}", 9 | "REMOTE_GID": "${localEnv:REMOTE_GID:1000}" 10 | }, 11 | "target": "devcontainer", 12 | "context": "." 13 | }, 14 | "runArgs": [ 15 | "--gpus", 16 | "all" 17 | ], 18 | "remoteUser": "${localEnv:USER}", 19 | "customizations": { 20 | "vscode": { 21 | "settings": {}, 22 | "extensions": [ 23 | "ms-python.python", 24 | "ms-azuretools.vscode-docker", 25 | "rust-lang.rust-analyzer", 26 | "tamasfe.even-better-toml" 27 | ] 28 | } 29 | }, 30 | "forwardPorts": [ 31 | 3000 32 | ], 33 | "postCreateCommand": "echo 'Dev container ready!'" 34 | } -------------------------------------------------------------------------------- /savant_core/src/transport/zeromq/sync_reader.rs: -------------------------------------------------------------------------------- 1 | use crate::transport::zeromq::reader::ReaderResult; 2 | use crate::transport::zeromq::{NoopResponder, Reader, ReaderConfig, ZmqSocketProvider}; 3 | use std::sync::Arc; 4 | 5 | #[derive(Clone)] 6 | pub struct SyncReader(Arc>); 7 | 8 | impl SyncReader { 9 | pub fn new(config: &ReaderConfig) -> anyhow::Result { 10 | Ok(Self(Arc::new(Reader::new(config)?))) 11 | } 12 | 13 | pub fn receive(&self) -> anyhow::Result { 14 | self.0.receive() 15 | } 16 | 17 | pub fn is_started(&self) -> bool { 18 | self.0.is_alive() 19 | } 20 | 21 | pub fn shutdown(&self) -> anyhow::Result<()> { 22 | self.0.destroy() 23 | } 24 | 25 | pub fn blacklist_source(&self, source_id: &[u8]) { 26 | self.0.blacklist_source(source_id); 27 | } 28 | 29 | pub fn is_blacklisted(&self, source_id: &[u8]) -> bool { 30 | self.0.is_blacklisted(source_id) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /python/etcd.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from savant_rs.match_query import (EtcdCredentials, TlsConfig, 4 | register_env_resolver, 5 | register_etcd_resolver) 6 | from savant_rs.utils import eval_expr 7 | 8 | register_env_resolver() 9 | 10 | # if not set env var RUN_ETCD_TESTS=1, skip 11 | if not eval_expr('env("RUN_ETCD_TESTS", 0)') == 0: 12 | print("Skipping etcd tests") 13 | exit(0) 14 | 15 | # read ca from file to string 16 | ca = Path("../savant_etcd/assets/certs/ca.crt").read_text() 17 | cert = Path("../savant_etcd/assets/certs/client.crt").read_text() 18 | key = Path("../savant_etcd/assets/certs/client.key").read_text() 19 | 20 | conf = TlsConfig( 21 | ca, 22 | cert, 23 | key, 24 | ) 25 | 26 | creds = EtcdCredentials("root", "qwerty") 27 | 28 | register_etcd_resolver( 29 | hosts=["https://127.0.0.1:2379"], credentials=creds, tls_config=conf, watch_path="" 30 | ) 31 | 32 | print(eval_expr('etcd("foo/bar", "default")')) 33 | -------------------------------------------------------------------------------- /savant_core/src/protobuf/serialize/video_frame_batch.rs: -------------------------------------------------------------------------------- 1 | use crate::primitives::frame::VideoFrameProxy; 2 | use crate::primitives::frame_batch::VideoFrameBatch; 3 | use crate::protobuf::serialize; 4 | use savant_protobuf::generated; 5 | 6 | impl From<&VideoFrameBatch> for generated::VideoFrameBatch { 7 | fn from(batch: &VideoFrameBatch) -> Self { 8 | generated::VideoFrameBatch { 9 | batch: batch 10 | .frames() 11 | .iter() 12 | .map(|(id, f)| (*id, generated::VideoFrame::from(f))) 13 | .collect(), 14 | } 15 | } 16 | } 17 | 18 | impl TryFrom<&generated::VideoFrameBatch> for VideoFrameBatch { 19 | type Error = serialize::Error; 20 | 21 | fn try_from(b: &generated::VideoFrameBatch) -> Result { 22 | let mut batch = VideoFrameBatch::new(); 23 | for (id, f) in b.batch.iter() { 24 | batch.add(*id, VideoFrameProxy::try_from(f)?); 25 | } 26 | Ok(batch) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /services/retina_rtsp/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "retina_rtsp" 3 | version.workspace = true 4 | edition.workspace = true 5 | authors.workspace = true 6 | description.workspace = true 7 | homepage.workspace = true 8 | repository.workspace = true 9 | readme.workspace = true 10 | keywords.workspace = true 11 | categories.workspace = true 12 | license.workspace = true 13 | rust-version.workspace = true 14 | 15 | [dependencies] 16 | rand = { workspace = true } 17 | anyhow = { workspace = true } 18 | cros-codecs = { workspace = true } 19 | derive_builder = { workspace = true } 20 | env_logger = { workspace = true } 21 | hashbrown = { workspace = true } 22 | log = { workspace = true } 23 | mini-moka = { workspace = true } 24 | parking_lot = { workspace = true } 25 | retina = { workspace = true } 26 | savant_core = { workspace = true } 27 | savant_services_common = { workspace = true } 28 | serde = { workspace = true } 29 | tokio = { workspace = true } 30 | twelf = { workspace = true } 31 | url = { workspace = true } 32 | futures = { workspace = true } 33 | 34 | -------------------------------------------------------------------------------- /savant_core_py/src/primitives/eos.rs: -------------------------------------------------------------------------------- 1 | use crate::primitives::message::Message; 2 | use pyo3::{pyclass, pymethods, Py, PyAny}; 3 | use savant_core::primitives::rust; 4 | 5 | #[pyclass] 6 | #[derive(Debug, Clone)] 7 | pub struct EndOfStream(pub(crate) rust::EndOfStream); 8 | 9 | #[pymethods] 10 | impl EndOfStream { 11 | #[classattr] 12 | const __hash__: Option> = None; 13 | 14 | fn __repr__(&self) -> String { 15 | format!("{:?}", &self.0) 16 | } 17 | 18 | fn __str__(&self) -> String { 19 | self.__repr__() 20 | } 21 | 22 | #[new] 23 | pub fn new(source_id: String) -> Self { 24 | Self(rust::EndOfStream { source_id }) 25 | } 26 | 27 | #[getter] 28 | pub fn get_source_id(&self) -> String { 29 | self.0.source_id.clone() 30 | } 31 | 32 | #[getter] 33 | pub fn get_json(&self) -> String { 34 | serde_json::json!(&self.0).to_string() 35 | } 36 | 37 | pub fn to_message(&self) -> Message { 38 | Message::end_of_stream(self.clone()) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /docker/services/Dockerfile.retina_rtsp: -------------------------------------------------------------------------------- 1 | FROM rust:1.91 AS builder 2 | 3 | WORKDIR /opt/retina_rtsp 4 | 5 | RUN rustup component add rustfmt 6 | RUN --mount=type=bind,source=.,target=/opt/savant-rs bash /opt/savant-rs/utils/services/docker-deps.sh 7 | RUN --mount=type=bind,source=.,target=/opt/savant-rs bash /opt/savant-rs/utils/services/protoc.sh 8 | RUN --mount=type=cache,target=/tmp/build --mount=type=bind,rw,source=.,target=/opt/savant-rs cd /opt/savant-rs && CARGO_TARGET_DIR=/tmp/build cargo build --release -p retina_rtsp -p savant_info 9 | RUN --mount=type=cache,target=/tmp/build --mount=type=bind,source=.,target=/opt/savant-rs bash /opt/savant-rs/utils/services/retina_rtsp/copy-deps.sh 10 | 11 | FROM ubuntu:24.04 AS runner 12 | 13 | RUN --mount=type=bind,source=.,target=/opt/savant-rs bash /opt/savant-rs/utils/services/docker-deps.sh 14 | 15 | COPY --from=builder /opt /opt 16 | 17 | WORKDIR /opt/retina_rtsp 18 | 19 | ENV LD_LIBRARY_PATH=/opt/libs 20 | ENV RUST_LOG=info 21 | 22 | ENTRYPOINT ["/opt/bin/retina_rtsp"] 23 | CMD ["/opt/etc/configuration.json"] 24 | -------------------------------------------------------------------------------- /python/primitives/vector_view_ops.py: -------------------------------------------------------------------------------- 1 | from savant_rs.match_query import IntExpression as IE 2 | from savant_rs.match_query import MatchQuery as Q 3 | from savant_rs.match_query import QueryFunctions as QF 4 | from savant_rs.utils import gen_frame 5 | 6 | f = gen_frame() 7 | 8 | objects_x = QF.filter(f.access_objects(Q.idle()), Q.eval("id % 2 == 1")).sorted_by_id 9 | 10 | objects = QF.filter(f.access_objects(Q.idle()), Q.id(IE.one_of(1, 2))).sorted_by_id 11 | 12 | ids = objects.ids 13 | print("Ids:", ids) 14 | track_ids = objects.track_ids 15 | print("Track ids:", track_ids) 16 | 17 | # boxes = objects.rotated_boxes_as_numpy(VideoObjectBBoxType.Detection) 18 | # print("Detections:", boxes) 19 | # 20 | # tr_boxes = objects.rotated_boxes_as_numpy(VideoObjectBBoxType.TrackingInfo) 21 | # print("Tracking:", tr_boxes) 22 | 23 | # objects.update_from_numpy_boxes(boxes, BBoxFormat.XcYcWidthHeight, VideoObjectBBoxType.Detection) 24 | # objects.update_from_numpy_rotated_boxes(boxes, VideoObjectBBoxType.Detection) 25 | 26 | # tracking_boxes = objects.tracking_boxes_as_numpy() 27 | -------------------------------------------------------------------------------- /python/primitives/buf_copy.py: -------------------------------------------------------------------------------- 1 | from timeit import default_timer as timer 2 | 3 | from savant_rs.primitives import Attribute, AttributeValue, VideoObject 4 | from savant_rs.primitives.geometry import RBBox 5 | 6 | o = VideoObject( 7 | id=1, 8 | namespace="some", 9 | label="person", 10 | detection_box=RBBox(0.1, 0.2, 0.3, 0.4, None), 11 | confidence=0.5, 12 | attributes=[], 13 | track_id=None, 14 | track_box=None, 15 | ) 16 | 17 | t = timer() 18 | 19 | bts = bytes(256) 20 | 21 | a = Attribute( 22 | namespace="other", 23 | name="attr", 24 | values=[ 25 | # Value.bytes(dims=[8, 3, 8, 8], blob=bts, confidence=None), 26 | AttributeValue.integer(1, confidence=0.5), 27 | AttributeValue.integer(2, confidence=0.5), 28 | AttributeValue.integer(3, confidence=0.5), 29 | AttributeValue.integer(4, confidence=0.5), 30 | ], 31 | ) 32 | 33 | for _ in range(1_000): 34 | o.set_attribute(a) 35 | a = o.get_attribute(namespace="other", name="attr") 36 | # x = a.name 37 | 38 | print(timer() - t) 39 | -------------------------------------------------------------------------------- /savant_core_py/src/primitives/bbox/utils.rs: -------------------------------------------------------------------------------- 1 | use crate::detach; 2 | use crate::primitives::bbox::{BBoxMetricType, RBBox}; 3 | use pyo3::prelude::*; 4 | use std::collections::HashMap; 5 | 6 | #[pyfunction] 7 | pub fn solely_owned_areas(bboxes: Vec, parallel: bool) -> Vec { 8 | let boxes = bboxes.iter().map(|b| &b.0).collect::>(); 9 | detach!(true, || { 10 | savant_core::primitives::bbox::utils::solely_owned_areas(&boxes, parallel) 11 | }) 12 | } 13 | 14 | #[pyfunction] 15 | pub fn associate_bboxes( 16 | candidates: Vec, 17 | owners: Vec, 18 | metric: BBoxMetricType, 19 | threshold: f32, 20 | ) -> HashMap> { 21 | let candidates = candidates.iter().map(|b| &b.0).collect::>(); 22 | let owners = owners.iter().map(|b| &b.0).collect::>(); 23 | detach!(true, || { 24 | savant_core::primitives::bbox::utils::associate_bboxes( 25 | &candidates, 26 | &owners, 27 | metric.into(), 28 | threshold, 29 | ) 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /savant_core/src/pipeline/stage_function_loader.rs: -------------------------------------------------------------------------------- 1 | use crate::pipeline::{PipelineStageFunction, PluginParams}; 2 | use hashbrown::HashMap; 3 | use lazy_static::lazy_static; 4 | use parking_lot::Mutex; 5 | 6 | lazy_static! { 7 | static ref LIBRARIES: Mutex> = Mutex::new(HashMap::new()); 8 | } 9 | 10 | pub fn load_stage_function_plugin( 11 | libname: &str, 12 | init_name: &str, 13 | plugin_name: &str, 14 | params: PluginParams, 15 | ) -> anyhow::Result> { 16 | let mut libs = LIBRARIES.lock(); 17 | if !libs.contains_key(libname) { 18 | let lib = unsafe { libloading::Library::new(libname)? }; 19 | libs.insert(libname.to_string(), lib); 20 | } 21 | let lib = libs 22 | .get(libname) 23 | .expect("Library must be available according to the code logic"); 24 | let init: libloading::Symbol = 25 | unsafe { lib.get(init_name.as_bytes())? }; 26 | let raw = init(plugin_name, params); 27 | Ok(unsafe { Box::from_raw(raw) }) 28 | } 29 | -------------------------------------------------------------------------------- /savant_core_py/src/primitives/shutdown.rs: -------------------------------------------------------------------------------- 1 | use crate::primitives::message::Message; 2 | use pyo3::{pyclass, pymethods, Py, PyAny}; 3 | use savant_core::json_api::ToSerdeJsonValue; 4 | use savant_core::primitives::rust; 5 | 6 | #[pyclass] 7 | #[derive(Debug, Clone)] 8 | pub struct Shutdown(pub(crate) rust::Shutdown); 9 | 10 | #[pymethods] 11 | impl Shutdown { 12 | #[classattr] 13 | const __hash__: Option> = None; 14 | 15 | fn __repr__(&self) -> String { 16 | format!("{:?}", &self.0) 17 | } 18 | 19 | fn __str__(&self) -> String { 20 | self.__repr__() 21 | } 22 | 23 | #[new] 24 | pub fn new(auth: &str) -> Self { 25 | Self(rust::Shutdown::new(auth)) 26 | } 27 | 28 | #[getter] 29 | pub fn get_auth(&self) -> String { 30 | self.0.get_auth().to_string() 31 | } 32 | 33 | #[getter] 34 | pub fn get_json(&self) -> String { 35 | serde_json::to_string(&self.0.to_serde_json_value()).unwrap() 36 | } 37 | 38 | pub fn to_message(&self) -> Message { 39 | Message::shutdown(self.clone()) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /services/retina_rtsp/assets/configuration_mediamtx.json: -------------------------------------------------------------------------------- 1 | { 2 | "sink": { 3 | "url": "pub+connect:tcp://127.0.0.1:3333" 4 | }, 5 | "rtsp_sources": { 6 | "group0": { 7 | "sources": [ 8 | { 9 | "source_id": "left", 10 | "url": "rtsp://127.0.0.1:554/stream/bullet_left" 11 | }, 12 | { 13 | "source_id": "right", 14 | "url": "rtsp://127.0.0.1:554/stream/bullet_right" 15 | } 16 | ], 17 | "rtcp_sr_sync": { 18 | "group_window_duration": { 19 | "secs": 5, 20 | "nanos": 0 21 | }, 22 | "batch_duration": { 23 | "secs": 0, 24 | "nanos": 100000000 25 | }, 26 | "network_skew_correction": false, 27 | "rtcp_once": false 28 | } 29 | } 30 | }, 31 | "reconnect_interval": { 32 | "secs": 5, 33 | "nanos": 0 34 | } 35 | } -------------------------------------------------------------------------------- /savant_core/src/pipeline/stage_plugin_sample.rs: -------------------------------------------------------------------------------- 1 | use crate::pipeline::stage::PipelineStage; 2 | use crate::pipeline::{ 3 | Pipeline, PipelinePayload, PipelineStageFunction, PipelineStageFunctionOrder, PluginParams, 4 | }; 5 | 6 | #[no_mangle] 7 | pub fn init_plugin_test(_: &str, params: PluginParams) -> *mut dyn PipelineStageFunction { 8 | let plugin = Plugin { 9 | pipeline: None, 10 | params, 11 | }; 12 | Box::into_raw(Box::new(plugin)) 13 | } 14 | 15 | pub struct Plugin { 16 | pipeline: Option, 17 | params: PluginParams, 18 | } 19 | 20 | impl PipelineStageFunction for Plugin { 21 | fn set_pipeline(&mut self, pipeline: Pipeline) { 22 | self.pipeline = Some(pipeline); 23 | } 24 | fn get_pipeline(&self) -> &Option { 25 | &self.pipeline 26 | } 27 | fn call( 28 | &self, 29 | id: i64, 30 | _: &PipelineStage, 31 | _: PipelineStageFunctionOrder, 32 | payload: &mut PipelinePayload, 33 | ) -> anyhow::Result<()> { 34 | dbg!(id, payload, &self.params); 35 | Ok(()) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /services/replay/replaydb/assets/rocksdb_opt_out.json: -------------------------------------------------------------------------------- 1 | { 2 | "common": { 3 | "pass_metadata_only": false, 4 | "management_port": 8080, 5 | "stats_period": { 6 | "secs": 60, 7 | "nanos": 0 8 | }, 9 | "job_writer_cache_max_capacity": 1000, 10 | "job_writer_cache_ttl": { 11 | "secs": 60, 12 | "nanos": 0 13 | }, 14 | "job_eviction_ttl": { 15 | "secs": 60, 16 | "nanos": 0 17 | } 18 | }, 19 | "in_stream": { 20 | "url": "router+bind:ipc:///tmp/${SOCKET_PATH_IN:-undefined}", 21 | "options": { 22 | "receive_timeout": { 23 | "secs": 1, 24 | "nanos": 0 25 | }, 26 | "receive_hwm": 1000, 27 | "topic_prefix_spec": { 28 | "source_id": "source_id" 29 | }, 30 | "source_cache_size": 1000, 31 | "fix_ipc_permissions": 511, 32 | "inflight_ops": 100 33 | } 34 | }, 35 | "out_stream": null, 36 | "storage": { 37 | "rocksdb": { 38 | "path": "${DB_PATH:-/tmp/rocksdb}", 39 | "data_expiration_ttl": { 40 | "secs": 3600, 41 | "nanos": 0 42 | } 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /savant_core/src/deadlock_detection.rs: -------------------------------------------------------------------------------- 1 | pub fn enable_dl_detection() { 2 | // only for #[cfg] 3 | use parking_lot::deadlock; 4 | use std::thread; 5 | use std::time::Duration; 6 | 7 | // Create a background thread which checks for deadlocks every 10s 8 | thread::spawn(move || loop { 9 | thread::sleep(Duration::from_secs(5)); 10 | log::trace!(target: "parking_lot::deadlock_detector", "Checking for deadlocks"); 11 | let deadlocks = deadlock::check_deadlock(); 12 | if deadlocks.is_empty() { 13 | continue; 14 | } 15 | 16 | log::error!(target: "parking_lot::deadlock_detector", "{} deadlocks detected", deadlocks.len()); 17 | 18 | for (i, threads) in deadlocks.iter().enumerate() { 19 | log::error!(target: "parking_lot::deadlock_detector", "Deadlock #{i}"); 20 | for t in threads { 21 | log::error!(target: "parking_lot::deadlock_detector", "Thread Id {:#?}", t.thread_id()); 22 | log::error!(target: "parking_lot::deadlock_detector", "{:#?}", t.backtrace()); 23 | } 24 | } 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /savant_core/src/primitives/segment.rs: -------------------------------------------------------------------------------- 1 | use crate::primitives::point::Point; 2 | 3 | #[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize)] 4 | pub struct Segment { 5 | pub begin: Point, 6 | pub end: Point, 7 | } 8 | 9 | impl Segment { 10 | pub fn new(begin: Point, end: Point) -> Self { 11 | Self { begin, end } 12 | } 13 | } 14 | 15 | #[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize)] 16 | pub enum IntersectionKind { 17 | Enter, 18 | Inside, 19 | Leave, 20 | Cross, 21 | Outside, 22 | } 23 | 24 | #[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize)] 25 | pub struct Intersection { 26 | pub kind: IntersectionKind, 27 | pub edges: Vec<(usize, Option)>, 28 | } 29 | 30 | impl Intersection { 31 | pub fn new(kind: IntersectionKind, edges: Vec<(usize, Option)>) -> Self { 32 | Self { kind, edges } 33 | } 34 | 35 | pub fn get_kind(&self) -> IntersectionKind { 36 | self.kind.clone() 37 | } 38 | 39 | pub fn get_edges(&self) -> Vec<(usize, Option)> { 40 | self.edges.clone() 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /savant_gstreamer_elements/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "savant_gstreamer_elements" 3 | version.workspace = true 4 | edition.workspace = true 5 | authors.workspace = true 6 | description.workspace = true 7 | homepage.workspace = true 8 | repository.workspace = true 9 | readme.workspace = true 10 | keywords.workspace = true 11 | categories.workspace = true 12 | license.workspace = true 13 | rust-version.workspace = true 14 | 15 | [lib] 16 | crate-type = ["dylib"] 17 | 18 | [dependencies] 19 | anyhow = { workspace = true } 20 | base64 = { workspace = true } 21 | hashbrown = { workspace = true } 22 | pyo3 = { workspace = true } 23 | savant_gstreamer = { workspace = true } 24 | savant_core = { workspace = true } 25 | savant_core_py = { workspace = true } 26 | gstreamer = { workspace = true } 27 | gstreamer-base = { workspace = true } 28 | gstreamer-video = { workspace = true } 29 | gstreamer-audio = { workspace = true } 30 | lazy_static = { workspace = true } 31 | log = { workspace = true } 32 | parking_lot = { workspace = true } 33 | 34 | [build-dependencies] 35 | pyo3-build-config = { workspace = true } 36 | gst-plugin-version-helper = { workspace = true } 37 | 38 | 39 | -------------------------------------------------------------------------------- /services/replay/replaydb/src/service.rs: -------------------------------------------------------------------------------- 1 | use std::future::Future; 2 | 3 | use uuid::Uuid; 4 | 5 | use crate::job::configuration::JobConfiguration; 6 | use crate::job::query::JobQuery; 7 | use crate::job::stop_condition::JobStopCondition; 8 | 9 | pub mod configuration; 10 | pub mod rocksdb_service; 11 | 12 | pub trait JobManager { 13 | fn add_job(&mut self, job: JobQuery) -> impl Future> + Send; 14 | fn stop_job(&mut self, job_id: Uuid) -> impl Future> + Send; 15 | fn update_stop_condition( 16 | &mut self, 17 | job_id: Uuid, 18 | stop_condition: JobStopCondition, 19 | ) -> anyhow::Result<()>; 20 | fn list_jobs(&self) -> Vec<(Uuid, JobConfiguration, JobStopCondition)>; 21 | fn list_stopped_jobs(&self) -> Vec<(Uuid, JobConfiguration, Option)>; 22 | fn check_stream_processor_finished( 23 | &mut self, 24 | ) -> impl Future> + Send; 25 | fn shutdown(&mut self) -> impl Future> + Send; 26 | fn clean_stopped_jobs(&mut self) -> impl Future> + Send; 27 | } 28 | -------------------------------------------------------------------------------- /services/replay/replaydb/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "replaydb" 3 | description = "Replay Service" 4 | license = "../LICENSE" 5 | readme = "../README.md" 6 | 7 | version.workspace = true 8 | edition.workspace = true 9 | authors.workspace = true 10 | homepage.workspace = true 11 | repository.workspace = true 12 | keywords.workspace = true 13 | categories.workspace = true 14 | rust-version.workspace = true 15 | 16 | [dependencies] 17 | 18 | anyhow = { workspace = true } 19 | bincode = { workspace = true } 20 | derive_builder = { workspace = true } 21 | env_logger = { workspace = true } 22 | hashbrown = { workspace = true } 23 | log = { workspace = true } 24 | md-5 = { workspace = true } 25 | mini-moka = { workspace = true } 26 | uuid = { workspace = true } 27 | parking_lot = { workspace = true } 28 | rocksdb = { workspace = true } 29 | savant_core = { workspace = true } 30 | savant_services_common = { workspace = true } 31 | serde = { workspace = true } 32 | serde_json = { workspace = true } 33 | tokio = { workspace = true } 34 | tokio-timerfd = { workspace = true } 35 | twelf = { workspace = true } 36 | 37 | [dev-dependencies] 38 | tempfile = { workspace = true } 39 | 40 | -------------------------------------------------------------------------------- /services/retina_rtsp/assets/configuration_no_ntp.json: -------------------------------------------------------------------------------- 1 | { 2 | "sink": { 3 | "url": "pub+connect:tcp://127.0.0.1:6666", 4 | "options": { 5 | "send_timeout": { 6 | "secs": 1, 7 | "nanos": 0 8 | }, 9 | "send_retries": 3, 10 | "receive_timeout": { 11 | "secs": 1, 12 | "nanos": 0 13 | }, 14 | "receive_retries": 3, 15 | "send_hwm": 1000, 16 | "receive_hwm": 1000, 17 | "inflight_ops": 100 18 | } 19 | }, 20 | "rtsp_sources": { 21 | "group0": { 22 | "sources": [ 23 | { 24 | "source_id": "city-traffic", 25 | "url": "rtsp://hello.savant.video:8554/stream/city-traffic" 26 | }, 27 | { 28 | "source_id": "town-centre", 29 | "url": "rtsp://hello.savant.video:8554/stream/town-centre" 30 | } 31 | ] 32 | } 33 | }, 34 | "reconnect_interval": { 35 | "secs": 5, 36 | "nanos": 0 37 | } 38 | } -------------------------------------------------------------------------------- /savant_etcd/utils/run_etcd_with_tls.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ROOT_PASSWD=$1 4 | 5 | if [ -z $ROOT_PASSWD ]; then 6 | docker run -it --rm \ 7 | -p 2379:2379 \ 8 | -e ALLOW_NONE_AUTHENTICATION=yes \ 9 | -e ETCD_TRUSTED_CA_FILE=/etc/etcd-ssl/ca.crt \ 10 | -e ETCD_CERT_FILE=/etc/etcd-ssl/server.crt \ 11 | -e ETCD_KEY_FILE=/etc/etcd-ssl/server.key \ 12 | -e ETCD_LISTEN_CLIENT_URLS=https://0.0.0.0:2379 \ 13 | -e ETCD_ADVERTISE_CLIENT_URLS=https://0.0.0.0:2379 \ 14 | -v $(pwd)/../assets/certs:/etc/etcd-ssl \ 15 | --name remote-etcd \ 16 | bitnamilegacy/etcd:3.6.4-debian-12-r4 17 | else 18 | docker run -it --rm \ 19 | -p 2379:2379 \ 20 | -e ALLOW_NONE_AUTHENTICATION=no \ 21 | -e ETCD_ROOT_PASSWORD=$ROOT_PASSWD \ 22 | -e ETCD_TRUSTED_CA_FILE=/etc/etcd-ssl/ca.crt \ 23 | -e ETCD_CERT_FILE=/etc/etcd-ssl/server.crt \ 24 | -e ETCD_KEY_FILE=/etc/etcd-ssl/server.key \ 25 | -e ETCD_LISTEN_CLIENT_URLS=https://0.0.0.0:2379 \ 26 | -e ETCD_ADVERTISE_CLIENT_URLS=https://0.0.0.0:2379 \ 27 | -v $(pwd)/../assets/certs:/etc/etcd-ssl \ 28 | --name remote-etcd \ 29 | bitnamilegacy/etcd:3.6.4-debian-12-r4 30 | fi 31 | 32 | -------------------------------------------------------------------------------- /services/replay/replay/src/web_service/find_keyframes.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{post, web, Responder}; 2 | use log::debug; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | use crate::web_service::{JobService, ResponseMessage}; 6 | 7 | #[derive(Debug, Serialize, Deserialize)] 8 | struct FindKeyframesQuery { 9 | source_id: String, 10 | from: Option, 11 | to: Option, 12 | limit: usize, 13 | } 14 | 15 | #[post("/keyframes/find")] 16 | async fn find_keyframes( 17 | js: web::Data, 18 | query: web::Json, 19 | ) -> impl Responder { 20 | let mut js_bind = js.service.lock().await; 21 | let uuids_res = js_bind 22 | .find_keyframes(&query.source_id, query.from, query.to, query.limit) 23 | .await; 24 | debug!( 25 | "Received Keyframe Lookup Query: {}", 26 | serde_json::to_string(&query).unwrap() 27 | ); 28 | match uuids_res { 29 | Ok(uuids) => ResponseMessage::FindKeyframes( 30 | query.source_id.clone(), 31 | uuids.into_iter().map(String::from).collect(), 32 | ), 33 | Err(e) => ResponseMessage::Error(e.to_string()), 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /docker/services/Dockerfile.replay: -------------------------------------------------------------------------------- 1 | # do not upgrade to 1.89, it breaks the build because of Glibc 2.38 absence in the runner image 2 | FROM rust:1.91 AS builder 3 | 4 | WORKDIR /opt/replay 5 | 6 | RUN rustup component add rustfmt 7 | RUN --mount=type=bind,source=.,target=/opt/savant-rs bash /opt/savant-rs/utils/services/docker-deps.sh 8 | RUN --mount=type=bind,source=.,target=/opt/savant-rs bash /opt/savant-rs/utils/services/protoc.sh 9 | RUN --mount=type=cache,target=/tmp/build --mount=type=bind,rw,source=.,target=/opt/savant-rs cd /opt/savant-rs && CARGO_TARGET_DIR=/tmp/build cargo build --release -p replay -p savant_info 10 | RUN --mount=type=cache,target=/tmp/build --mount=type=bind,source=.,target=/opt/savant-rs bash /opt/savant-rs/utils/services/replay/copy-deps.sh 11 | 12 | FROM ubuntu:24.04 AS runner 13 | 14 | RUN --mount=type=bind,source=.,target=/opt/savant-rs bash /opt/savant-rs/utils/services/docker-deps.sh 15 | 16 | COPY --from=builder /opt /opt 17 | 18 | WORKDIR /opt/replay 19 | 20 | ENV LD_LIBRARY_PATH=/opt/libs 21 | ENV DB_PATH=/opt/rocksdb 22 | ENV RUST_LOG=info 23 | 24 | EXPOSE 8080 25 | EXPOSE 5555 26 | EXPOSE 5556 27 | 28 | ENTRYPOINT ["/opt/bin/replay"] 29 | CMD ["/opt/etc/config.json"] 30 | -------------------------------------------------------------------------------- /savant_python/python/savant_rs/py/log/logger_mixin.py: -------------------------------------------------------------------------------- 1 | """LoggerMixin module.""" 2 | 3 | import logging 4 | 5 | from .log_setup import get_logger, init_logging 6 | 7 | 8 | class LoggerMixin: 9 | """Mixes logger in GStreamer element. 10 | 11 | When the element name is available, logger name changes to 12 | `module_name/element_name`. Otherwise, logger name is `module_name`. 13 | 14 | Note: we cannot override `do_set_state` or any other method where element name 15 | becomes available since base classes are bindings. 16 | """ 17 | 18 | _logger: logging.Logger = None 19 | _logger_initialized: bool = False 20 | 21 | def __init__(self): 22 | self._init_logger() 23 | 24 | @property 25 | def logger(self): 26 | """Logger.""" 27 | if not self._logger_initialized: 28 | self._init_logger() 29 | return self._logger 30 | 31 | def _init_logger(self): 32 | logger_name = self.__module__ 33 | if hasattr(self, 'get_name') and self.get_name(): 34 | logger_name += f'.{self.get_name()}' 35 | 36 | init_logging() 37 | self._logger = get_logger(logger_name) 38 | 39 | self._logger_initialized = True 40 | -------------------------------------------------------------------------------- /docker/services/Dockerfile.router: -------------------------------------------------------------------------------- 1 | FROM ghcr.io/insight-platform/py314_rust:v1.3.0 AS builder 2 | 3 | ENV PATH="/root/.cargo/bin:$PATH" 4 | ENV CARGO_TARGET_DIR=/tmp/build 5 | 6 | RUN --mount=type=cache,target=/var/cache/apt \ 7 | apt-get update && apt install -y \ 8 | libunwind-dev \ 9 | libpython3-dev 10 | 11 | WORKDIR /opt/router 12 | 13 | RUN rustup component add rustfmt 14 | RUN --mount=type=bind,source=.,target=/opt/savant-rs bash /opt/savant-rs/utils/services/docker-deps.sh 15 | RUN --mount=type=cache,target=/tmp/build --mount=type=bind,rw,source=.,target=/opt/savant-rs cd /opt/savant-rs && CARGO_TARGET_DIR=/tmp/build cargo build --release -p router -p savant_info 16 | RUN --mount=type=cache,target=/tmp/build --mount=type=bind,source=.,target=/opt/savant-rs bash /opt/savant-rs/utils/services/router/copy-deps.sh 17 | 18 | FROM python:3.14 AS runner 19 | 20 | RUN --mount=type=bind,source=.,target=/opt/savant-rs bash /opt/savant-rs/utils/services/docker-deps.sh 21 | 22 | COPY --from=builder /opt /opt 23 | 24 | WORKDIR /opt/router 25 | 26 | ENV LD_LIBRARY_PATH=/opt/libs 27 | ENV LOGLEVEL=info 28 | ENV PYTHON_MODULE_ROOT=/opt/python 29 | 30 | ENTRYPOINT ["/opt/bin/router"] 31 | CMD ["/opt/etc/configuration.json"] 32 | -------------------------------------------------------------------------------- /services/buffer_ng/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "buffer_ng" 3 | version.workspace = true 4 | edition.workspace = true 5 | authors.workspace = true 6 | description.workspace = true 7 | homepage.workspace = true 8 | repository.workspace = true 9 | readme.workspace = true 10 | keywords.workspace = true 11 | categories.workspace = true 12 | license.workspace = true 13 | rust-version.workspace = true 14 | 15 | [dependencies] 16 | 17 | anyhow = { workspace = true } 18 | bitcode = { workspace = true, features=["serde"] } 19 | chrono = { workspace = true } 20 | crossbeam = { workspace = true } 21 | ctrlc = { workspace = true } 22 | derive_builder = { workspace = true } 23 | env_logger = { workspace = true } 24 | hashbrown = { workspace = true } 25 | log = { workspace = true } 26 | parking_lot = { workspace = true } 27 | pyo3 = { workspace = true, features = ["auto-initialize"] } 28 | rocksdb = { workspace = true } 29 | savant_core = { workspace = true } 30 | savant_core_py = { workspace = true } 31 | savant_rs = { workspace = true } 32 | savant_services_common = { path = "../common" } 33 | serde = { workspace = true } 34 | serde_json = { workspace = true } 35 | twelf = { workspace = true } 36 | 37 | [dev-dependencies] 38 | tempfile = { workspace = true } 39 | -------------------------------------------------------------------------------- /services/replay/replay/src/web_service/del_job.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{delete, web, Responder}; 2 | use log::{info, warn}; 3 | use uuid::Uuid; 4 | 5 | use replaydb::service::JobManager; 6 | 7 | use crate::web_service::{JobService, ResponseMessage}; 8 | 9 | #[delete("/job/{id}")] 10 | async fn delete_job(js: web::Data, q: web::Path) -> impl Responder { 11 | let query_uuid_str = q.into_inner(); 12 | let job_uuid = Uuid::try_from(query_uuid_str.as_str()); 13 | if let Err(e) = job_uuid { 14 | let message = format!("Invalid job UUID: {}, error: {}", query_uuid_str, e); 15 | return ResponseMessage::Error(message); 16 | } 17 | let job_uuid = job_uuid.unwrap(); 18 | info!("Deleting job: {}", &job_uuid); 19 | let mut js_bind = js.service.lock().await; 20 | 21 | let cleanup = js_bind.clean_stopped_jobs().await; 22 | if let Err(e) = cleanup { 23 | return ResponseMessage::Error(e.to_string()); 24 | } 25 | 26 | let res = js_bind.stop_job(job_uuid).await; 27 | match res { 28 | Ok(_) => ResponseMessage::Ok, 29 | Err(e) => { 30 | warn!("Error stopping job: {}", &e); 31 | ResponseMessage::Error(e.to_string()) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /savant_core/src/transport/zeromq/sync_writer.rs: -------------------------------------------------------------------------------- 1 | use crate::transport::zeromq::{ 2 | NoopResponder, Writer, WriterConfig, WriterResult, ZmqSocketProvider, 3 | }; 4 | use parking_lot::Mutex; 5 | use std::sync::Arc; 6 | 7 | #[derive(Clone)] 8 | pub struct SyncWriter(Arc>>); 9 | 10 | impl SyncWriter { 11 | pub fn new(config: &WriterConfig) -> anyhow::Result { 12 | Ok(Self(Arc::new(Mutex::new(Writer::new(config)?)))) 13 | } 14 | 15 | pub fn send_eos(&self, topic: &str) -> anyhow::Result { 16 | let mut writer = self.0.lock(); 17 | writer.send_eos(topic) 18 | } 19 | 20 | pub fn send_message( 21 | &self, 22 | topic: &str, 23 | message: &crate::message::Message, 24 | data: &[&[u8]], 25 | ) -> anyhow::Result { 26 | let mut writer = self.0.lock(); 27 | writer.send_message(topic, message, data) 28 | } 29 | 30 | pub fn is_started(&self) -> bool { 31 | let writer = self.0.lock(); 32 | writer.is_started() 33 | } 34 | 35 | pub fn shutdown(&self) -> anyhow::Result<()> { 36 | let mut writer = self.0.lock(); 37 | writer.destroy() 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /python/primitives/video_frame_update.py: -------------------------------------------------------------------------------- 1 | from savant_rs.match_query import MatchQuery as Q 2 | from savant_rs.primitives import (AttributeUpdatePolicy, ObjectUpdatePolicy, 3 | VideoFrameUpdate) 4 | from savant_rs.utils import gen_frame 5 | from savant_rs.utils.serialization import Message, load_message, save_message 6 | 7 | frame = gen_frame() 8 | update = VideoFrameUpdate() 9 | 10 | update.object_policy = ObjectUpdatePolicy.AddForeignObjects 11 | update.frame_attribute_policy = AttributeUpdatePolicy.ReplaceWithForeignWhenDuplicate 12 | update.object_attribute_policy = AttributeUpdatePolicy.ReplaceWithForeignWhenDuplicate 13 | 14 | objects = frame.access_objects(Q.idle()) 15 | 16 | for o in objects: 17 | update.add_object(o.detached_copy(), None) 18 | 19 | attributes = frame.attributes 20 | 21 | for namespace, label in attributes: 22 | attr = frame.get_attribute(namespace, label) 23 | update.add_frame_attribute(attr) 24 | 25 | print(update.json) 26 | print(update.json_pretty) 27 | 28 | pb = update.to_protobuf() 29 | restored = VideoFrameUpdate.from_protobuf(pb) 30 | assert update.json == restored.json 31 | 32 | m = Message.video_frame_update(update) 33 | binary = save_message(m) 34 | m2 = load_message(binary) 35 | -------------------------------------------------------------------------------- /savant_core_py/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "savant_core_py" 3 | version.workspace = true 4 | edition.workspace = true 5 | authors.workspace = true 6 | description.workspace = true 7 | homepage.workspace = true 8 | repository.workspace = true 9 | readme.workspace = true 10 | keywords.workspace = true 11 | categories.workspace = true 12 | license.workspace = true 13 | rust-version.workspace = true 14 | 15 | [lib] 16 | crate-type = ["dylib"] 17 | 18 | [dependencies] 19 | anyhow = { workspace = true } 20 | colored = { workspace = true } 21 | evalexpr = { workspace = true } 22 | hashbrown = { workspace = true } 23 | geo = { workspace = true } 24 | lazy_static = { workspace = true } 25 | log = { workspace = true } 26 | num-bigint = { workspace = true } 27 | num-traits = { workspace = true } 28 | opentelemetry = { workspace = true } 29 | parking_lot = { workspace = true } 30 | pyo3 = { workspace = true, features = ["num-bigint"] } 31 | savant_core = { workspace = true } 32 | serde = { workspace = true } 33 | serde_json = { workspace = true } 34 | prometheus-client = { workspace = true } 35 | tokio = { workspace = true } 36 | uuid = { workspace = true } 37 | 38 | [build-dependencies] 39 | pyo3-build-config = { workspace = true } 40 | cbindgen = { workspace = true } 41 | 42 | -------------------------------------------------------------------------------- /savant_core/src/macros.rs: -------------------------------------------------------------------------------- 1 | #[macro_export] 2 | macro_rules! function { 3 | () => {{ 4 | fn f() {} 5 | fn type_name_of(_: T) -> &'static str { 6 | std::any::type_name::() 7 | } 8 | let name = type_name_of(f); 9 | 10 | // Find and cut the rest of the path 11 | match &name[..name.len() - 3].rfind(':') { 12 | Some(pos) => &name[pos + 1..name.len() - 3], 13 | None => &name[..name.len() - 3], 14 | } 15 | }}; 16 | } 17 | 18 | #[macro_export] 19 | macro_rules! trace { 20 | ($expression:expr) => {{ 21 | let thread_id = std::thread::current().id(); 22 | log::trace!( 23 | target: "savant::trace::before", 24 | "[{:?}] Trace line ({}, {}, {})", 25 | thread_id, 26 | $crate::function!(), 27 | file!(), 28 | line!() 29 | ); 30 | let result = $expression; 31 | log::trace!( 32 | target: "savant::trace::after", 33 | "[{:?}] Trace line ({}, {}, {})", 34 | thread_id, 35 | $crate::function!(), 36 | file!(), 37 | line!() 38 | ); 39 | result 40 | }}; 41 | } 42 | -------------------------------------------------------------------------------- /services/replay/replay/src/web_service/update_stop_condition.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{patch, web, Responder}; 2 | use log::info; 3 | use uuid::Uuid; 4 | 5 | use replaydb::job::stop_condition::JobStopCondition; 6 | use replaydb::service::JobManager; 7 | 8 | use crate::web_service::{JobService, ResponseMessage}; 9 | 10 | #[patch("/job/{job_id}/stop-condition")] 11 | async fn update_stop_condition( 12 | js: web::Data, 13 | job_id: web::Path, 14 | query: web::Json, 15 | ) -> impl Responder { 16 | let query_uuid_str = job_id.into_inner(); 17 | let job_uuid = Uuid::try_from(query_uuid_str.as_str()); 18 | if let Err(e) = job_uuid { 19 | let message = format!("Invalid job UUID: {}, error: {}", query_uuid_str, e); 20 | return ResponseMessage::Error(message); 21 | } 22 | let job_uuid = job_uuid.unwrap(); 23 | info!( 24 | "Received the stop condition update request for job {}: {:?}", 25 | job_uuid, query 26 | ); 27 | 28 | let mut js_bind = js.service.lock().await; 29 | let job = js_bind.update_stop_condition(job_uuid, query.into_inner()); 30 | match job { 31 | Ok(_) => ResponseMessage::Ok, 32 | Err(e) => ResponseMessage::Error(e.to_string()), 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /python/primitives/bbox/cmp.py: -------------------------------------------------------------------------------- 1 | from savant_rs.primitives.geometry import BBox, RBBox 2 | 3 | box1 = BBox(50, 50, 50, 50) 4 | box2 = BBox(50, 50, 50, 50) 5 | box3 = BBox(50, 50, 50, 60) 6 | 7 | assert box1 == box2 8 | assert box1.eq(box2) 9 | assert box1.almost_eq(box2, 0.1) 10 | 11 | assert box1 != box3 12 | assert not box1.eq(box3) 13 | 14 | box1 = RBBox(50, 50, 50, 50, 30) 15 | box2 = RBBox(50, 50, 50, 50, 30) 16 | box3 = RBBox(50, 50, 50, 50, 30.001) 17 | 18 | assert box1 == box2 19 | assert box1.eq(box2) 20 | assert box1.almost_eq(box2, 0.1) 21 | 22 | assert box1 != box3 23 | assert box1.almost_eq(box3, 0.1) 24 | iou = box1.iou(box2) 25 | assert iou == 1.0 26 | 27 | iou = box1.iou(box3) 28 | assert iou > 0.9 29 | 30 | for f in [ 31 | lambda: box1 > box2, 32 | lambda: box1 < box2, 33 | lambda: box1 >= box2, 34 | lambda: box1 <= box2, 35 | ]: 36 | try: 37 | f() 38 | assert False 39 | except NotImplementedError: 40 | pass 41 | 42 | empty_box = RBBox(0, 0, 0, 0) 43 | try: 44 | iou = box1.iou(empty_box) 45 | assert False 46 | except ValueError: 47 | pass 48 | 49 | empty_box = BBox(0, 0, 0, 0) 50 | box1 = BBox(50, 50, 50, 50) 51 | try: 52 | iou = box1.iou(empty_box) 53 | assert False 54 | except ValueError: 55 | pass 56 | -------------------------------------------------------------------------------- /savant_core_py/src/primitives.rs: -------------------------------------------------------------------------------- 1 | /// Attribute module specifies attribute code for [crate::primitives::BorrowedVideoObject] and [crate::primitives::VideoFrame]. 2 | /// 3 | pub mod attribute; 4 | pub mod attribute_value; 5 | pub mod batch; 6 | /// Here are decleared bounding boxes 7 | /// 8 | pub mod bbox; 9 | pub mod eos; 10 | pub mod frame; 11 | pub mod frame_update; 12 | pub mod message; 13 | pub mod object; 14 | pub mod objects_view; 15 | /// Simple point structure. 16 | pub mod point; 17 | /// A structure representing polygonal areas and functions. 18 | pub mod polygonal_area; 19 | /// Implementation for Python attributes in VideoObject and VideoFrame. 20 | pub mod pyobject; 21 | /// A line consisting of two points. 22 | pub mod segment; 23 | pub mod shutdown; 24 | pub mod user_data; 25 | 26 | use crate::primitives::frame::VideoFrame; 27 | 28 | use crate::primitives::attribute::Attribute; 29 | 30 | use crate::primitives::batch::VideoFrameBatch; 31 | use crate::primitives::bbox::BBox; 32 | use crate::primitives::bbox::RBBox; 33 | 34 | use crate::primitives::eos::EndOfStream; 35 | 36 | use crate::primitives::point::Point; 37 | use crate::primitives::polygonal_area::PolygonalArea; 38 | use crate::primitives::segment::{Intersection, Segment}; 39 | use crate::primitives::shutdown::Shutdown; 40 | -------------------------------------------------------------------------------- /python/primitives/bbox/utils.py: -------------------------------------------------------------------------------- 1 | from savant_rs.primitives.geometry import (RBBox, associate_bboxes, 2 | solely_owned_areas) 3 | from savant_rs.utils import BBoxMetricType 4 | 5 | red = RBBox.ltrb(0.0, 2.0, 2.0, 4.0) 6 | green = RBBox.ltrb(1.0, 3.0, 5.0, 5.0) 7 | yellow = RBBox.ltrb(1.0, 1.0, 3.0, 6.0) 8 | purple = RBBox.ltrb(4.0, 0.0, 7.0, 2.0) 9 | 10 | areas = solely_owned_areas([red, green, yellow, purple], parallel=True) 11 | 12 | red = areas[0] 13 | green = areas[1] 14 | yellow = areas[2] 15 | purple = areas[3] 16 | 17 | assert red == 2.0 18 | assert green == 4.0 19 | assert yellow == 5.0 20 | assert purple == 6.0 21 | 22 | lp1 = RBBox.ltrb(0.0, 1.0, 2.0, 2.0) 23 | lp2 = RBBox.ltrb(5.0, 2.0, 8.0, 3.0) 24 | lp3 = RBBox.ltrb(100.0, 0.0, 106.0, 3.0) 25 | owner1 = RBBox.ltrb(1.0, 0.0, 6.0, 3.0) 26 | owner2 = RBBox.ltrb(6.0, 1.0, 9.0, 4.0) 27 | 28 | associations_iou = associate_bboxes( 29 | [lp1, lp2, lp3], [owner1, owner2], BBoxMetricType.IoU, 0.01 30 | ) 31 | 32 | lp1_associations = associations_iou[0] 33 | lp2_associations = associations_iou[1] 34 | lp3_associations = associations_iou[2] 35 | 36 | assert list(map(lambda t: t[0], lp1_associations)) == [0] 37 | assert list(map(lambda t: t[0], lp2_associations)) == [1, 0] 38 | assert lp3_associations == [] 39 | -------------------------------------------------------------------------------- /utils/services/protoc.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -euo pipefail # Stricter error handling 3 | 4 | WORK_DIR=$(mktemp -d) 5 | cd "${WORK_DIR}" 6 | 7 | # Detect architecture in a more robust way 8 | ARCH=$(dpkg --print-architecture) 9 | 10 | # Define versions as variables for easier maintenance 11 | PROTOC_VERSION="3.15.8" 12 | PB_REL="https://github.com/protocolbuffers/protobuf/releases" 13 | 14 | # Download and verify protoc based on architecture 15 | case "${ARCH}" in 16 | amd64) 17 | PROTOC_FILE="protoc-${PROTOC_VERSION}-linux-x86_64.zip" 18 | ;; 19 | arm64) 20 | PROTOC_FILE="protoc-${PROTOC_VERSION}-linux-aarch_64.zip" 21 | ;; 22 | *) 23 | echo "Unsupported architecture: ${ARCH}" 24 | exit 1 25 | ;; 26 | esac 27 | 28 | # Download with error checking 29 | if ! curl -LO --fail "${PB_REL}/download/v${PROTOC_VERSION}/${PROTOC_FILE}"; then 30 | echo "Failed to download protoc" 31 | exit 1 32 | fi 33 | 34 | # Verify and install protoc 35 | if ! unzip -q "${PROTOC_FILE}"; then 36 | echo "Failed to extract protoc" 37 | exit 1 38 | fi 39 | 40 | install -m 755 bin/protoc /usr/local/bin/ 41 | rm -rf "${WORK_DIR}" 42 | 43 | # Verify installation 44 | if ! command -v protoc >/dev/null 2>&1; then 45 | echo "protoc installation failed" 46 | exit 1 47 | fi -------------------------------------------------------------------------------- /python/zmq/zmq_reqrep.py: -------------------------------------------------------------------------------- 1 | from threading import Thread 2 | from time import time 3 | 4 | import zmq 5 | from savant_rs.utils import gen_frame 6 | from savant_rs.utils.serialization import (Message, load_message_from_bytes, 7 | save_message_to_bytes) 8 | 9 | socket_name = "ipc:///tmp/test_hello" 10 | 11 | NUMBER = 1000 12 | BLOCK_SIZE = 1024 * 1024 13 | 14 | 15 | def server(): 16 | context = zmq.Context() 17 | socket = context.socket(zmq.ROUTER) 18 | socket.connect(socket_name) 19 | while True: 20 | message = socket.recv_multipart() 21 | if message[1] == b"end": 22 | print("Received end") 23 | break 24 | 25 | _ = load_message_from_bytes(message[1]) 26 | 27 | 28 | frame = gen_frame() 29 | p1 = Thread(target=server) 30 | p1.start() 31 | 32 | context = zmq.Context() 33 | socket = context.socket(zmq.DEALER) 34 | socket.bind(socket_name) 35 | 36 | buf_1024b = bytes(BLOCK_SIZE) 37 | 38 | start = time() 39 | wait_time = 0 40 | m = Message.video_frame(frame) 41 | for _ in range(NUMBER): 42 | s = save_message_to_bytes(m) 43 | socket.send_multipart([s, buf_1024b]) 44 | wait = time() 45 | wait_time += time() - wait 46 | 47 | print("Time taken", time() - start, wait_time) 48 | socket.send_multipart([b"end"]) 49 | p1.join() 50 | -------------------------------------------------------------------------------- /python/match_query/simple_queries.py: -------------------------------------------------------------------------------- 1 | from savant_rs.match_query import FloatExpression as FE 2 | from savant_rs.match_query import IntExpression as IE 3 | from savant_rs.match_query import MatchQuery as MQ 4 | from savant_rs.match_query import StringExpression as SE 5 | from savant_rs.primitives.geometry import RBBox 6 | from savant_rs.utils import BBoxMetricType 7 | 8 | q = MQ.and_(MQ.id(IE.eq(5)), MQ.label(SE.eq("hello"))) 9 | print(q.yaml, "\n", q.json) 10 | 11 | q = MQ.or_(MQ.namespace(SE.eq("model1")), MQ.namespace(SE.eq("model2"))) 12 | print(q.yaml, "\n", q.json) 13 | 14 | q = MQ.not_(MQ.id(IE.eq(5))) 15 | print(q.yaml, "\n", q.json) 16 | 17 | q = MQ.stop_if_false(MQ.frame_is_key_frame()) 18 | print(q.yaml, "\n", q.json) 19 | 20 | q = MQ.stop_if_true(MQ.not_(MQ.frame_is_key_frame())) 21 | print(q.yaml, "\n", q.json) 22 | 23 | # More than one person among the children of the object 24 | q = MQ.with_children(MQ.label(SE.eq("person")), IE.ge(1)) 25 | print(q.yaml, "\n", q.json) 26 | 27 | q = MQ.eval("1 + 1 == 2") 28 | print(q.yaml, "\n", q.json) 29 | 30 | q = MQ.eval( 31 | """(etcd("pipeline_status", false) == true || env("PIPELINE_STATUS", false) == true) && frame.keyframe""" 32 | ) 33 | print(q.yaml, "\n", q.json) 34 | 35 | q = MQ.box_metric(RBBox(0.5, 0.5, 0.5, 0.5, 0.0), BBoxMetricType.IoU, FE.gt(0.5)) 36 | print(q.yaml, "\n", q.json) 37 | -------------------------------------------------------------------------------- /docker/services/Dockerfile.buffer_ng: -------------------------------------------------------------------------------- 1 | FROM ghcr.io/insight-platform/py314_rust:v1.3.0 AS builder 2 | 3 | ENV PATH="/root/.cargo/bin:$PATH" 4 | ENV CARGO_TARGET_DIR=/tmp/build 5 | 6 | RUN --mount=type=cache,target=/var/cache/apt \ 7 | apt-get update && apt install -y \ 8 | libunwind-dev \ 9 | libpython3-dev 10 | 11 | WORKDIR /opt/buffer_ng 12 | 13 | RUN rustup component add rustfmt 14 | RUN --mount=type=bind,source=.,target=/opt/savant-rs bash /opt/savant-rs/utils/services/docker-deps.sh 15 | RUN --mount=type=cache,target=/tmp/build --mount=type=bind,rw,source=.,target=/opt/savant-rs cd /opt/savant-rs && CARGO_TARGET_DIR=/tmp/build cargo build --release -p buffer_ng -p savant_info 16 | RUN --mount=type=cache,target=/tmp/build --mount=type=bind,source=.,target=/opt/savant-rs bash /opt/savant-rs/utils/services/buffer_ng/copy-deps.sh 17 | 18 | FROM python:3.14 AS runner 19 | 20 | RUN --mount=type=bind,source=.,target=/opt/savant-rs bash /opt/savant-rs/utils/services/docker-deps.sh 21 | 22 | COPY --from=builder /opt /opt 23 | 24 | WORKDIR /opt/buffer_ng 25 | 26 | ENV LD_LIBRARY_PATH=/opt/libs 27 | ENV LOGLEVEL=info 28 | ENV PYTHON_MODULE_ROOT=/opt/python 29 | 30 | EXPOSE 8080 31 | HEALTHCHECK --interval=10s --timeout=1s CMD curl -f http://localhost:8080/status || exit 1 32 | 33 | ENTRYPOINT ["/opt/bin/buffer_ng"] 34 | CMD ["/opt/etc/configuration.json"] 35 | -------------------------------------------------------------------------------- /services/router/assets/python/zmq_consumer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import argparse 3 | import sys 4 | from time import time 5 | 6 | from savant_rs.logging import LogLevel, set_log_level 7 | from savant_rs.zmq import ReaderConfigBuilder, BlockingReader, ReaderResultTimeout 8 | 9 | set_log_level(LogLevel.Info) 10 | 11 | 12 | def main(): 13 | parser = argparse.ArgumentParser(description="ZMQ Message Consumer") 14 | parser.add_argument( 15 | "--socket", 16 | required=True, 17 | help="ZMQ socket URI (e.g. router+bind:tcp://127.0.0.1:6666)", 18 | ) 19 | parser.add_argument( 20 | "--count", type=int, default=1000, help="Number of messages to receive" 21 | ) 22 | args = parser.parse_args() 23 | 24 | # Configure and start reader 25 | reader_config = ReaderConfigBuilder(args.socket).build() 26 | reader = BlockingReader(reader_config) 27 | reader.start() 28 | 29 | i = 0 30 | try: 31 | while i < args.count: 32 | m = reader.receive() 33 | if m.__class__ == ReaderResultTimeout: 34 | continue 35 | i += 1 36 | print(m.topic) 37 | 38 | print(f"Received {i} messages") 39 | 40 | except KeyboardInterrupt: 41 | print("\nConsumer interrupted by user") 42 | 43 | 44 | if __name__ == "__main__": 45 | main() 46 | -------------------------------------------------------------------------------- /services/replay/replay/assets/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "common": { 3 | "pass_metadata_only": false, 4 | "management_port": 8080, 5 | "stats_period": { 6 | "secs": 60, 7 | "nanos": 0 8 | }, 9 | "job_writer_cache_max_capacity": 1000, 10 | "job_writer_cache_ttl": { 11 | "secs": 60, 12 | "nanos": 0 13 | }, 14 | "job_eviction_ttl": { 15 | "secs": 60, 16 | "nanos": 0 17 | }, 18 | "default_job_sink_options": { 19 | "send_timeout": { 20 | "secs": 1, 21 | "nanos": 0 22 | }, 23 | "send_retries": 3, 24 | "receive_timeout": { 25 | "secs": 1, 26 | "nanos": 0 27 | }, 28 | "receive_retries": 3, 29 | "send_hwm": 1000, 30 | "receive_hwm": 100, 31 | "inflight_ops": 100 32 | } 33 | }, 34 | "in_stream": { 35 | "url": "router+bind:ipc:///tmp/${SOCKET_PATH_IN:-undefined}", 36 | "options": { 37 | "receive_timeout": { 38 | "secs": 1, 39 | "nanos": 0 40 | }, 41 | "receive_hwm": 1000, 42 | "topic_prefix_spec": "none", 43 | "source_cache_size": 1000, 44 | "inflight_ops": 100, 45 | "fix_ipc_permissions": 511 46 | } 47 | }, 48 | "out_stream": null, 49 | "storage": { 50 | "rocksdb": { 51 | "path": "${DB_PATH:-/tmp/rocksdb}", 52 | "data_expiration_ttl": { 53 | "secs": 60, 54 | "nanos": 0 55 | } 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /services/buffer_ng/assets/python/zmq_consumer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import argparse 3 | import sys 4 | from time import time 5 | 6 | from savant_rs.logging import LogLevel, set_log_level 7 | from savant_rs.zmq import ReaderConfigBuilder, BlockingReader, ReaderResultTimeout 8 | from time import time 9 | 10 | set_log_level(LogLevel.Info) 11 | 12 | 13 | def main(): 14 | parser = argparse.ArgumentParser(description="ZMQ Message Consumer") 15 | parser.add_argument( 16 | "--socket", 17 | required=True, 18 | help="ZMQ socket URI (e.g. router+bind:tcp://127.0.0.1:6666)", 19 | ) 20 | parser.add_argument( 21 | "--count", type=int, default=1000, help="Number of messages to receive" 22 | ) 23 | args = parser.parse_args() 24 | 25 | # Configure and start reader 26 | reader_config = ReaderConfigBuilder(args.socket).build() 27 | reader = BlockingReader(reader_config) 28 | reader.start() 29 | 30 | i = 0 31 | now = time() 32 | try: 33 | while i < args.count: 34 | m = reader.receive() 35 | if m.__class__ == ReaderResultTimeout: 36 | continue 37 | i += 1 38 | if i % 1000 == 0: 39 | print(f"Received {i} messages") 40 | 41 | print(f"Time taken: {time() - now}s") 42 | 43 | except KeyboardInterrupt: 44 | print("\nConsumer interrupted by user") 45 | 46 | 47 | if __name__ == "__main__": 48 | main() 49 | -------------------------------------------------------------------------------- /docker/Dockerfile.py313: -------------------------------------------------------------------------------- 1 | FROM ghcr.io/insight-platform/py313_rust:v1.3.0 AS builder 2 | 3 | ENV PYTHON_INTERPRETER=python3.13 4 | 5 | RUN --mount=type=cache,target=/var/cache/apt \ 6 | apt-get update && apt install -y \ 7 | jq \ 8 | libunwind-dev \ 9 | libgstreamer-plugins-base1.0-dev \ 10 | gstreamer1.0-plugins-base gstreamer1.0-plugins-good \ 11 | gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly \ 12 | gstreamer1.0-libav libgstrtspserver-1.0-dev libges-1.0-dev \ 13 | libpython3-dev 14 | 15 | RUN --mount=type=bind,source=.,target=/opt/savant-rs \ 16 | pip install -r /opt/savant-rs/requirements.txt 17 | 18 | # add rust path to PATH 19 | ENV PATH="/root/.cargo/bin:$PATH" 20 | ENV CARGO_TARGET_DIR=/tmp/build 21 | 22 | RUN --mount=type=cache,target=/root/.cargo/registry \ 23 | --mount=type=cache,target=/tmp/build \ 24 | --mount=type=bind,rw,source=.,target=/opt/savant-rs \ 25 | cd /opt/savant-rs && make release && cp dist/*.whl / 26 | 27 | RUN --mount=type=cache,target=/tmp/build --mount=type=bind,rw,source=.,target=/opt/savant-rs \ 28 | cd /opt/savant-rs && CARGO_TARGET_DIR=/tmp/build \ 29 | cargo build --release -p savant_info 30 | 31 | RUN --mount=type=cache,target=/tmp/build --mount=type=bind,source=.,target=/opt/savant-rs bash /opt/savant-rs/utils/python313/copy-deps.sh 32 | 33 | FROM python:3.13-slim AS runner 34 | 35 | COPY --from=builder /opt /opt 36 | COPY --from=builder /*.whl /tmp 37 | RUN pip3 install /tmp/*.whl && rm /tmp/*.whl 38 | -------------------------------------------------------------------------------- /docker/Dockerfile.py314: -------------------------------------------------------------------------------- 1 | FROM ghcr.io/insight-platform/py314_rust:v1.3.0 AS builder 2 | 3 | ENV PYTHON_INTERPRETER=python3.14 4 | 5 | RUN --mount=type=cache,target=/var/cache/apt \ 6 | apt-get update && apt install -y \ 7 | jq \ 8 | libunwind-dev \ 9 | libgstreamer-plugins-base1.0-dev \ 10 | gstreamer1.0-plugins-base gstreamer1.0-plugins-good \ 11 | gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly \ 12 | gstreamer1.0-libav libgstrtspserver-1.0-dev libges-1.0-dev \ 13 | libpython3-dev 14 | 15 | RUN --mount=type=bind,source=.,target=/opt/savant-rs \ 16 | pip install -r /opt/savant-rs/requirements.txt 17 | 18 | # add rust path to PATH 19 | ENV PATH="/root/.cargo/bin:$PATH" 20 | ENV CARGO_TARGET_DIR=/tmp/build 21 | 22 | RUN --mount=type=cache,target=/root/.cargo/registry \ 23 | --mount=type=cache,target=/tmp/build \ 24 | --mount=type=bind,rw,source=.,target=/opt/savant-rs \ 25 | cd /opt/savant-rs && make release && cp dist/*.whl / 26 | 27 | RUN --mount=type=cache,target=/tmp/build --mount=type=bind,rw,source=.,target=/opt/savant-rs \ 28 | cd /opt/savant-rs && CARGO_TARGET_DIR=/tmp/build \ 29 | cargo build --release -p savant_info 30 | 31 | RUN --mount=type=cache,target=/tmp/build --mount=type=bind,source=.,target=/opt/savant-rs bash /opt/savant-rs/utils/python314/copy-deps.sh 32 | 33 | FROM python:3.14-slim AS runner 34 | 35 | COPY --from=builder /opt /opt 36 | COPY --from=builder /*.whl /tmp 37 | RUN pip3 install /tmp/*.whl && rm /tmp/*.whl 38 | -------------------------------------------------------------------------------- /services/replay/replaydb/assets/rocksdb.json: -------------------------------------------------------------------------------- 1 | { 2 | "common": { 3 | "pass_metadata_only": false, 4 | "management_port": 8080, 5 | "stats_period": { 6 | "secs": 60, 7 | "nanos": 0 8 | }, 9 | "job_writer_cache_max_capacity": 1000, 10 | "job_writer_cache_ttl": { 11 | "secs": 60, 12 | "nanos": 0 13 | }, 14 | "job_eviction_ttl": { 15 | "secs": 60, 16 | "nanos": 0 17 | } 18 | }, 19 | "in_stream": { 20 | "url": "router+bind:ipc:///tmp/${SOCKET_PATH_IN:-undefined}", 21 | "options": { 22 | "receive_timeout": { 23 | "secs": 1, 24 | "nanos": 0 25 | }, 26 | "receive_hwm": 1000, 27 | "topic_prefix_spec": { 28 | "source_id": "source_id" 29 | }, 30 | "source_cache_size": 1000, 31 | "fix_ipc_permissions": 511, 32 | "inflight_ops": 100 33 | } 34 | }, 35 | "out_stream": { 36 | "url": "dealer+connect:ipc:///tmp/${SOCKET_PATH_OUT:-undefined}", 37 | "send_timeout": { 38 | "secs": 1, 39 | "nanos": 0 40 | }, 41 | "send_retries": 3, 42 | "receive_timeout": { 43 | "secs": 1, 44 | "nanos": 0 45 | }, 46 | "receive_retries": 3, 47 | "send_hwm": 1000, 48 | "receive_hwm": 100, 49 | "inflight_ops": 100 50 | }, 51 | "storage": { 52 | "rocksdb": { 53 | "path": "${DB_PATH:-/tmp/rocksdb}", 54 | "data_expiration_ttl": { 55 | "secs": 3600, 56 | "nanos": 0 57 | } 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /savant_core/src/primitives/any_object.rs: -------------------------------------------------------------------------------- 1 | use parking_lot::Mutex; 2 | use std::any::Any; 3 | use std::sync::Arc; 4 | 5 | #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] 6 | pub struct AnyObject { 7 | #[serde(skip_deserializing, skip_serializing)] 8 | pub value: Arc>>>, 9 | } 10 | 11 | impl Default for AnyObject { 12 | fn default() -> Self { 13 | Self { 14 | value: Arc::new(Mutex::new(None)), 15 | } 16 | } 17 | } 18 | 19 | impl PartialEq for AnyObject { 20 | fn eq(&self, _: &Self) -> bool { 21 | false 22 | } 23 | } 24 | 25 | impl AnyObject { 26 | pub fn new(value: Box) -> Self { 27 | Self { 28 | value: Arc::new(Mutex::new(Some(value))), 29 | } 30 | } 31 | pub fn set(&self, value: Box) { 32 | let mut bind = self.value.lock(); 33 | *bind = Some(value); 34 | } 35 | 36 | pub fn take(&self) -> Option> { 37 | let mut value = self.value.lock(); 38 | value.take() 39 | } 40 | 41 | pub fn access(&self) -> Arc>>> { 42 | self.value.clone() 43 | } 44 | } 45 | 46 | #[cfg(test)] 47 | mod tests { 48 | #[test] 49 | fn test_any_object() { 50 | let p = super::AnyObject::new(Box::new(1.0)); 51 | let v = p.take().unwrap(); 52 | let v = v.downcast::().unwrap(); 53 | assert_eq!(v, Box::new(1.0)); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /savant_core/src/utils/default_once.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Debug; 2 | use std::sync::OnceLock; 3 | 4 | #[derive(Debug, Clone)] 5 | pub struct DefaultOnceCell { 6 | cell: OnceLock, 7 | default: T, 8 | } 9 | 10 | impl DefaultOnceCell 11 | where 12 | T: Clone + Debug, 13 | { 14 | pub fn new(default: T) -> Self { 15 | Self { 16 | cell: OnceLock::new(), 17 | default, 18 | } 19 | } 20 | 21 | pub fn set(&self, value: T) -> anyhow::Result<()> { 22 | self.cell 23 | .set(value) 24 | .map_err(|_| anyhow::anyhow!("Cell already initialized"))?; 25 | Ok(()) 26 | } 27 | 28 | pub fn is_initialized(&self) -> bool { 29 | self.cell.get().is_some() 30 | } 31 | 32 | pub fn get_or_init(&self) -> &T { 33 | self.cell.get_or_init(|| self.default.clone()) 34 | } 35 | 36 | pub fn get_default(&self) -> T { 37 | self.default.clone() 38 | } 39 | } 40 | 41 | #[cfg(test)] 42 | mod tests { 43 | use super::*; 44 | 45 | #[test] 46 | fn test_default_once_cell() { 47 | let cell = DefaultOnceCell::new(1); 48 | assert_eq!(*cell.get_or_init(), 1); 49 | assert!(cell.set(2).is_err()); 50 | assert_eq!(*cell.get_or_init(), 1); 51 | } 52 | 53 | #[test] 54 | fn test_set_once_cell() { 55 | let cell = DefaultOnceCell::new(1); 56 | assert!(cell.set(2).is_ok()); 57 | assert_eq!(*cell.get_or_init(), 2); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /savant_python/python/savant_rs/telemetry/telemetry.pyi: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import Optional 3 | 4 | __all__ = [ 5 | "ContextPropagationFormat", 6 | "Protocol", 7 | "Identity", 8 | "ClientTlsConfig", 9 | "TracerConfiguration", 10 | "TelemetryConfiguration", 11 | "init", 12 | "shutdown", 13 | "init_from_file", 14 | ] 15 | 16 | class ContextPropagationFormat(Enum): 17 | Jaeger: int 18 | W3C: int 19 | 20 | class Protocol(Enum): 21 | Grpc: int 22 | HttpBinary: int 23 | HttpJson: int 24 | 25 | class Identity: 26 | def __init__(self, key: str, certificate: str): ... 27 | 28 | class ClientTlsConfig: 29 | def __init__( 30 | self, certificate: Optional[str] = None, identity: Optional[Identity] = None 31 | ): ... 32 | 33 | class TracerConfiguration: 34 | def __init__( 35 | self, 36 | service_name: str, 37 | protocol: Protocol, 38 | endpoint: str, 39 | tls: Optional[ClientTlsConfig] = None, 40 | timeout: Optional[int] = None, 41 | ): ... 42 | 43 | class TelemetryConfiguration: 44 | def __init__( 45 | self, 46 | context_propagation_format: Optional[ContextPropagationFormat] = None, 47 | tracer: Optional[TracerConfiguration] = None, 48 | ): ... 49 | @classmethod 50 | def no_op(cls) -> TelemetryConfiguration: ... 51 | 52 | def init(config: TelemetryConfiguration) -> None: ... 53 | def init_from_file(path: str) -> None: ... 54 | def shutdown() -> None: ... 55 | -------------------------------------------------------------------------------- /services/retina_rtsp/assets/configuration_3groups_no_ntp.json: -------------------------------------------------------------------------------- 1 | { 2 | "sink": { 3 | "url": "pub+connect:tcp://127.0.0.1:6666", 4 | "options": { 5 | "send_timeout": { 6 | "secs": 1, 7 | "nanos": 0 8 | }, 9 | "send_retries": 3, 10 | "receive_timeout": { 11 | "secs": 1, 12 | "nanos": 0 13 | }, 14 | "receive_retries": 3, 15 | "send_hwm": 1000, 16 | "receive_hwm": 1000, 17 | "inflight_ops": 100 18 | } 19 | }, 20 | "rtsp_sources": { 21 | "group0": { 22 | "sources": [ 23 | { 24 | "source_id": "city-traffic", 25 | "url": "rtsp://hello.savant.video:8554/stream/city-traffic" 26 | } 27 | ] 28 | }, 29 | "group1": { 30 | "sources": [ 31 | { 32 | "source_id": "town-centre", 33 | "url": "rtsp://hello.savant.video:8554/stream/town-centre" 34 | } 35 | ] 36 | }, 37 | "group2": { 38 | "sources": [ 39 | { 40 | "source_id": "fake-town-centre", 41 | "url": "rtsp://127.0.0.1:8554/stream/town-centre" 42 | } 43 | ] 44 | } 45 | }, 46 | "reconnect_interval": { 47 | "secs": 5, 48 | "nanos": 0 49 | } 50 | } -------------------------------------------------------------------------------- /savant_core/src/primitives/userdata.rs: -------------------------------------------------------------------------------- 1 | use crate::json_api::ToSerdeJsonValue; 2 | use crate::primitives::{Attribute, WithAttributes}; 3 | use serde_json::Value; 4 | 5 | #[derive(Debug, PartialEq, Clone, serde::Serialize)] 6 | pub struct UserData { 7 | pub source_id: String, 8 | pub attributes: Vec, 9 | } 10 | 11 | impl ToSerdeJsonValue for UserData { 12 | fn to_serde_json_value(&self) -> Value { 13 | serde_json::json!(self) 14 | } 15 | } 16 | 17 | const DEFAULT_ATTRIBUTES_COUNT: usize = 4; 18 | 19 | impl UserData { 20 | pub fn new(source_id: &str) -> Self { 21 | Self { 22 | source_id: source_id.to_string(), 23 | attributes: Vec::with_capacity(DEFAULT_ATTRIBUTES_COUNT), 24 | } 25 | } 26 | 27 | pub fn json(&self) -> String { 28 | serde_json::to_string(&self.to_serde_json_value()).unwrap() 29 | } 30 | 31 | pub fn json_pretty(&self) -> String { 32 | serde_json::to_string_pretty(&self.to_serde_json_value()).unwrap() 33 | } 34 | 35 | pub fn get_source_id(&self) -> &str { 36 | &self.source_id 37 | } 38 | } 39 | 40 | impl WithAttributes for UserData { 41 | fn with_attributes_ref(&self, f: F) -> R 42 | where 43 | F: FnOnce(&Vec) -> R, 44 | { 45 | f(&self.attributes) 46 | } 47 | 48 | fn with_attributes_mut(&mut self, f: F) -> R 49 | where 50 | F: FnOnce(&mut Vec) -> R, 51 | { 52 | f(&mut self.attributes) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /python/primitives/load_save_various_approaches.py: -------------------------------------------------------------------------------- 1 | from timeit import default_timer as timer 2 | 3 | from savant_rs.utils import gen_frame 4 | from savant_rs.utils.serialization import (Message, load_message, 5 | load_message_from_bytebuffer, 6 | load_message_from_bytes, 7 | save_message, 8 | save_message_to_bytebuffer, 9 | save_message_to_bytes) 10 | 11 | f = gen_frame() 12 | m = Message.video_frame(f) 13 | t = timer() 14 | for _ in range(1_000): 15 | s = save_message(m) 16 | new_m = load_message(s) 17 | assert new_m.is_video_frame() 18 | 19 | print("Regular Save/Load", timer() - t) 20 | 21 | t = timer() 22 | for _ in range(1_000): 23 | s = save_message_to_bytebuffer(m, with_hash=False) 24 | new_m = load_message_from_bytebuffer(s) 25 | assert new_m.is_video_frame() 26 | 27 | print("ByteBuffer (no hash) Save/Load", timer() - t) 28 | 29 | t = timer() 30 | for _ in range(1_000): 31 | s = save_message_to_bytebuffer(m, with_hash=True) 32 | new_m = load_message_from_bytebuffer(s) 33 | assert new_m.is_video_frame() 34 | 35 | print("ByteBuffer (with hash) Save/Load", timer() - t) 36 | 37 | t = timer() 38 | for _ in range(1_000): 39 | s = save_message_to_bytes(m) 40 | new_m = load_message_from_bytes(s) 41 | assert new_m.is_video_frame() 42 | 43 | print("Python bytes Save/Load", timer() - t) 44 | -------------------------------------------------------------------------------- /savant_core_py/src/primitives/pyobject.rs: -------------------------------------------------------------------------------- 1 | use crate::attach; 2 | use parking_lot::RwLock; 3 | use pyo3::{Py, PyAny}; 4 | use std::collections::HashMap; 5 | use std::sync::Arc; 6 | 7 | type PyObjectsMap = Arc>>>; 8 | 9 | pub trait PyObjectMeta: Send { 10 | fn get_py_objects_ref(&self) -> PyObjectsMap; 11 | 12 | fn get_py_object_by_ref(&self, namespace: &str, name: &str) -> Option> { 13 | attach!(|py| { 14 | self.get_py_objects_ref() 15 | .read() 16 | .get(&(namespace.to_owned(), name.to_owned())) 17 | .map(|o| o.clone_ref(py)) 18 | }) 19 | } 20 | 21 | fn del_py_object(&self, namespace: &str, name: &str) -> Option> { 22 | self.get_py_objects_ref() 23 | .write() 24 | .remove(&(namespace.to_owned(), name.to_owned())) 25 | } 26 | 27 | fn set_py_object(&self, namespace: &str, name: &str, pyobject: Py) -> Option> { 28 | self.get_py_objects_ref() 29 | .write() 30 | .insert((namespace.to_owned(), name.to_owned()), pyobject) 31 | } 32 | 33 | fn clear_py_objects(&self) { 34 | self.get_py_objects_ref().write().clear(); 35 | } 36 | 37 | fn list_py_objects(&self) -> Vec<(String, String)> { 38 | self.get_py_objects_ref() 39 | .read() 40 | .keys() 41 | .map(|(namespace, name)| (namespace.clone(), name.clone())) 42 | .collect() 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /savant_python/python/savant_rs/metrics/metrics.pyi: -------------------------------------------------------------------------------- 1 | from typing import Dict, List, Optional 2 | 3 | __all__ = [ 4 | "CounterFamily", 5 | "GaugeFamily", 6 | "delete_metric_family", 7 | "set_extra_labels", 8 | ] 9 | 10 | class CounterFamily: 11 | @classmethod 12 | def get_or_create_counter_family( 13 | cls, 14 | name: str, 15 | description: Optional[str], 16 | label_names: List[str], 17 | unit: Optional[str], 18 | ) -> CounterFamily: ... 19 | @classmethod 20 | def get_counter_family(cls, name: str) -> Optional[CounterFamily]: ... 21 | def set(self, value: int, label_values: List[str]) -> int: ... 22 | def inc(self, value: int, label_values: List[str]) -> int: ... 23 | def delete(self, label_values: List[str]) -> Optional[int]: ... 24 | def get(self, label_values: List[str]) -> Optional[int]: ... 25 | 26 | class GaugeFamily: 27 | @classmethod 28 | def get_or_create_gauge_family( 29 | cls, 30 | name: str, 31 | description: Optional[str], 32 | label_names: List[str], 33 | unit: Optional[str], 34 | ) -> GaugeFamily: ... 35 | @classmethod 36 | def get_gauge_family(cls, name: str) -> Optional[GaugeFamily]: ... 37 | def set(self, value: float, label_values: List[str]) -> float: ... 38 | def delete(self, label_values: List[str]) -> Optional[float]: ... 39 | def get(self, label_values: List[str]) -> Optional[float]: ... 40 | 41 | def delete_metric_family(name: str) -> None: ... 42 | def set_extra_labels(labels: Dict[str, str]) -> None: ... 43 | -------------------------------------------------------------------------------- /savant_core_py/src/utils/bigint.rs: -------------------------------------------------------------------------------- 1 | use lazy_static::lazy_static; 2 | use num_bigint::BigInt; 3 | 4 | lazy_static! { 5 | // Initialize these values only once at runtime 6 | static ref MAX_I64: BigInt = BigInt::from(i64::MAX); 7 | static ref MIN_I64: BigInt = BigInt::from(i64::MIN); 8 | static ref MAX_U64: BigInt = BigInt::from(u64::MAX); 9 | static ref MIN_U64: BigInt = BigInt::from(u64::MIN); 10 | } 11 | 12 | pub fn fit_i64(v: BigInt) -> i64 { 13 | // Use the static references instead of creating new instances 14 | let v = if v > *MAX_I64 { 15 | let res = v.clone() % &*MAX_I64; 16 | log::warn!( 17 | "v is greater than i64::MAX, cropping to fit the i64 range, original: {}, result: {}", 18 | &v, 19 | &res 20 | ); 21 | res 22 | } else if v < *MIN_I64 { 23 | let res = v.clone() % &*MIN_I64; 24 | log::warn!( 25 | "v is less than i64::MIN, cropping to fit the i64 range, original: {}, result: {}", 26 | &v, 27 | &res 28 | ); 29 | res 30 | } else { 31 | v 32 | }; 33 | i64::try_from(v).expect("v must be in the range of i64") 34 | } 35 | 36 | #[cfg(test)] 37 | mod tests { 38 | use super::*; 39 | 40 | #[test] 41 | fn test_fit_i64() { 42 | assert_eq!(fit_i64(BigInt::from(i64::MAX)), i64::MAX); 43 | assert_eq!(fit_i64(BigInt::from(i64::MIN)), i64::MIN); 44 | assert_eq!(fit_i64(BigInt::from(i64::MAX) + 1), 1); 45 | assert_eq!(fit_i64(BigInt::from(i64::MIN) - 1), -1); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /services/buffer_ng/assets/python/module.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Callable 2 | from savant_rs.logging import log, LogLevel 3 | from savant_rs.utils.serialization import Message 4 | from time import sleep, time 5 | 6 | 7 | class MessageHandler: 8 | """ 9 | This handler is called for each message received from the ingress. 10 | """ 11 | 12 | def __init__(self): 13 | self.count = 0 14 | self.now = time() 15 | 16 | def __call__( 17 | self, topic: str, message: Message 18 | ) -> (str, Message): 19 | """ 20 | This handler is called for each message received from the ingress. 21 | 22 | :param topic: ZMQ topic of the message if any 23 | :param message: message object, can be modified in place to add/remove labels, attributes, etc. 24 | :return: message object to be sent to the next step or None if message must be dropped 25 | """ 26 | self.count += 1 27 | if self.count % 1000 == 0: 28 | print(f"message_handler {self.count}, elapsed {time() - self.now}s") 29 | self.now = time() 30 | return topic, message 31 | # return None 32 | 33 | 34 | def init(params: Any) -> Callable: 35 | """ 36 | This function is called once when the service starts. It is specified in the configuration.json file. 37 | """ 38 | log(LogLevel.Info, "buffer_ng::init", "Buffer NG service initialized successfully") 39 | # True means that the service is initialized successfully and can start processing messages 40 | return MessageHandler() 41 | #return None 42 | -------------------------------------------------------------------------------- /savant_core/src/metrics/metric_collector.rs: -------------------------------------------------------------------------------- 1 | use crate::metrics::{export_metrics, ConstMetric}; 2 | use prometheus_client::collector::Collector; 3 | use prometheus_client::encoding::{DescriptorEncoder, EncodeMetric}; 4 | use prometheus_client::metrics::MetricType; 5 | 6 | #[derive(Debug)] 7 | pub struct SystemMetricCollector; 8 | 9 | impl Collector for SystemMetricCollector { 10 | fn encode(&self, mut encoder: DescriptorEncoder) -> Result<(), std::fmt::Error> { 11 | let exported_user_metrics = export_metrics(); 12 | for m in exported_user_metrics { 13 | let (name, description, unit, metric) = (m.name, m.description, m.unit, m.metric); 14 | let desc_str = description.unwrap_or("".to_string()); 15 | match metric { 16 | ConstMetric::Counter(c) => { 17 | let metric_encoder = encoder.encode_descriptor( 18 | &name, 19 | &desc_str, 20 | unit.as_ref(), 21 | MetricType::Counter, 22 | )?; 23 | 24 | c.encode(metric_encoder)?; 25 | } 26 | ConstMetric::Gauge(g) => { 27 | let metric_encoder = encoder.encode_descriptor( 28 | &name, 29 | &desc_str, 30 | unit.as_ref(), 31 | MetricType::Gauge, 32 | )?; 33 | g.encode(metric_encoder)?; 34 | } 35 | } 36 | } 37 | Ok(()) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /services/replay/README.md: -------------------------------------------------------------------------------- 1 | # Replay 2 | 3 | Collect video streams in [Savant](https://github.com/insight-platform/Savant) pipelines and re-stream them effortlessly with REST API. 4 | 5 | ![](docs/_static/replay_usage_diagram.png) 6 | 7 | ## Features 8 | 9 | Replay is an advanced storage providing features required for non-linear computer vision and video analytics: 10 | 11 | - collects video from multiple streams (archiving with TTL eviction); 12 | - provides a REST API for video re-streaming to Savant sinks or modules; 13 | - supports time-synchronized and fast video re-streaming; 14 | - supports configurable video re-streaming stop conditions; 15 | - supports setting minimum and maximum frame duration to increase or decrease the video playback speed; 16 | - can fix incorrect TS in re-streaming video streams; 17 | - can look backward when video stream re-streamed; 18 | - can set additional attributes to retrieved video streams; 19 | - can work as a sidecar or intermediary service in Savant pipelines. 20 | 21 | ## How It Is Implemented 22 | 23 | Replay is implemented with Rust and RocksDB. It allows delivering video with low latency and high 24 | throughput. Replay can be deployed on edge devices and in the cloud on ARM or X86 devices. 25 | 26 | ## Sample 27 | 28 | This [sample](samples/file_restreaming) shows how to ingest video file to Replay and then re-stream it to AO-RTSP with 29 | REST API. 30 | 31 | ## Documentation 32 | 33 | The documentation is available at [GitHub Pages](https://insight-platform.github.io/Replay/). 34 | 35 | ## License 36 | 37 | Replay is licensed under the Apache 2.0 license. See [LICENSE](LICENSE) for more information. 38 | -------------------------------------------------------------------------------- /savant_core/examples/rtp_pts_mapper.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | use std::thread::sleep; 3 | use std::time::{Duration, UNIX_EPOCH}; 4 | 5 | use savant_core::utils::rtp_pts_mapper::RtpPtsMapper; 6 | 7 | const VIDEO_CLOCK_RATE: i64 = 900_000_000; 8 | const TARGET_FPS: u32 = 1; 9 | const SEED_RTP: u32 = u32::MAX - 1_000_000; 10 | const SEED_TS: Duration = Duration::from_secs(0); 11 | 12 | fn main() -> Result<(), Box> { 13 | let mut mapper = RtpPtsMapper::with_seed( 14 | SEED_RTP, 15 | SEED_TS, 16 | (1, VIDEO_CLOCK_RATE), 17 | (1, 1_000_000), // 1us ticks for PTS 18 | )?; 19 | 20 | let frame_interval_ticks = (VIDEO_CLOCK_RATE as u32) / TARGET_FPS; 21 | let frame_interval_sleep = Duration::from_micros(1_000_000 / TARGET_FPS as u64); 22 | 23 | let mut rtp = SEED_RTP; 24 | let mut last_pts: Option = None; 25 | let mut last_ts: Option = None; 26 | loop { 27 | rtp = rtp.wrapping_add(frame_interval_ticks); 28 | let mapping = mapper.map(rtp)?; 29 | 30 | let wall_clock = UNIX_EPOCH + mapping.ts; 31 | let pts_delta = last_pts.map(|p| mapping.pts - p).unwrap_or(0); 32 | let ts_delta = last_ts 33 | .map(|ts| mapping.ts.checked_sub(ts).unwrap_or_default()) 34 | .unwrap_or_default(); 35 | 36 | println!( 37 | "RTP {:>12} -> PTS {:>12} (ΔPTS = {:>6}, Δt = {:?}, ts = {:?})", 38 | rtp, mapping.pts, pts_delta, ts_delta, wall_clock 39 | ); 40 | 41 | last_pts = Some(mapping.pts); 42 | last_ts = Some(mapping.ts); 43 | 44 | sleep(frame_interval_sleep); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /python/zmq/zmq_native.py: -------------------------------------------------------------------------------- 1 | from threading import Thread 2 | from time import sleep, time 3 | 4 | from savant_rs.logging import LogLevel, set_log_level 5 | from savant_rs.utils import gen_frame 6 | from savant_rs.utils.serialization import Message 7 | from savant_rs.zmq import (BlockingReader, BlockingWriter, ReaderConfigBuilder, 8 | WriterConfigBuilder) 9 | 10 | set_log_level(LogLevel.Info) 11 | 12 | socket_name = "tcp://127.0.0.1:3333" 13 | 14 | NUMBER = 1000 15 | BLOCK_SIZE = 1024 * 1024 16 | 17 | 18 | def server(): 19 | reader_config = ReaderConfigBuilder("router+connect:" + socket_name).build() 20 | reader = BlockingReader(reader_config) 21 | reader.start() 22 | reader.blacklist_source(b"unused-topic") 23 | assert reader.is_blacklisted(b"unused-topic") 24 | wait_time = 0 25 | for _ in range(NUMBER): 26 | wait = time() 27 | m = reader.receive() 28 | wait_time += time() - wait 29 | assert len(m.data(0)) == BLOCK_SIZE 30 | print("Reader time awaited", wait_time) 31 | 32 | 33 | frame = gen_frame() 34 | p1 = Thread(target=server) 35 | p1.start() 36 | 37 | buf = bytes(BLOCK_SIZE) 38 | 39 | # test late start up for bind socket 40 | sleep(0.1) 41 | 42 | writer_config = WriterConfigBuilder("dealer+bind:" + socket_name).build() 43 | writer = BlockingWriter(writer_config) 44 | writer.start() 45 | 46 | start = time() 47 | wait_time = 0 48 | for _ in range(NUMBER): 49 | m = Message.video_frame(frame) 50 | wait = time() 51 | writer.send_message("topic", m, buf) 52 | wait_time += time() - wait 53 | 54 | print("Writer time taken", time() - start, "awaited", wait_time) 55 | p1.join() 56 | -------------------------------------------------------------------------------- /savant_core/src/primitives/attribute_set.rs: -------------------------------------------------------------------------------- 1 | use crate::json_api::ToSerdeJsonValue; 2 | use crate::primitives::{Attribute, WithAttributes}; 3 | use crate::protobuf::from_pb; 4 | use savant_protobuf::generated; 5 | use serde_json::Value; 6 | 7 | #[derive(Debug, PartialEq, Clone, serde::Serialize, Default)] 8 | pub struct AttributeSet { 9 | pub attributes: Vec, 10 | } 11 | 12 | impl ToSerdeJsonValue for AttributeSet { 13 | fn to_serde_json_value(&self) -> Value { 14 | serde_json::json!(self) 15 | } 16 | } 17 | 18 | impl From> for AttributeSet { 19 | fn from(attributes: Vec) -> Self { 20 | Self { attributes } 21 | } 22 | } 23 | 24 | impl AttributeSet { 25 | pub fn new() -> Self { 26 | Self::default() 27 | } 28 | 29 | pub fn deserialize(bytes: &[u8]) -> anyhow::Result> { 30 | let deser = from_pb::(bytes)?; 31 | Ok(deser.attributes) 32 | } 33 | 34 | pub fn json(&self) -> String { 35 | serde_json::to_string(&self.to_serde_json_value()).unwrap() 36 | } 37 | 38 | pub fn json_pretty(&self) -> String { 39 | serde_json::to_string_pretty(&self.to_serde_json_value()).unwrap() 40 | } 41 | } 42 | 43 | impl WithAttributes for AttributeSet { 44 | fn with_attributes_ref(&self, f: F) -> R 45 | where 46 | F: FnOnce(&Vec) -> R, 47 | { 48 | f(&self.attributes) 49 | } 50 | 51 | fn with_attributes_mut(&mut self, f: F) -> R 52 | where 53 | F: FnOnce(&mut Vec) -> R, 54 | { 55 | f(&mut self.attributes) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /services/replay/samples/file_restreaming/replay_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "common": { 3 | "pass_metadata_only": false, 4 | "management_port": 8080, 5 | "stats_period": { 6 | "secs": 60, 7 | "nanos": 0 8 | }, 9 | "job_writer_cache_max_capacity": 1000, 10 | "job_writer_cache_ttl": { 11 | "secs": 60, 12 | "nanos": 0 13 | }, 14 | "job_eviction_ttl": { 15 | "secs": 60, 16 | "nanos": 0 17 | }, 18 | "default_job_sink_options": { 19 | "send_timeout": { 20 | "secs": 1, 21 | "nanos": 0 22 | }, 23 | "send_retries": 3, 24 | "receive_timeout": { 25 | "secs": 1, 26 | "nanos": 0 27 | }, 28 | "receive_retries": 3, 29 | "send_hwm": 1000, 30 | "receive_hwm": 100, 31 | "inflight_ops": 100 32 | } 33 | }, 34 | "in_stream": { 35 | "url": "router+bind:tcp://127.0.0.1:5555", 36 | "options": { 37 | "receive_timeout": { 38 | "secs": 1, 39 | "nanos": 0 40 | }, 41 | "receive_hwm": 1000, 42 | "topic_prefix_spec": { 43 | "source_id": "in-video" 44 | }, 45 | "source_cache_size": 1000, 46 | "fix_ipc_permissions": 511, 47 | "inflight_ops": 100 48 | } 49 | }, 50 | "out_stream": null, 51 | "storage": { 52 | "rocksdb": { 53 | "path": "${DB_PATH:-/tmp/rocksdb}", 54 | "data_expiration_ttl": { 55 | "secs": 3600, 56 | "nanos": 0 57 | }, 58 | "disable_wal": false, 59 | "max_total_wal_size": 1073741824, 60 | "compaction_style": "universal", 61 | "max_log_file_size": 0, 62 | "keep_log_file_num": 1000 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /python/match_query/query.py: -------------------------------------------------------------------------------- 1 | from savant_rs.match_query import FloatExpression as FE 2 | from savant_rs.match_query import IntExpression as IE 3 | from savant_rs.match_query import MatchQuery as Q 4 | from savant_rs.match_query import StringExpression as SE 5 | from savant_rs.primitives.geometry import RBBox 6 | from savant_rs.utils import BBoxMetricType 7 | 8 | and_ = Q.and_ 9 | or_ = Q.or_ 10 | not_ = Q.not_ 11 | 12 | gt = IE.gt 13 | lt = IE.lt 14 | eq = IE.eq 15 | fgt = FE.gt 16 | 17 | q = and_( 18 | Q.stop_if_false(Q.frame_width(IE.le(1280))), 19 | Q.eval( 20 | """!is_empty(id) || id == 13 || label == "hello" || namespace == "where" """ 21 | ), 22 | Q.namespace(SE.one_of("savant", "deepstream")), 23 | Q.label(SE.one_of("person", "cyclist")), 24 | Q.box_metric(RBBox(100.0, 50.0, 20.0, 30.0, 50), BBoxMetricType.IoU, FE.gt(0.5)), 25 | and_( 26 | or_( 27 | not_(Q.parent_defined()), 28 | or_(Q.parent_id(IE.one_of(0, 1, 2)), Q.parent_id(gt(10))), 29 | ) 30 | ), 31 | Q.attributes_jmes_query("[?(name=='test' && namespace=='test')]"), 32 | Q.confidence(FE.gt(0.5)), 33 | Q.box_height(FE.gt(100)), 34 | ) 35 | 36 | print("------------------------") 37 | print("Condensed JSON:") 38 | print("------------------------") 39 | print(q.json) 40 | 41 | print("------------------------") 42 | print("Pretty JSON:") 43 | print("------------------------") 44 | print(q.json_pretty) 45 | 46 | print("------------------------") 47 | print("YAML:") 48 | print("------------------------") 49 | print(q.yaml) 50 | 51 | q2 = Q.from_json(q.json) 52 | assert q.json == q2.json 53 | 54 | q3 = Q.from_yaml(q.yaml) 55 | assert q3.yaml == q.yaml 56 | -------------------------------------------------------------------------------- /savant_deepstream/deepstream/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Safe Rust API for NVIDIA DeepStream 2 | //! 3 | //! This crate provides safe, idiomatic Rust wrappers around the DeepStream C API, 4 | //! particularly focusing on metadata structures like `NvDsObjectMeta`. 5 | //! 6 | //! # Example 7 | //! 8 | //! ```rust 9 | //! use deepstream::ObjectMeta; 10 | //! 11 | //! // Note: ObjectMeta is a wrapper around existing DeepStream metadata 12 | //! // You would typically get it from a frame or batch, not create it directly 13 | //! // For demonstration purposes, this shows how to work with an existing instance: 14 | //! 15 | //! // Assuming you have a raw pointer to NvDsObjectMeta 16 | //! // let raw_ptr: *mut deepstream_sys::NvDsObjectMeta = /* ... */; 17 | //! // let mut obj_meta = unsafe { ObjectMeta::from_raw(raw_ptr)? }; 18 | //! 19 | //! // Set properties 20 | //! // obj_meta.set_class_id(0); 21 | //! // obj_meta.set_object_id(123); 22 | //! // obj_meta.set_confidence(0.95); 23 | //! 24 | //! // Set bounding box 25 | //! // obj_meta.set_bbox(100.0, 200.0, 300.0, 400.0); 26 | //! ``` 27 | 28 | pub mod batch_meta; 29 | pub mod error; 30 | pub mod frame_meta; 31 | pub mod object_meta; 32 | pub mod rect_params; 33 | pub mod types; 34 | pub mod user_meta; 35 | 36 | pub use batch_meta::BatchMeta; 37 | pub use error::DeepStreamError; 38 | pub use frame_meta::FrameMeta; 39 | pub use object_meta::ObjectMeta; 40 | pub use rect_params::RectParams; 41 | pub use types::ColorParams; 42 | pub use user_meta::UserMeta; 43 | 44 | /// Result type for DeepStream operations 45 | pub type Result = std::result::Result; 46 | 47 | /// Re-export the raw sys crate for advanced usage 48 | pub use deepstream_sys as sys; 49 | -------------------------------------------------------------------------------- /services/retina_rtsp/assets/configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "sink": { 3 | "url": "pub+connect:tcp://127.0.0.1:3333", 4 | "options": { 5 | "send_timeout": { 6 | "secs": 1, 7 | "nanos": 0 8 | }, 9 | "send_retries": 3, 10 | "receive_timeout": { 11 | "secs": 1, 12 | "nanos": 0 13 | }, 14 | "receive_retries": 3, 15 | "send_hwm": 1000, 16 | "receive_hwm": 1000, 17 | "inflight_ops": 100 18 | } 19 | }, 20 | "rtsp_sources": { 21 | "group0": { 22 | "sources": [ 23 | { 24 | "source_id": "city-traffic", 25 | "url": "rtsp://hello.savant.video:8554/stream/city-traffic", 26 | "options": ${OPTIONS:-null} 27 | }, 28 | { 29 | "source_id": "town-centre", 30 | "url": "rtsp://hello.savant.video:8554/stream/town-centre", 31 | "options": ${OPTIONS:-null} 32 | } 33 | ], 34 | "rtcp_sr_sync": { 35 | "group_window_duration": { 36 | "secs": 5, 37 | "nanos": 0 38 | }, 39 | "batch_duration": { 40 | "secs": 0, 41 | "nanos": 100000000 42 | }, 43 | "network_skew_correction": false, 44 | "rtcp_once": false 45 | } 46 | } 47 | }, 48 | "reconnect_interval": { 49 | "secs": 5, 50 | "nanos": 0 51 | } 52 | } -------------------------------------------------------------------------------- /savant_core_py/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod atomic_counter; 2 | pub mod capi; 3 | /// The draw specification used to draw objects on the frame when they are visualized. 4 | pub mod draw_spec; 5 | pub mod gst; 6 | pub mod logging; 7 | pub mod match_query; 8 | pub mod metrics; 9 | pub mod pipeline; 10 | /// # Basic objects 11 | /// 12 | pub mod primitives; 13 | pub mod telemetry; 14 | pub mod test; 15 | /// # Utility functions 16 | /// 17 | pub mod utils; 18 | pub mod webserver; 19 | pub mod zmq; 20 | 21 | use pyo3::prelude::*; 22 | 23 | use hashbrown::HashMap; 24 | use lazy_static::lazy_static; 25 | use parking_lot::RwLock; 26 | use pyo3::exceptions::PyValueError; 27 | 28 | /// Returns the version of the package set in Cargo.toml 29 | /// 30 | /// Returns 31 | /// ------- 32 | /// str 33 | /// The version of the package. 34 | /// 35 | #[pyfunction] 36 | pub fn version() -> String { 37 | savant_core::version() 38 | } 39 | 40 | lazy_static! { 41 | pub static ref REGISTERED_HANDLERS: RwLock>> = 42 | RwLock::new(HashMap::new()); 43 | } 44 | 45 | #[pyfunction] 46 | pub fn register_handler(name: &str, handler: Bound<'_, PyAny>) -> PyResult<()> { 47 | let mut handlers = REGISTERED_HANDLERS.write(); 48 | let unbound = handler.unbind(); 49 | handlers.insert(name.to_string(), unbound); 50 | Ok(()) 51 | } 52 | 53 | #[pyfunction] 54 | pub fn unregister_handler(name: &str) -> PyResult<()> { 55 | let mut handlers = REGISTERED_HANDLERS.write(); 56 | let res = handlers.remove(name); 57 | if res.is_none() { 58 | return Err(PyValueError::new_err(format!( 59 | "Handler with name {name} not found" 60 | ))); 61 | } 62 | Ok(()) 63 | } 64 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | export PROJECT_DIR=$(CURDIR) 2 | export PYTHON_VERSION=$(shell python3 -c 'import sys; print(f"cp{sys.version_info.major}{sys.version_info.minor}")') 3 | 4 | .PHONY: docs build_savant build_savant_release clean tests bench reformat 5 | 6 | dev: clean build_savant 7 | release: clean build_savant_release 8 | 9 | install: 10 | @WHL_NAME=$$(ls $(PROJECT_DIR)/dist/*$(PYTHON_VERSION)*.whl); \ 11 | echo "Installing $$WHL_NAME[clientsdk]"; \ 12 | pip install --force-reinstall "$$WHL_NAME[clientsdk]"; \ 13 | echo "Installed $$WHL_NAME[clientsdk]" 14 | 15 | docs: 16 | @echo "Building docs..." 17 | make dev install 18 | cd $(PROJECT_DIR)/docs && make clean html 19 | tar --dereference --hard-dereference --directory $(PROJECT_DIR)/docs/build/html -cvf $(PROJECT_DIR)/docs-artifact.tar . 20 | 21 | build_savant: 22 | @echo "Building..." 23 | utils/build.sh debug 24 | 25 | build_savant_release: 26 | @echo "Building..." 27 | utils/build.sh release 28 | 29 | clean: 30 | @echo "Cleaning..." 31 | rm -rf $(PROJECT_DIR)/dist/*.whl 32 | 33 | pythontests: 34 | @echo "Running tests..." 35 | cd savant_python && cargo build && cargo test --no-default-features -- --test-threads=1 # --show-output --nocapture 36 | 37 | core-tests: 38 | @echo "Running core lib tests..." 39 | cd savant_core && cargo build && cargo test -- --test-threads=1 # --show-output --nocapture 40 | 41 | bench: 42 | @echo "Running benchmarks..." 43 | cd savant_core && cargo bench --no-default-features -- --show-output --nocapture 44 | 45 | 46 | reformat: 47 | unify --in-place --recursive python 48 | unify --in-place --recursive savant_python/python 49 | black python 50 | black savant_python/python 51 | isort python 52 | isort savant_python/python 53 | -------------------------------------------------------------------------------- /docs/source/services/router/index.rst: -------------------------------------------------------------------------------- 1 | Router Service Documentation 2 | ============================= 3 | 4 | Router is a Python-extendable service that processes, modifies, and routes Savant messages based on their labels and conditions. With the router, you can process multiple ingress streams coming from multiple sockets and route them to multiple egress streams based on configurable matching criteria. 5 | 6 | This service is crucial for complex streaming applications requiring conditional processing and routing of streams with many circuits: 7 | 8 | - processes multiple ingress streams simultaneously from different sources; 9 | - routes messages to multiple egress destinations based on label matching conditions; 10 | - provides Python-based extensibility for custom message processing logic; 11 | - supports configurable source and topic mapping for each egress endpoint; 12 | - handles high-throughput message routing with efficient caching mechanisms; 13 | - supports complex boolean logic for message routing decisions; 14 | - can modify message labels, attributes, and metadata in real-time; 15 | - provides backpressure control with configurable high watermarks; 16 | - can work as a central routing hub in distributed Savant pipeline architectures. 17 | 18 | Use Cases 19 | ^^^^^^^^^ 20 | 21 | - Route a stream between multiple processing pipelines based on the stream-pipelines assignment; 22 | - Duplicate a stream to process it by different pipelines simultaneously; 23 | - Load balance streams based on their name hashes among the same pipelines launched on multiple GPUs; 24 | 25 | 26 | .. toctree:: 27 | :maxdepth: 2 28 | :caption: Contents 29 | 30 | 0_introduction 31 | 1_platforms 32 | 2_installation 33 | 3_configuration 34 | -------------------------------------------------------------------------------- /savant_python/python/savant_rs/utils/symbol_mapper/symbol_mapper.pyi: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import Dict, List, Optional, Tuple 3 | 4 | __all__ = [ 5 | "RegistrationPolicy", 6 | "build_model_object_key", 7 | "clear_symbol_maps", 8 | "dump_registry", 9 | "get_model_id", 10 | "get_model_name", 11 | "get_object_id", 12 | "get_object_ids", 13 | "get_object_label", 14 | "get_object_labels", 15 | "is_model_registered", 16 | "is_object_registered", 17 | "parse_compound_key", 18 | "register_model_objects", 19 | "validate_base_key", 20 | ] 21 | 22 | class RegistrationPolicy(Enum): 23 | Override: ... 24 | ErrorIfNonUnique: ... 25 | 26 | def build_model_object_key(model_name: str, object_label: str) -> str: ... 27 | def clear_symbol_maps() -> None: ... 28 | def dump_registry() -> str: ... 29 | def get_model_id(model_name: str) -> int: ... 30 | def get_model_name(model_id: int) -> Optional[str]: ... 31 | def get_object_id(model_name: str, object_label: str) -> Tuple[int, int]: ... 32 | def get_object_ids( 33 | model_name: str, object_labels: List[str] 34 | ) -> List[Tuple[str, Optional[int]]]: ... 35 | def get_object_label(model_id: int, object_id: int) -> Optional[str]: ... 36 | def get_object_labels( 37 | model_id: int, object_ids: List[int] 38 | ) -> List[Tuple[int, Optional[str]]]: ... 39 | def is_model_registered(model_name: str) -> bool: ... 40 | def is_object_registered(model_name: str, object_label: str) -> bool: ... 41 | def parse_compound_key(key: str) -> Tuple[str, str]: ... 42 | def register_model_objects( 43 | model_name: str, elements: Dict[int, str], policy: RegistrationPolicy 44 | ) -> int: ... 45 | def validate_base_key(key: str) -> str: ... 46 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # For the full list of built-in configuration values, see the documentation: 4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 5 | 6 | # -- Project information ----------------------------------------------------- 7 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 8 | 9 | import savant_rs 10 | 11 | project = "savant_rs" 12 | copyright = "2023, Ivan A. Kudriavtsev" 13 | author = "Ivan A. Kudriavtsev" 14 | release = savant_rs.version() 15 | version = release 16 | 17 | # -- General configuration --------------------------------------------------- 18 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 19 | 20 | extensions = [ 21 | "sphinx.ext.autodoc", 22 | "sphinx.ext.autosummary", 23 | "sphinx.ext.intersphinx", 24 | "sphinx.ext.todo", 25 | "sphinx.ext.inheritance_diagram", 26 | "sphinx.ext.autosectionlabel", 27 | "sphinx.ext.napoleon", 28 | "sphinx_rtd_theme", 29 | ] 30 | 31 | autosummary_generate = True 32 | autosummary_imported_members = True 33 | 34 | templates_path = ["_templates"] 35 | exclude_patterns = [] 36 | 37 | html_theme = "sphinx_rtd_theme" 38 | 39 | html_static_path = ["_static"] 40 | 41 | html_css_files = [ 42 | "css/custom.css", 43 | ] 44 | 45 | 46 | # Custom theme options 47 | html_theme_options = { 48 | "style_nav_header_background": "#2980B9", 49 | "style_external_links": True, 50 | "collapse_navigation": True, 51 | "sticky_navigation": True, 52 | "navigation_depth": 4, 53 | "includehidden": True, 54 | "titles_only": False, 55 | } 56 | 57 | suppress_warnings = ["toc.duplicate_entry"] 58 | -------------------------------------------------------------------------------- /savant_core/benches/bench_bbox_utils.rs: -------------------------------------------------------------------------------- 1 | use criterion::{criterion_group, criterion_main, Criterion}; 2 | use rand::Rng; 3 | use savant_core::primitives::utils::solely_owned_areas; 4 | use savant_core::primitives::RBBox; 5 | use std::hint::black_box; 6 | 7 | fn bench_solely_owned_areas(bbox_count: usize, parallel: bool) { 8 | let pos_x_range = 0.0..1920.0; 9 | let pos_y_range = 0.0..1080.0; 10 | let width_range = 50.0..600.0; 11 | let height_range = 50.0..400.0; 12 | let mut rng = rand::rng(); 13 | let bboxes: Vec = (0..bbox_count) 14 | .map(|_| { 15 | RBBox::new( 16 | rng.random_range(pos_x_range.clone()), 17 | rng.random_range(pos_y_range.clone()), 18 | rng.random_range(width_range.clone()), 19 | rng.random_range(height_range.clone()), 20 | Some(0.0), 21 | ) 22 | }) 23 | .collect(); 24 | let bbox_refs = bboxes.iter().collect::>(); 25 | solely_owned_areas(&bbox_refs, parallel); 26 | } 27 | 28 | fn bench_solely_owned_areas_criterion(c: &mut Criterion) { 29 | let mut group = c.benchmark_group("solely_owned_areas"); 30 | 31 | for &bbox_count in &[10, 20, 50] { 32 | group.bench_function(&format!("seq_{:03}", bbox_count), |b| { 33 | b.iter(|| bench_solely_owned_areas(black_box(bbox_count), black_box(false))) 34 | }); 35 | 36 | group.bench_function(&format!("par_{:03}", bbox_count), |b| { 37 | b.iter(|| bench_solely_owned_areas(black_box(bbox_count), black_box(true))) 38 | }); 39 | } 40 | 41 | group.finish(); 42 | } 43 | 44 | criterion_group!(benches, bench_solely_owned_areas_criterion); 45 | criterion_main!(benches); 46 | -------------------------------------------------------------------------------- /savant_core_py/src/webserver.rs: -------------------------------------------------------------------------------- 1 | pub mod kvs; 2 | 3 | use pyo3::exceptions::{PySystemError, PyValueError}; 4 | use pyo3::prelude::*; 5 | use savant_core::webserver::PipelineStatus; 6 | 7 | /// Starts embedded webserver providing status, shutdown and metrics features. 8 | /// 9 | /// Parameters 10 | /// ---------- 11 | /// port : int 12 | /// 13 | #[pyfunction] 14 | pub fn init_webserver(port: u16) -> PyResult<()> { 15 | savant_core::webserver::init_webserver(port) 16 | .map_err(|e| PySystemError::new_err(e.to_string()))?; 17 | Ok(()) 18 | } 19 | 20 | /// Stops the embedded webserver. 21 | /// 22 | #[pyfunction] 23 | pub fn stop_webserver() -> PyResult<()> { 24 | savant_core::webserver::stop_webserver(); 25 | Ok(()) 26 | } 27 | 28 | /// Sets the token to be used to shut down the webserver. 29 | /// 30 | /// Parameters 31 | /// ---------- 32 | /// token : str 33 | /// 34 | #[pyfunction] 35 | pub fn set_shutdown_token(token: String) { 36 | savant_core::webserver::set_shutdown_token(token); 37 | } 38 | 39 | /// Returns the status of the webserver. 40 | /// 41 | /// Returns 42 | /// ------- 43 | /// bool 44 | /// True if the webserver installed shutdown status, False otherwise. 45 | /// 46 | #[pyfunction] 47 | pub fn is_shutdown_set() -> bool { 48 | savant_core::webserver::is_shutdown_set() 49 | } 50 | 51 | #[pyfunction] 52 | pub fn set_status_running() -> PyResult<()> { 53 | savant_core::webserver::set_status(PipelineStatus::Running) 54 | .map_err(|e| PyValueError::new_err(e.to_string())) 55 | } 56 | 57 | #[pyfunction] 58 | pub fn set_shutdown_signal(signal: i32) -> PyResult<()> { 59 | savant_core::webserver::set_shutdown_signal(signal) 60 | .map_err(|e| PyValueError::new_err(e.to_string())) 61 | } 62 | -------------------------------------------------------------------------------- /savant_python/python/savant_rs/py/log/savant_rs_handler.py: -------------------------------------------------------------------------------- 1 | """SavantRsLoggingHandler module.""" 2 | 3 | import logging 4 | 5 | import pretty_traceback 6 | from savant_rs.logging import LogLevel, log 7 | 8 | LOG_LEVEL_PY_TO_RS = { 9 | logging.CRITICAL: LogLevel.Error, 10 | logging.ERROR: LogLevel.Error, 11 | logging.WARNING: LogLevel.Warning, 12 | logging.INFO: LogLevel.Info, 13 | logging.DEBUG: LogLevel.Debug, 14 | } 15 | 16 | 17 | class SavantRsLoggingHandler(logging.Handler): 18 | """Custom logging Handler that passes the log messages 19 | to the rust log from savant_rs with appropriate log level. 20 | 21 | No need to specify formatter during logging configuration: 22 | 1. for all messages under ERROR priority no formatting is performed 23 | (rely on underlying rust log formatter) 24 | 2. pretty_traceback formatter for ERROR and CRITICAL messages 25 | """ 26 | 27 | def __init__(self) -> None: 28 | logging.Handler.__init__(self) 29 | self.pretty_trace_back_threshold = logging.ERROR 30 | self.formatter_pretty_traceback = pretty_traceback.LoggingFormatter() 31 | 32 | def format(self, record: logging.LogRecord) -> str: 33 | """Format a record.""" 34 | if record.levelno < self.pretty_trace_back_threshold: 35 | return record.getMessage() 36 | return self.formatter_pretty_traceback.format(record) 37 | 38 | def emit(self, record: logging.LogRecord): 39 | """Emit a record.""" 40 | try: 41 | formatted = self.format(record) 42 | except Exception: 43 | self.handleError(record) 44 | return 45 | 46 | log_level = LOG_LEVEL_PY_TO_RS[record.levelno] 47 | log(log_level, record.name, formatted) 48 | -------------------------------------------------------------------------------- /savant_python/python/savant_rs/primitives/attribute.pyi: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import List, Optional 3 | 4 | from .attribute_value import AttributeValue, AttributeValueView 5 | 6 | __all__ = [ 7 | "Attribute", 8 | "AttributeUpdatePolicy", 9 | ] 10 | 11 | class Attribute: 12 | values: List[AttributeValue] 13 | 14 | def __init__( 15 | self, 16 | namespace: str, 17 | name: str, 18 | values: List[AttributeValue], 19 | hint: Optional[str], 20 | is_persistent: bool = True, 21 | is_hidden: bool = False, 22 | ): ... 23 | @classmethod 24 | def persistent( 25 | cls, 26 | namespace: str, 27 | name: str, 28 | values: List[AttributeValue], 29 | hint: Optional[str] = None, 30 | is_hidden: bool = False, 31 | ): ... 32 | @classmethod 33 | def temporary( 34 | cls, 35 | namespace: str, 36 | name: str, 37 | values: List[AttributeValue], 38 | hint: Optional[str] = None, 39 | is_hidden: bool = False, 40 | ): ... 41 | def is_temporary(self) -> bool: ... 42 | def is_hidden(self) -> bool: ... 43 | def make_peristent(self): ... 44 | def make_temporary(self): ... 45 | @property 46 | def namespace(self) -> str: ... 47 | @property 48 | def name(self) -> str: ... 49 | @property 50 | def values_view(self) -> AttributeValueView: ... 51 | @property 52 | def hint(self) -> Optional[str]: ... 53 | @property 54 | def json(self) -> str: ... 55 | @classmethod 56 | def from_json(cls, json: str) -> "Attribute": ... 57 | 58 | class AttributeUpdatePolicy(Enum): 59 | ReplaceWithForeignWhenDuplicate: ... 60 | KeepOwnWhenDuplicate: ... 61 | ErrorWhenDuplicate: ... 62 | -------------------------------------------------------------------------------- /python/primitives/bench_obj_add_vs_create.py: -------------------------------------------------------------------------------- 1 | from timeit import timeit 2 | 3 | from savant_rs.primitives import (IdCollisionResolutionPolicy, VideoFrame, 4 | VideoFrameContent, VideoObject) 5 | from savant_rs.primitives.geometry import BBox 6 | 7 | frame = VideoFrame( 8 | source_id="Test", 9 | framerate="30/1", 10 | width=1920, 11 | height=1080, 12 | content=VideoFrameContent.external("s3", "s3://some-bucket/some-key.jpeg"), 13 | codec="jpeg", 14 | keyframe=True, 15 | pts=0, 16 | dts=None, 17 | duration=None, 18 | ) 19 | 20 | 21 | def add_object_fn(frame: VideoFrame): 22 | obj = VideoObject( 23 | id=0, 24 | namespace="some", 25 | label="person", 26 | detection_box=BBox(0.1, 0.2, 0.3, 0.4).as_rbbox(), 27 | confidence=0.5, 28 | attributes=[], 29 | track_id=None, 30 | track_box=None, 31 | ) 32 | frame.add_object(obj, IdCollisionResolutionPolicy.GenerateNewId) 33 | 34 | 35 | print(timeit(lambda: add_object_fn(frame), number=10000)) 36 | 37 | 38 | frame = VideoFrame( 39 | source_id="Test", 40 | framerate="30/1", 41 | width=1920, 42 | height=1080, 43 | content=VideoFrameContent.external("s3", "s3://some-bucket/some-key.jpeg"), 44 | codec="jpeg", 45 | keyframe=True, 46 | pts=0, 47 | dts=None, 48 | duration=None, 49 | ) 50 | 51 | 52 | def create_object_fn(frame: VideoFrame): 53 | frame.create_object( 54 | namespace="some", 55 | label="person", 56 | detection_box=BBox(0.1, 0.2, 0.3, 0.4).as_rbbox(), 57 | confidence=0.5, 58 | attributes=[], 59 | track_id=None, 60 | track_box=None, 61 | ) 62 | 63 | 64 | print(timeit(lambda: create_object_fn(frame), number=10000)) 65 | -------------------------------------------------------------------------------- /savant_python/python/savant_rs/py/client/image_source/img_header_parse.py: -------------------------------------------------------------------------------- 1 | """Image header parse utility.""" 2 | 3 | import re 4 | from os import PathLike 5 | from typing import BinaryIO, Tuple, Union 6 | 7 | import magic 8 | 9 | # positive lookbehind is included to avoid matching density in the JPEG header 10 | PATTERN = re.compile(r'(?<=, )(?P\d+)( x |x)(?P\d+)') 11 | 12 | 13 | def get_image_size_codec(file: Union[str, PathLike, BinaryIO]) -> Tuple[int, int, str]: 14 | """Get JPEG or PNG image width and height by parsing the file header. 15 | 16 | :param file: Path to an image file or a file handle 17 | to an image file opened as binary. 18 | :return: Image width, height and codec. 19 | """ 20 | if hasattr(file, 'read') and hasattr(file, 'seek'): 21 | # read only the first 512 KB of the file 22 | # hoping that the SOF header segment is there 23 | magic_out = magic.from_buffer(file.read(512 * 1024)) 24 | file.seek(0) 25 | elif isinstance(file, (str, PathLike)): 26 | magic_out = magic.from_file(file) 27 | else: 28 | raise ValueError('File path or file handle is expected.') 29 | 30 | # codec str should correspond to savant.gstreamer.codecs.Codec enum values 31 | # can't import directly because of extra dependencies (gstreamer) 32 | # that aren't going to be present in all the adapter images 33 | if magic_out.startswith('JPEG image data'): 34 | codec = 'jpeg' 35 | elif magic_out.startswith('PNG image data'): 36 | codec = 'png' 37 | else: 38 | raise ValueError('Not a JPEG or PNG file.') 39 | 40 | match = PATTERN.search(magic_out) 41 | if match: 42 | return int(match['width']), int(match['height']), codec 43 | raise ValueError('Failed to get image size from image header.') 44 | -------------------------------------------------------------------------------- /utils/services/replay/copy-deps.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -euo pipefail # Strict error handling 3 | 4 | # Define target directories 5 | readonly TARGET_LIB_DIR="/opt/libs" 6 | readonly TARGET_BIN_DIR="/opt/bin" 7 | readonly TARGET_ETC_DIR="/opt/etc" 8 | 9 | # Create directories with specific permissions 10 | install -d -m 755 "${TARGET_LIB_DIR}" 11 | install -d -m 755 "${TARGET_BIN_DIR}" 12 | install -d -m 755 "${TARGET_ETC_DIR}" 13 | 14 | # Find and validate Rust standard library 15 | RUST_STD_LIB=$(find / -name 'libstd-*.so' -type f -print -quit) 16 | if [ -z "${RUST_STD_LIB}" ]; then 17 | echo "Error: Could not find Rust standard library" >&2 18 | exit 1 19 | fi 20 | echo "Rust std lib: ${RUST_STD_LIB}" 21 | 22 | # Copy files with proper permissions and error checking 23 | if ! install -m 644 "${RUST_STD_LIB}" "${TARGET_LIB_DIR}/"; then 24 | echo "Error: Failed to copy Rust standard library" >&2 25 | exit 1 26 | fi 27 | 28 | # Copy dependency libraries 29 | if ! install -m 644 /tmp/build/release/deps/*.so "${TARGET_LIB_DIR}/"; then 30 | echo "Error: Failed to copy dependency libraries" >&2 31 | exit 1 32 | fi 33 | 34 | # Copy binary with executable permissions 35 | if ! install -m 755 /tmp/build/release/replay "${TARGET_BIN_DIR}/"; then 36 | echo "Error: Failed to copy replay binary" >&2 37 | exit 1 38 | fi 39 | 40 | # Copy binary with executable permissions 41 | if ! install -m 755 /tmp/build/release/savant_info "${TARGET_BIN_DIR}/"; then 42 | echo "Error: Failed to copy savant_info binary" >&2 43 | exit 1 44 | fi 45 | 46 | 47 | # Copy configuration file 48 | if ! install -m 644 /opt/savant-rs/services/replay/replay/assets/test.json "${TARGET_ETC_DIR}/config.json"; then 49 | echo "Error: Failed to copy configuration file" >&2 50 | exit 1 51 | fi 52 | 53 | echo "All files copied successfully" 54 | -------------------------------------------------------------------------------- /services/replay/replay/src/web_service.rs: -------------------------------------------------------------------------------- 1 | use actix_web::body::BoxBody; 2 | use actix_web::http::header::ContentType; 3 | use actix_web::{HttpResponse, Responder}; 4 | use serde::Serialize; 5 | use tokio::sync::Mutex; 6 | 7 | use replaydb::job::configuration::JobConfiguration; 8 | use replaydb::job::stop_condition::JobStopCondition; 9 | use replaydb::service::rocksdb_service::RocksDbService; 10 | 11 | pub mod del_job; 12 | pub mod find_keyframes; 13 | pub mod get_keyframe_by_uuid; 14 | pub mod list_jobs; 15 | pub mod new_job; 16 | pub mod shutdown; 17 | pub mod status; 18 | pub mod update_stop_condition; 19 | 20 | pub struct JobService { 21 | pub service: Mutex, 22 | pub shutdown: Mutex, 23 | } 24 | 25 | #[derive(Debug, Serialize)] 26 | enum ResponseMessage { 27 | #[serde(rename = "ok")] 28 | Ok, 29 | #[serde(rename = "jobs")] 30 | ListJobs(Vec<(String, JobConfiguration, JobStopCondition)>), 31 | #[serde(rename = "stopped_jobs")] 32 | ListStoppedJobs(Vec<(String, JobConfiguration, Option)>), 33 | #[serde(rename = "new_job")] 34 | NewJob(String), 35 | #[serde(rename = "keyframes")] 36 | FindKeyframes(String, Vec), 37 | #[serde(rename = "running")] 38 | StatusRunning, 39 | #[serde(rename = "finished")] 40 | StatusFinished, 41 | #[serde(rename = "error")] 42 | Error(String), 43 | } 44 | 45 | impl Responder for ResponseMessage { 46 | type Body = BoxBody; 47 | fn respond_to(self, _req: &actix_web::HttpRequest) -> HttpResponse { 48 | let body = serde_json::to_string(&self).unwrap(); 49 | let mut resp = match self { 50 | ResponseMessage::Error(_) => HttpResponse::InternalServerError(), 51 | _ => HttpResponse::Ok(), 52 | }; 53 | resp.content_type(ContentType::json()).body(body) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /utils/services/retina_rtsp/copy-deps.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -euo pipefail # Strict error handling 3 | 4 | # Define target directories 5 | readonly TARGET_LIB_DIR="/opt/libs" 6 | readonly TARGET_BIN_DIR="/opt/bin" 7 | readonly TARGET_ETC_DIR="/opt/etc" 8 | 9 | # Create directories with specific permissions 10 | install -d -m 755 "${TARGET_LIB_DIR}" 11 | install -d -m 755 "${TARGET_BIN_DIR}" 12 | install -d -m 755 "${TARGET_ETC_DIR}" 13 | 14 | # Find and validate Rust standard library 15 | RUST_STD_LIB=$(find / -name 'libstd-*.so' -type f -print -quit) 16 | if [ -z "${RUST_STD_LIB}" ]; then 17 | echo "Error: Could not find Rust standard library" >&2 18 | exit 1 19 | fi 20 | echo "Rust std lib: ${RUST_STD_LIB}" 21 | 22 | # Copy files with proper permissions and error checking 23 | if ! install -m 644 "${RUST_STD_LIB}" "${TARGET_LIB_DIR}/"; then 24 | echo "Error: Failed to copy Rust standard library" >&2 25 | exit 1 26 | fi 27 | 28 | # Copy dependency libraries 29 | if ! install -m 644 /tmp/build/release/deps/*.so "${TARGET_LIB_DIR}/"; then 30 | echo "Error: Failed to copy dependency libraries" >&2 31 | exit 1 32 | fi 33 | 34 | # Copy binary with executable permissions 35 | if ! install -m 755 /tmp/build/release/retina_rtsp "${TARGET_BIN_DIR}/"; then 36 | echo "Error: Failed to copy retina_rtsp binary" >&2 37 | exit 1 38 | fi 39 | 40 | # Copy binary with executable permissions 41 | if ! install -m 755 /tmp/build/release/savant_info "${TARGET_BIN_DIR}/"; then 42 | echo "Error: Failed to copy savant_info binary" >&2 43 | exit 1 44 | fi 45 | 46 | 47 | # Copy configuration file 48 | if ! install -m 644 /opt/savant-rs/services/retina_rtsp/assets/configuration.json "${TARGET_ETC_DIR}/configuration.json"; then 49 | echo "Error: Failed to copy configuration file" >&2 50 | exit 1 51 | fi 52 | 53 | echo "All files copied successfully" 54 | -------------------------------------------------------------------------------- /docs/source/services/router/1_platforms.rst: -------------------------------------------------------------------------------- 1 | Hardware Requirements 2 | ===================== 3 | 4 | CPU 5 | --- 6 | 7 | Currently, we support two platforms: 8 | 9 | - ARM64 (Nvidia Jetson, Raspberry Pi 4/5, AWS Graviton, etc); 10 | - X86_64 (Intel/AMD CPUs). 11 | 12 | The Router service is optimized for multi-core processors and can efficiently utilize available CPU cores for message processing and routing operations. 13 | 14 | RAM 15 | --- 16 | 17 | The Router service has modest memory requirements due to its efficient Rust implementation. We recommend having at least 512MB of RAM for basic operations. However, memory usage scales with: 18 | 19 | - Number of concurrent ingress and egress connections 20 | - Complexity of Python handlers 21 | - Message throughput and processing volume 22 | - Number of in-flight messages configured for ingress and egress connections 23 | 24 | For high-throughput deployments processing thousands of messages per second, we recommend 2-4GB of RAM to ensure optimal performance with adequate caching. 25 | 26 | Storage 27 | ------- 28 | 29 | Router has minimal storage requirements as it operates as a message routing service without persistent data storage. Any standard storage medium (HDD, SSD, or even SD cards) is sufficient for Router's storage needs. The choice of storage medium does not impact Router's performance since it operates in-memory. 30 | 31 | Network 32 | ------- 33 | 34 | Router is designed for high-throughput network operations and benefits from: 35 | 36 | - Low-latency network connections between ingress sources and egress destinations 37 | - Adequate network bandwidth to handle aggregate message throughput 38 | - Stable network connections to prevent message loss during routing 39 | 40 | The service supports various ZeroMQ transport protocols (TCP and IPC) and can be configured to optimize for different network topologies and requirements. -------------------------------------------------------------------------------- /savant_core/src/primitives.rs: -------------------------------------------------------------------------------- 1 | pub mod attribute; 2 | pub use attribute::*; 3 | pub mod point; 4 | pub use point::*; 5 | pub mod polygonal_area; 6 | pub use polygonal_area::*; 7 | pub mod bbox; 8 | pub use bbox::*; 9 | 10 | pub mod any_object; 11 | pub mod attribute_set; 12 | pub mod attribute_value; 13 | pub mod eos; 14 | pub mod frame; 15 | pub mod frame_batch; 16 | pub mod frame_update; 17 | pub mod object; 18 | pub mod segment; 19 | pub mod shutdown; 20 | pub mod userdata; 21 | 22 | pub use segment::*; 23 | 24 | pub mod rust { 25 | pub use super::attribute::Attribute; 26 | pub use super::attribute_set::AttributeSet; 27 | pub use super::attribute_value::AttributeValue; 28 | pub use super::bbox::BBoxMetricType; 29 | pub use super::bbox::RBBox; 30 | pub use super::bbox::RBBoxData; 31 | pub use super::eos::EndOfStream; 32 | pub use super::frame::BelongingVideoFrame; 33 | pub use super::frame::VideoFrameContent; 34 | pub use super::frame::VideoFrameProxy; 35 | pub use super::frame::VideoFrameTranscodingMethod; 36 | pub use super::frame::VideoFrameTransformation; 37 | pub use super::frame::VideoObjectTree; 38 | pub use super::frame_batch::VideoFrameBatch; 39 | pub use super::frame_update::VideoFrameUpdate; 40 | pub use super::object::BorrowedVideoObject; 41 | pub use super::object::VideoObject; 42 | pub use super::object::VideoObjectBBoxTransformation; 43 | pub use super::object::VideoObjectBuilder; 44 | pub use super::point::Point; 45 | pub use super::polygonal_area::PolygonalArea; 46 | pub use super::segment::Intersection; 47 | pub use super::segment::IntersectionKind; 48 | pub use super::segment::Segment; 49 | pub use super::shutdown::Shutdown; 50 | pub use super::userdata::UserData; 51 | pub use crate::message::Message; 52 | pub use crate::primitives::frame::ExternalFrame; 53 | pub use crate::primitives::object::IdCollisionResolutionPolicy; 54 | } 55 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | # This file is autogenerated by maturin v0.15.1 2 | # To update, run 3 | # 4 | # maturin generate-ci github 5 | # 6 | name: Savant-RS Docs 7 | 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.head_ref }} 10 | cancel-in-progress: true 11 | 12 | on: 13 | push: 14 | branches: 15 | - 'main' 16 | tags: 17 | - '*' 18 | pull_request: 19 | paths: 20 | - 'docs/**' 21 | workflow_dispatch: 22 | 23 | permissions: 24 | contents: read 25 | pages: write 26 | id-token: write 27 | 28 | jobs: 29 | build-docs: 30 | runs-on: 31 | group: native-builders-x86 32 | # if: "startsWith(github.ref, 'refs/tags/')" 33 | steps: 34 | - uses: actions/checkout@v4 35 | - uses: actions-rust-lang/setup-rust-toolchain@v1 36 | with: 37 | toolchain: stable 38 | components: clippy 39 | 40 | - name: Build docs 41 | uses: docker/build-push-action@v5 42 | with: 43 | file: docker/Dockerfile.docs 44 | tags: savant-rs-docs 45 | push: false 46 | load: true 47 | context: . 48 | 49 | - name: Copy docs 50 | run: docker run --rm -v $(pwd)/docs:/tmp savant-rs-docs cp -R /opt/docs-artifact.tar /tmp 51 | 52 | 53 | - name: Upload artifact 54 | uses: actions/upload-artifact@v4 55 | with: 56 | name: github-pages 57 | path: docs/docs-artifact.tar 58 | if-no-files-found: error 59 | 60 | deploy-docs: 61 | if: github.event_name != 'pull_request' 62 | runs-on: ubuntu-latest 63 | needs: build-docs 64 | environment: 65 | name: github-pages 66 | url: ${{ steps.deployment.outputs.page_url }} 67 | 68 | steps: 69 | - name: Setup Pages 70 | uses: actions/configure-pages@v5 71 | 72 | - name: Deploy to GitHub Pages 73 | id: deployment 74 | uses: actions/deploy-pages@v4 75 | 76 | -------------------------------------------------------------------------------- /savant_gstreamer_elements/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod rspy; 2 | pub mod utils; 3 | mod zeromq_src; 4 | 5 | const SAVANT_EOS_EVENT_NAME: &str = "savant-eos"; 6 | const SAVANT_EOS_EVENT_SOURCE_ID_PROPERTY: &str = "source-id"; 7 | 8 | const SAVANT_USERDATA_EVENT_NAME: &str = "savant-userdata"; 9 | const SAVANT_USERDATA_EVENT_DATA_PROPERTY: &str = "data"; 10 | 11 | use gstreamer::prelude::StaticType; 12 | use gstreamer::{glib, FlowError}; 13 | use gstreamer_base::subclass::base_src::CreateSuccess; 14 | // The public Rust wrapper type for our element 15 | glib::wrapper! { 16 | pub struct RsPy(ObjectSubclass) @extends gstreamer::Element, gstreamer::Object; 17 | } 18 | 19 | glib::wrapper! { 20 | pub struct ZeromqSrc(ObjectSubclass) @extends gstreamer_base::PushSrc, gstreamer_base::BaseSrc, gstreamer::Element, gstreamer::Object; 21 | } 22 | 23 | // Registers the type for our element, and then registers in GStreamer under 24 | // the name "rspy" for being able to instantiate it via e.g. 25 | // gstreamer::ElementFactory::make(). 26 | pub fn register(plugin: &gstreamer::Plugin) -> Result<(), glib::BoolError> { 27 | gstreamer::Element::register( 28 | Some(plugin), 29 | "rspy", 30 | gstreamer::Rank::NONE, 31 | RsPy::static_type(), 32 | )?; 33 | gstreamer::Element::register( 34 | Some(plugin), 35 | "zeromq_src", 36 | gstreamer::Rank::NONE, 37 | ZeromqSrc::static_type(), 38 | )?; 39 | Ok(()) 40 | } 41 | 42 | gstreamer::plugin_define!( 43 | savant_gstreamer_elements, 44 | env!("CARGO_PKG_DESCRIPTION"), 45 | register, 46 | env!("CARGO_PKG_VERSION"), 47 | "APACHE-2.0", 48 | env!("CARGO_PKG_NAME"), 49 | env!("CARGO_PKG_NAME"), 50 | env!("CARGO_PKG_REPOSITORY") 51 | ); 52 | 53 | pub type OptionalGstFlowReturn = Option>; 54 | pub type OptionalGstBufferReturn = Option>; 55 | -------------------------------------------------------------------------------- /savant_core/src/rwlock.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | #[derive(Debug, Default)] 4 | pub struct SavantRwLock(parking_lot::RwLock); 5 | 6 | #[derive(Debug, Default, Clone)] 7 | pub struct SavantArcRwLock(pub Arc>); 8 | 9 | impl From>> for SavantArcRwLock { 10 | #[inline] 11 | fn from(arc: Arc>) -> Self { 12 | Self(arc) 13 | } 14 | } 15 | 16 | impl From<&Arc>> for SavantArcRwLock { 17 | #[inline] 18 | fn from(arc: &Arc>) -> Self { 19 | Self(arc.clone()) 20 | } 21 | } 22 | 23 | impl SavantArcRwLock { 24 | #[inline] 25 | pub fn new(v: T) -> Self { 26 | Self(Arc::new(SavantRwLock::new(v))) 27 | } 28 | 29 | #[inline] 30 | pub fn read(&self) -> parking_lot::RwLockReadGuard<'_, T> { 31 | self.0.read() 32 | } 33 | 34 | #[inline] 35 | pub fn read_recursive(&self) -> parking_lot::RwLockReadGuard<'_, T> { 36 | self.0.read_recursive() 37 | } 38 | 39 | #[inline] 40 | pub fn write(&self) -> parking_lot::RwLockWriteGuard<'_, T> { 41 | self.0.write() 42 | } 43 | 44 | // #[inline] 45 | // pub fn into_inner(self) -> T { 46 | // let inner = self.0; 47 | // inner.into_inner() 48 | // } 49 | } 50 | 51 | impl SavantRwLock { 52 | #[inline] 53 | pub fn new(t: T) -> Self { 54 | Self(parking_lot::RwLock::new(t)) 55 | } 56 | 57 | #[inline] 58 | pub fn read(&self) -> parking_lot::RwLockReadGuard<'_, T> { 59 | self.0.read() 60 | } 61 | 62 | #[inline] 63 | pub fn read_recursive(&self) -> parking_lot::RwLockReadGuard<'_, T> { 64 | self.0.read_recursive() 65 | } 66 | 67 | #[inline] 68 | pub fn write(&self) -> parking_lot::RwLockWriteGuard<'_, T> { 69 | self.0.write() 70 | } 71 | 72 | #[inline] 73 | pub fn into_inner(self) -> T { 74 | self.0.into_inner() 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /services/replay/replay/scripts/rest_api/new_job.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | query() { 4 | 5 | ANCHOR_KEYFRAME=$1 6 | 7 | cat < for generated::VideoFrameTranscodingMethod { 5 | fn from(value: &VideoFrameTranscodingMethod) -> Self { 6 | match value { 7 | VideoFrameTranscodingMethod::Copy => generated::VideoFrameTranscodingMethod::Copy, 8 | VideoFrameTranscodingMethod::Encoded => generated::VideoFrameTranscodingMethod::Encoded, 9 | } 10 | } 11 | } 12 | 13 | impl From<&generated::VideoFrameTranscodingMethod> for VideoFrameTranscodingMethod { 14 | fn from(value: &generated::VideoFrameTranscodingMethod) -> Self { 15 | match value { 16 | generated::VideoFrameTranscodingMethod::Copy => VideoFrameTranscodingMethod::Copy, 17 | generated::VideoFrameTranscodingMethod::Encoded => VideoFrameTranscodingMethod::Encoded, 18 | } 19 | } 20 | } 21 | 22 | #[cfg(test)] 23 | mod tests { 24 | use crate::primitives::frame::VideoFrameTranscodingMethod; 25 | use savant_protobuf::generated; 26 | 27 | #[test] 28 | fn test_video_frame_transcoding_method() { 29 | assert_eq!( 30 | VideoFrameTranscodingMethod::Copy, 31 | VideoFrameTranscodingMethod::from(&generated::VideoFrameTranscodingMethod::Copy) 32 | ); 33 | assert_eq!( 34 | VideoFrameTranscodingMethod::Encoded, 35 | VideoFrameTranscodingMethod::from(&generated::VideoFrameTranscodingMethod::Encoded) 36 | ); 37 | assert_eq!( 38 | generated::VideoFrameTranscodingMethod::Copy, 39 | generated::VideoFrameTranscodingMethod::from(&VideoFrameTranscodingMethod::Copy) 40 | ); 41 | assert_eq!( 42 | generated::VideoFrameTranscodingMethod::Encoded, 43 | generated::VideoFrameTranscodingMethod::from(&VideoFrameTranscodingMethod::Encoded) 44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /savant_core_py/src/primitives/object/object_tree.rs: -------------------------------------------------------------------------------- 1 | use pyo3::{exceptions::PyRuntimeError, prelude::*}; 2 | use savant_core::primitives::rust; 3 | 4 | use crate::err_to_pyerr; 5 | 6 | use super::VideoObject; 7 | 8 | #[pyclass] 9 | #[derive(Debug, Clone)] 10 | pub struct VideoObjectTree(pub(crate) rust::VideoObjectTree); 11 | 12 | #[pymethods] 13 | impl VideoObjectTree { 14 | #[classattr] 15 | const __hash__: Option> = None; 16 | 17 | fn __repr__(&self) -> String { 18 | format!("{:?}", &self.0) 19 | } 20 | 21 | fn __str__(&self) -> String { 22 | self.__repr__() 23 | } 24 | 25 | /// Walk the object tree and call the callable for each object. 26 | /// 27 | /// Parameters 28 | /// ---------- 29 | /// callable : Callable 30 | /// A callable that will be called for each object in the tree. The callable signature is 31 | /// ``(object: VideoObject, parent: Optional[VideoObject], result: Optional[Any]) -> Any``. The result is the result 32 | /// of the previous call (upper level in the tree). The callable should return the result of the current 33 | /// call. 34 | /// 35 | /// Returns 36 | /// ------- 37 | /// None 38 | /// 39 | /// Raises 40 | /// ------ 41 | /// PyRuntimeError 42 | /// If the walk fails. 43 | /// 44 | pub fn walk_objects(&self, callable: &Bound<'_, PyAny>) -> PyResult<()> { 45 | let callable = |object: &rust::VideoObject, 46 | parent: Option<&rust::VideoObject>, 47 | result: Option<&Py>| 48 | -> anyhow::Result> { 49 | let current_object = VideoObject(object.clone()); 50 | let parent_object = parent.map(|p| VideoObject(p.clone())); 51 | let result = callable.call1((current_object, parent_object, result))?; 52 | let result = result.unbind(); 53 | Ok(result) 54 | }; 55 | err_to_pyerr!(self.0.walk_objects(callable), PyRuntimeError) 56 | } 57 | } 58 | --------------------------------------------------------------------------------