├── .cargo └── config.toml ├── .dockerignore ├── .github └── workflows │ ├── build.yml │ ├── docs.yml │ ├── service-replay.yml │ ├── service-retina-rtsp.yml │ └── service-router.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── Makefile ├── README.md ├── docker ├── Dockerfile.docs ├── Dockerfile.manylinux_2_28_ARM64 ├── Dockerfile.manylinux_2_28_X64 └── services │ ├── Dockerfile.replay │ ├── Dockerfile.retina_rtsp │ └── Dockerfile.router ├── docs ├── Makefile ├── make.bat └── source │ ├── _static │ └── css │ │ └── custom.css │ ├── conf.py │ ├── index.rst │ ├── savant_rs │ ├── draw_spec.rst │ ├── index.rst │ ├── logging.rst │ ├── match_query.rst │ ├── metrics.rst │ ├── pipeline.rst │ ├── primitives.rst │ ├── primitives_geometry.rst │ ├── utils.rst │ ├── utils_serialization.rst │ ├── utils_symbol_mapper.rst │ ├── webserver.rst │ ├── webserver_kvs.rst │ └── zmq.rst │ └── services │ ├── replay │ ├── 0_introduction.rst │ ├── 1_platforms.rst │ ├── 2_installation.rst │ ├── 3_jobs.rst │ ├── 4_api.rst │ ├── _static │ │ └── replay_usage_diagram.png │ └── index.rst │ └── retina_rtsp │ ├── 0_introduction.rst │ ├── 1_platforms.rst │ ├── 2_installation.rst │ ├── 3_configuration.rst │ └── index.rst ├── python ├── bugs │ └── bug_110.py ├── etcd.py ├── frame_pipeline_evolution.py ├── match_query │ ├── query.py │ └── simple_queries.py ├── metrics.py ├── pipeline.py ├── pipeline_prev_uuid.py ├── pipeline_workload.py ├── primitives │ ├── attribute.py │ ├── attribute_value_type.py │ ├── bbox │ │ ├── bug_091.py │ │ ├── cmp.py │ │ ├── ops.py │ │ ├── savant_scale.py │ │ └── utils.py │ ├── bench_obj_add_vs_create.py │ ├── buf_copy.py │ ├── draw_specification.py │ ├── eos.py │ ├── frame_geometric_transformations.py │ ├── frame_ops.py │ ├── load_save_various_approaches.py │ ├── object_track_id_range.py │ ├── polygon_match.py │ ├── pyobj_filter.py │ ├── shutdown.py │ ├── user_data.py │ ├── vector_view_ops.py │ └── video_frame_update.py ├── requirements.txt ├── run_all.sh ├── utils │ ├── atomic_counter.py │ ├── eval.py │ ├── inc_uuid_v7.py │ ├── relative_uuid_v7.py │ └── symbol_mapper │ │ └── rust_symbol_mapper.py ├── webserver_kvs.py └── zmq │ ├── zmq_native.py │ ├── zmq_reqrep.py │ └── zmq_reqrep_native_async.py ├── requirements.txt ├── savant_core ├── Cargo.toml ├── LICENSE ├── benches │ ├── bench_bbox_utils.rs │ ├── bench_bboxes.rs │ ├── bench_frame_save_load_pb.rs │ ├── bench_json_attrs.rs │ ├── bench_label_filter.rs │ ├── bench_message_save_load.rs │ ├── bench_object_filter.rs │ ├── bench_pipeline.rs │ └── bench_zmq.rs ├── build.rs └── src │ ├── atomic_f32.rs │ ├── deadlock_detection.rs │ ├── draw.rs │ ├── eval_cache.rs │ ├── eval_context.rs │ ├── eval_resolvers.rs │ ├── json_api.rs │ ├── lib.rs │ ├── macros.rs │ ├── match_query.rs │ ├── message.rs │ ├── message │ ├── label_filter.rs │ └── label_filter_parser.rs │ ├── metrics.rs │ ├── metrics │ ├── metric_collector.rs │ └── pipeline_metric_builder.rs │ ├── otlp.rs │ ├── pipeline.rs │ ├── pipeline │ ├── stage.rs │ ├── stage_function_loader.rs │ ├── stage_plugin_sample.rs │ └── stats.rs │ ├── primitives.rs │ ├── primitives │ ├── any_object.rs │ ├── attribute.rs │ ├── attribute_set.rs │ ├── attribute_value.rs │ ├── bbox.rs │ ├── bbox │ │ └── utils.rs │ ├── eos.rs │ ├── frame.rs │ ├── frame_batch.rs │ ├── frame_update.rs │ ├── object.rs │ ├── object │ │ └── object_tree.rs │ ├── point.rs │ ├── polygonal_area.rs │ ├── segment.rs │ ├── shutdown.rs │ └── userdata.rs │ ├── protobuf.rs │ ├── protobuf │ ├── serialize.rs │ └── serialize │ │ ├── attribute.rs │ │ ├── attribute_set.rs │ │ ├── bounding_box.rs │ │ ├── intersection_kind.rs │ │ ├── message_envelope.rs │ │ ├── polygonal_area.rs │ │ ├── user_data.rs │ │ ├── video_frame.rs │ │ ├── video_frame_batch.rs │ │ ├── video_frame_content.rs │ │ ├── video_frame_transcoding_method.rs │ │ ├── video_frame_transformation.rs │ │ ├── video_frame_update.rs │ │ └── video_object.rs │ ├── rwlock.rs │ ├── symbol_mapper.rs │ ├── telemetry.rs │ ├── test.rs │ ├── transport.rs │ ├── transport │ ├── zeromq.rs │ └── zeromq │ │ ├── nonblocking_reader.rs │ │ ├── nonblocking_writer.rs │ │ ├── reader.rs │ │ ├── reader_config.rs │ │ ├── sync_reader.rs │ │ ├── sync_writer.rs │ │ ├── writer.rs │ │ └── writer_config.rs │ ├── utils.rs │ ├── utils │ ├── default_once.rs │ ├── iter.rs │ └── uuid_v7.rs │ ├── webserver.rs │ └── webserver │ ├── kvs.rs │ ├── kvs_handlers.rs │ └── kvs_subscription.rs ├── savant_core_py ├── Cargo.toml ├── README.md ├── build.rs └── src │ ├── atomic_counter.rs │ ├── capi.rs │ ├── capi │ ├── frame.rs │ ├── object.rs │ └── pipeline.rs │ ├── draw_spec.rs │ ├── gst.rs │ ├── lib.rs │ ├── logging.rs │ ├── match_query.rs │ ├── metrics.rs │ ├── pipeline.rs │ ├── primitives.rs │ ├── primitives │ ├── attribute.rs │ ├── attribute_value.rs │ ├── batch.rs │ ├── bbox.rs │ ├── bbox │ │ └── utils.rs │ ├── eos.rs │ ├── frame.rs │ ├── frame_update.rs │ ├── message.rs │ ├── message │ │ ├── loader.rs │ │ └── saver.rs │ ├── object.rs │ ├── object │ │ └── object_tree.rs │ ├── objects_view.rs │ ├── point.rs │ ├── polygonal_area.rs │ ├── pyobject.rs │ ├── segment.rs │ ├── shutdown.rs │ └── user_data.rs │ ├── telemetry.rs │ ├── test.rs │ ├── utils.rs │ ├── utils │ ├── bigint.rs │ ├── byte_buffer.rs │ ├── eval_resolvers.rs │ ├── otlp.rs │ ├── python.rs │ └── symbol_mapper.rs │ ├── webserver.rs │ ├── webserver │ └── kvs.rs │ ├── zmq.rs │ └── zmq │ ├── basic_types.rs │ ├── blocking.rs │ ├── configs.rs │ ├── nonblocking.rs │ └── results.rs ├── savant_gstreamer ├── Cargo.toml └── src │ ├── id_meta.rs │ └── lib.rs ├── savant_gstreamer_elements ├── Cargo.toml ├── build.rs └── src │ ├── lib.rs │ ├── rspy.rs │ ├── utils.rs │ ├── zeromq_src.rs │ └── zeromq_src │ ├── message_handlers.rs │ ├── object_impl.rs │ └── py_functions.rs ├── savant_info ├── Cargo.toml ├── README.md └── src │ └── main.rs ├── savant_launcher ├── Cargo.toml ├── README.md ├── assets │ └── entrypoint.py └── src │ └── main.rs ├── savant_protobuf ├── Cargo.toml ├── LICENSE ├── build.rs └── src │ ├── generated.rs │ ├── lib.rs │ └── savant_rs.proto ├── savant_python ├── Cargo.toml ├── README.md ├── build.rs ├── pyproject.toml ├── python │ └── savant_rs │ │ ├── __init__.py │ │ ├── atomic_counter │ │ ├── __init__.py │ │ └── atomic_counter.pyi │ │ ├── draw_spec │ │ ├── __init__.py │ │ └── draw_spec.pyi │ │ ├── gstreamer │ │ ├── __init__.py │ │ └── gstreamer.pyi │ │ ├── include │ │ └── savant_rs.h │ │ ├── logging │ │ ├── __init__.py │ │ └── logging.pyi │ │ ├── match_query │ │ ├── __init__.py │ │ └── match_query.pyi │ │ ├── metrics │ │ ├── __init__.py │ │ └── metrics.pyi │ │ ├── pipeline │ │ ├── __init__.py │ │ └── pipeline.pyi │ │ ├── primitives │ │ ├── __init__.py │ │ ├── attribute.pyi │ │ ├── attribute_value.pyi │ │ ├── end_of_stream.pyi │ │ ├── geometry │ │ │ ├── __init__.py │ │ │ └── geometry.pyi │ │ ├── shutdown.pyi │ │ ├── user_data.pyi │ │ ├── video_frame.pyi │ │ └── video_object.pyi │ │ ├── py.typed │ │ ├── savant_rs.pyi │ │ ├── telemetry │ │ ├── __init__.py │ │ └── telemetry.pyi │ │ ├── test │ │ ├── __init__.py │ │ └── test.pyi │ │ ├── utils │ │ ├── __init__.py │ │ ├── atomic_counter.pyi │ │ ├── serialization │ │ │ ├── __init__.py │ │ │ └── serialization.pyi │ │ ├── symbol_mapper │ │ │ ├── __init__.py │ │ │ └── symbol_mapper.pyi │ │ └── utils.pyi │ │ ├── webserver │ │ ├── __init__.py │ │ ├── kvs │ │ │ ├── __init__.py │ │ │ └── kvs.pyi │ │ └── webserver.pyi │ │ └── zmq │ │ ├── __init__.py │ │ └── zmq.pyi └── src │ └── lib.rs ├── services ├── common │ ├── Cargo.toml │ └── src │ │ ├── job_writer.rs │ │ ├── job_writer │ │ └── cache.rs │ │ ├── lib.rs │ │ └── source.rs ├── replay │ ├── .cargo │ │ └── config.toml │ ├── LICENSE │ ├── README.md │ ├── replay │ │ ├── Cargo.toml │ │ ├── assets │ │ │ ├── stub_imgs │ │ │ │ └── smpte100_640x360.jpeg │ │ │ └── test.json │ │ ├── scripts │ │ │ ├── ao-rtsp.sh │ │ │ ├── rest_api │ │ │ │ ├── del_job.sh │ │ │ │ ├── find_keyframes.sh │ │ │ │ ├── list_job.sh │ │ │ │ ├── list_jobs.sh │ │ │ │ ├── list_stopped_jobs.sh │ │ │ │ ├── new_job.sh │ │ │ │ └── update_stop_condition.sh │ │ │ └── video-loop.sh │ │ └── src │ │ │ ├── main.rs │ │ │ ├── web_service.rs │ │ │ └── web_service │ │ │ ├── del_job.rs │ │ │ ├── find_keyframes.rs │ │ │ ├── list_jobs.rs │ │ │ ├── new_job.rs │ │ │ ├── shutdown.rs │ │ │ ├── status.rs │ │ │ └── update_stop_condition.rs │ ├── replaydb │ │ ├── Cargo.toml │ │ ├── assets │ │ │ ├── rocksdb.json │ │ │ └── rocksdb_opt_out.json │ │ └── src │ │ │ ├── job.rs │ │ │ ├── job │ │ │ ├── configuration.rs │ │ │ ├── factory.rs │ │ │ ├── query.rs │ │ │ └── stop_condition.rs │ │ │ ├── lib.rs │ │ │ ├── service.rs │ │ │ ├── service │ │ │ ├── configuration.rs │ │ │ └── rocksdb_service.rs │ │ │ ├── store.rs │ │ │ ├── store │ │ │ └── rocksdb.rs │ │ │ └── stream_processor.rs │ └── samples │ │ └── file_restreaming │ │ ├── README.md │ │ ├── assets │ │ └── stub_imgs │ │ │ └── smpte100_1280x720.jpeg │ │ └── replay_config.json ├── retina_rtsp │ ├── Cargo.toml │ ├── LICENSE │ ├── README.md │ ├── assets │ │ ├── configuration.json │ │ ├── configuration_3groups_no_ntp.json │ │ ├── configuration_mediamtx.json │ │ ├── configuration_no_ntp.json │ │ └── empty_configuration.json │ └── src │ │ ├── configuration.rs │ │ ├── main.rs │ │ ├── ntp_sync.rs │ │ ├── service.rs │ │ ├── syncer.rs │ │ └── utils.rs └── router │ ├── Cargo.toml │ ├── LICENSE │ ├── README.md │ ├── assets │ ├── configuration.json │ └── python │ │ ├── module.py │ │ ├── zmq_consumer.py │ │ └── zmq_producer.py │ └── src │ ├── configuration.rs │ ├── egress.rs │ ├── egress_mapper.rs │ ├── ingress.rs │ └── main.rs └── utils ├── build.sh ├── install_protoc.sh └── services ├── docker-deps.sh ├── replay └── copy-deps.sh ├── retina_rtsp └── copy-deps.sh └── router └── copy-deps.sh /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [registries.crates-io] 2 | protocol = "sparse" 3 | 4 | rustflags = "-C prefer-dynamic" 5 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | target 2 | python 3 | .github 4 | .idea 5 | .zed 6 | .vscode 7 | .venv 8 | venv* 9 | dist 10 | docs-artifact.tar 11 | -------------------------------------------------------------------------------- /.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 | - self-hosted 32 | - X64 33 | # if: "startsWith(github.ref, 'refs/tags/')" 34 | steps: 35 | - uses: actions/checkout@v4 36 | - uses: actions-rust-lang/setup-rust-toolchain@v1 37 | with: 38 | toolchain: stable 39 | components: clippy 40 | 41 | - name: Build docs 42 | uses: docker/build-push-action@v5 43 | with: 44 | file: docker/Dockerfile.docs 45 | tags: savant-rs-docs 46 | push: false 47 | load: true 48 | context: . 49 | 50 | - name: Copy docs 51 | run: docker run --rm -v $(pwd)/docs:/tmp savant-rs-docs cp -R /opt/docs-artifact.tar /tmp 52 | 53 | 54 | - name: Upload artifact 55 | uses: actions/upload-artifact@v4 56 | with: 57 | name: github-pages 58 | path: docs/docs-artifact.tar 59 | if-no-files-found: error 60 | 61 | deploy-docs: 62 | if: github.event_name != 'pull_request' 63 | runs-on: ubuntu-latest 64 | needs: build-docs 65 | environment: 66 | name: github-pages 67 | url: ${{ steps.deployment.outputs.page_url }} 68 | 69 | steps: 70 | - name: Setup Pages 71 | uses: actions/configure-pages@v5 72 | 73 | - name: Deploy to GitHub Pages 74 | id: deployment 75 | uses: actions/deploy-pages@v4 76 | 77 | -------------------------------------------------------------------------------- /.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 | # Added by cargo 20 | 21 | /.idea 22 | /dist 23 | /target 24 | /Cargo.lock 25 | /docs/build 26 | /docs/source/generated 27 | 28 | /venv 29 | /venv312 30 | 31 | **/*.pyc 32 | 33 | .zed 34 | .vscode 35 | -------------------------------------------------------------------------------- /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 | pip install --force-reinstall $(PROJECT_DIR)/dist/*$(PYTHON_VERSION)*.whl 11 | 12 | docs: 13 | @echo "Building docs..." 14 | make dev install 15 | cd $(PROJECT_DIR)/docs && make clean html 16 | tar --dereference --hard-dereference --directory $(PROJECT_DIR)/docs/build/html -cvf $(PROJECT_DIR)/docs-artifact.tar . 17 | 18 | build_savant: 19 | @echo "Building..." 20 | utils/build.sh debug 21 | 22 | build_savant_release: 23 | @echo "Building..." 24 | utils/build.sh release 25 | 26 | clean: 27 | @echo "Cleaning..." 28 | rm -rf $(PROJECT_DIR)/dist/*.whl 29 | 30 | pythontests: 31 | @echo "Running tests..." 32 | cd savant_python && cargo build && cargo test --no-default-features -- --test-threads=1 # --show-output --nocapture 33 | 34 | core-tests: 35 | @echo "Running core lib tests..." 36 | cd savant_core && cargo build && cargo test -- --test-threads=1 # --show-output --nocapture 37 | 38 | bench: 39 | @echo "Running benchmarks..." 40 | cd savant_core && cargo bench --no-default-features -- --show-output --nocapture 41 | 42 | 43 | reformat: 44 | unify --in-place --recursive python 45 | unify --in-place --recursive savant_python/python 46 | black python 47 | black savant_python/python 48 | isort python 49 | isort savant_python/python -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docker/Dockerfile.docs: -------------------------------------------------------------------------------- 1 | FROM ghcr.io/insight-platform/py313_rust:v0.0.8 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,rw,source=.,target=/opt/savant-rs \ 15 | /opt/savant-rs/utils/install_protoc.sh 16 | 17 | RUN --mount=type=bind,source=.,target=/opt/savant-rs \ 18 | pip install -r /opt/savant-rs/requirements.txt 19 | 20 | # add rust path to PATH 21 | ENV PATH="/root/.cargo/bin:$PATH" 22 | ENV CARGO_TARGET_DIR=/tmp/build 23 | 24 | RUN --mount=type=cache,target=/root/.cargo/registry \ 25 | --mount=type=cache,target=/tmp/build \ 26 | --mount=type=bind,rw,source=.,target=/opt/savant-rs \ 27 | cd /opt/savant-rs && PYTHON_INTERPRETER=python3.13 make docs && cp docs-artifact.tar ../ 28 | -------------------------------------------------------------------------------- /docker/Dockerfile.manylinux_2_28_ARM64: -------------------------------------------------------------------------------- 1 | FROM ghcr.io/insight-platform/manylinux_2_28_arm64:v0.0.8 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 | -------------------------------------------------------------------------------- /docker/Dockerfile.manylinux_2_28_X64: -------------------------------------------------------------------------------- 1 | FROM ghcr.io/insight-platform/manylinux_2_28_x64:v0.0.8 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 | -------------------------------------------------------------------------------- /docker/services/Dockerfile.replay: -------------------------------------------------------------------------------- 1 | FROM rust:1.87 AS builder 2 | 3 | WORKDIR /opt/replay 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=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 8 | 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 9 | 10 | FROM debian:bookworm-slim AS runner 11 | 12 | RUN --mount=type=bind,source=.,target=/opt/savant-rs bash /opt/savant-rs/utils/services/docker-deps.sh 13 | 14 | COPY --from=builder /opt /opt 15 | 16 | WORKDIR /opt/replay 17 | 18 | ENV LD_LIBRARY_PATH=/opt/libs 19 | ENV DB_PATH=/opt/rocksdb 20 | ENV RUST_LOG=info 21 | 22 | EXPOSE 8080 23 | EXPOSE 5555 24 | EXPOSE 5556 25 | 26 | ENTRYPOINT ["/opt/bin/replay"] 27 | CMD ["/opt/etc/config.json"] 28 | -------------------------------------------------------------------------------- /docker/services/Dockerfile.retina_rtsp: -------------------------------------------------------------------------------- 1 | FROM rust:1.87 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=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 8 | 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 9 | 10 | FROM debian:bookworm-slim AS runner 11 | 12 | RUN --mount=type=bind,source=.,target=/opt/savant-rs bash /opt/savant-rs/utils/services/docker-deps.sh 13 | 14 | COPY --from=builder /opt /opt 15 | 16 | WORKDIR /opt/retina_rtsp 17 | 18 | ENV LD_LIBRARY_PATH=/opt/libs 19 | ENV RUST_LOG=info 20 | 21 | ENTRYPOINT ["/opt/bin/retina_rtsp"] 22 | CMD ["/opt/etc/configuration.json"] 23 | -------------------------------------------------------------------------------- /docker/services/Dockerfile.router: -------------------------------------------------------------------------------- 1 | FROM rust:1.87 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.11 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/match_query.rst: -------------------------------------------------------------------------------- 1 | savant_rs.match_query 2 | ---------------------------- 3 | 4 | .. automodule:: savant_rs.match_query 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/pipeline.rst: -------------------------------------------------------------------------------- 1 | savant_rs.pipeline 2 | -------------------- 3 | 4 | .. automodule:: savant_rs.pipeline 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/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.rst: -------------------------------------------------------------------------------- 1 | savant_rs.utils 2 | --------------- 3 | 4 | .. automodule:: savant_rs.utils 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 | -------------------------------------------------------------------------------- /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/webserver_kvs.rst: -------------------------------------------------------------------------------- 1 | savant_rs.webserver.kvs 2 | ----------------------------- 3 | 4 | .. automodule:: savant_rs.webserver.kvs 5 | :members: 6 | :undoc-members: 7 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /docs/source/services/replay/_static/replay_usage_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insight-platform/savant-rs/240fb8792a4dcfb8470137b55b9586821d28e5c6/docs/source/services/replay/_static/replay_usage_diagram.png -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /docs/source/services/retina_rtsp/0_introduction.rst: -------------------------------------------------------------------------------- 1 | Introduction 2 | ============ 3 | 4 | Retina RTSP is a solution for handling multiple RTSP streams within a single adapter in a reliable and optionally synchronized manner. It provides a robust way to connect to multiple RTSP sources, synchronize their timelines, and stream the video data to Savant pipelines. This service is essential for applications that require precise timing coordination between multiple video sources. 5 | 6 | The service restarts failed RTSP connections automatically, so you do not need to worry about the health of the RTSP streams or rely on external tools to restart the streams. Retina RTSP is a pure Rust adapter that does not use GStreamer or FFmpeg libraries. It is based on `Retina `_ library by Scott Lamb. 7 | 8 | We develop this adapter mostly to work with precise RTSP stream synchronization for multi-camera video analytics. Another options were to use GStreamer or patched FFmpeg. GStreamer has a very fragile RTSP implmentaion and using of patched FFmpeg is also looks difficult because it is a custom build of FFmpeg which is difficult to maintain and extend/fix. 9 | 10 | Nevertheless, the FFmpeg-based RTSP `adapter `_ is a first-class citizen and will be maintained and recommended for use in cases where precise synchronization is not required and the cams are diverse and include different brands and models, so you cannot guarantee that all the cams are supported by Retina RTSP. 11 | 12 | Also, if RTSP streams contain B-frames, Retina RTSP is not an option since the underlying library does not support them. So, before using this adapter, please test that your cameras are normally processed. Nevertheless, we think that this adapter is a future replacement for RTSP processing in Savant pipelines when it comes to working with cameras. 13 | 14 | Core Features 15 | ------------- 16 | 17 | * RTSP Streams Synchronization with RTCP SR protocol messages; 18 | * Multiple RTSP streams handling, which decreases the number of moving parts in the solution; 19 | * Automatic RTSP reconnection; 20 | * Pure-Rust implementation without GStreamer or FFmpeg dependencies; 21 | * Convenient JSON-based configuration. 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /python/bugs/bug_110.py: -------------------------------------------------------------------------------- 1 | from savant_rs.primitives import ( 2 | IdCollisionResolutionPolicy, 3 | VideoFrame, 4 | VideoFrameContent, 5 | VideoObject, 6 | ) 7 | from savant_rs.primitives.geometry import BBox 8 | from savant_rs.utils.serialization import Message, load_message, save_message 9 | 10 | frame = VideoFrame( 11 | source_id="Test", 12 | framerate="30/1", 13 | width=1920, 14 | height=1080, 15 | content=VideoFrameContent.external("s3", "s3://some-bucket/some-key.jpeg"), 16 | codec="jpeg", 17 | keyframe=True, 18 | pts=0, 19 | dts=None, 20 | duration=None, 21 | ) 22 | 23 | frame.add_object( 24 | VideoObject( 25 | id=-1401514819, 26 | namespace="yolov8n", 27 | label="Car", 28 | detection_box=BBox(485, 675, 886, 690).as_rbbox(), 29 | confidence=0.933, 30 | attributes=[], 31 | track_id=None, 32 | track_box=None, 33 | ), 34 | IdCollisionResolutionPolicy.Error, 35 | ) 36 | 37 | frame.add_object( 38 | VideoObject( 39 | id=537435614, 40 | namespace="LPDNet", 41 | label="lpd", 42 | detection_box=BBox(557.58374, 883.9291, 298.5735, 84.460144).as_rbbox(), 43 | confidence=0.39770508, 44 | attributes=[], 45 | track_id=None, 46 | track_box=None, 47 | ), 48 | IdCollisionResolutionPolicy.Error, 49 | ) 50 | 51 | frame.set_parent_by_id(537435614, -1401514819) 52 | 53 | print(frame.get_object(-1401514819)) 54 | print(frame.get_object(537435614)) 55 | 56 | assert len(frame.get_children(-1401514819)) == 1 57 | 58 | m = Message.video_frame(frame) 59 | 60 | s = save_message(m) 61 | new_m = load_message(s) 62 | 63 | frame = new_m.as_video_frame() 64 | frame.source_id = "Test2" 65 | 66 | print(frame.get_object(-1401514819)) 67 | print(frame.get_object(537435614)) 68 | 69 | assert len(frame.get_children(-1401514819)) == 1 70 | 71 | frame2 = frame.copy() 72 | frame2.source_id = "Test3" 73 | 74 | print(frame2.get_object(-1401514819)) 75 | print(frame2.get_object(537435614)) 76 | 77 | assert len(frame2.get_children(-1401514819)) == 1 78 | -------------------------------------------------------------------------------- /python/etcd.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from savant_rs.match_query import ( 4 | EtcdCredentials, 5 | TlsConfig, 6 | register_env_resolver, 7 | register_etcd_resolver, 8 | ) 9 | from savant_rs.utils import eval_expr 10 | 11 | register_env_resolver() 12 | 13 | # if not set env var RUN_ETCD_TESTS=1, skip 14 | if not eval_expr('env("RUN_ETCD_TESTS", 0)') == 0: 15 | print("Skipping etcd tests") 16 | exit(0) 17 | 18 | # read ca from file to string 19 | ca = Path("../../etcd_dynamic_state/assets/certs/ca.crt").read_text() 20 | cert = Path("../../etcd_dynamic_state/assets/certs/client.crt").read_text() 21 | key = Path("../../etcd_dynamic_state/assets/certs/client.key").read_text() 22 | 23 | conf = TlsConfig( 24 | ca, 25 | cert, 26 | key, 27 | ) 28 | 29 | creds = EtcdCredentials("root", "qwerty") 30 | 31 | register_etcd_resolver( 32 | hosts=["https://127.0.0.1:2379"], credentials=creds, tls_config=conf, watch_path="" 33 | ) 34 | 35 | print(eval_expr('etcd("foo/bar", "default")')) 36 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /python/pipeline_prev_uuid.py: -------------------------------------------------------------------------------- 1 | import time 2 | from threading import Thread, current_thread 3 | 4 | import savant_plugin_sample 5 | import savant_rs 6 | from savant_rs.logging import LogLevel, log, log_level_enabled, set_log_level 7 | from savant_rs.pipeline import ( 8 | StageFunction, 9 | VideoPipeline, 10 | VideoPipelineConfiguration, 11 | VideoPipelineStagePayloadType, 12 | ) 13 | from savant_rs.primitives import AttributeValue 14 | 15 | set_log_level(LogLevel.Trace) 16 | 17 | from savant_rs.utils import TelemetrySpan, enable_dl_detection, gen_frame 18 | 19 | if __name__ == "__main__": 20 | savant_rs.savant_rs.version() 21 | enable_dl_detection() # enables internal DL detection (checks every 5 secs) 22 | log( 23 | LogLevel.Info, 24 | "root", 25 | "Begin operation", 26 | dict(savant_rs_version=savant_rs.version()), 27 | ) 28 | 29 | # from savant_rs import init_jaeger_tracer 30 | # init_jaeger_tracer("demo-pipeline", "localhost:6831") 31 | 32 | conf = VideoPipelineConfiguration() 33 | conf.append_frame_meta_to_otlp_span = True 34 | conf.frame_period = 1 # every single frame, insane 35 | conf.timestamp_period = 1000 # every sec 36 | 37 | p = VideoPipeline( 38 | "video-pipeline-root", 39 | [ 40 | ( 41 | "input", 42 | VideoPipelineStagePayloadType.Frame, 43 | StageFunction.none(), 44 | StageFunction.none(), 45 | ), 46 | ], 47 | conf, 48 | ) 49 | p.sampling_period = 10 50 | 51 | assert p.get_stage_type("input") == VideoPipelineStagePayloadType.Frame 52 | frame1 = gen_frame() 53 | frame1.keyframe = True 54 | frame1.source_id = "test1" 55 | frame_id1 = p.add_frame("input", frame1) 56 | frame1, _ = p.get_independent_frame(frame_id1) 57 | assert frame1.previous_keyframe_uuid is None 58 | uuid = frame1.uuid 59 | 60 | frame2 = gen_frame() 61 | frame2.keyframe = False 62 | frame2.source_id = "test1" 63 | frame_id2 = p.add_frame("input", frame2) 64 | frame2, _ = p.get_independent_frame(frame_id2) 65 | assert frame2.previous_keyframe_uuid == uuid 66 | 67 | del p 68 | -------------------------------------------------------------------------------- /python/primitives/attribute.py: -------------------------------------------------------------------------------- 1 | from savant_rs.primitives import Attribute, AttributeValue 2 | 3 | attr = Attribute( 4 | namespace="some", 5 | name="attr", 6 | hint="x", 7 | values=[ 8 | AttributeValue.bytes(dims=[8, 3, 8, 8], blob=bytes(3 * 8 * 8), confidence=None), 9 | AttributeValue.bytes_from_list(dims=[4, 1], blob=[0, 1, 2, 3], confidence=None), 10 | AttributeValue.integer(1, confidence=0.5), 11 | AttributeValue.float(1.0, confidence=0.5), 12 | ], 13 | ) 14 | print(attr.json) 15 | 16 | vals = attr.values 17 | 18 | view = attr.values_view 19 | print(len(view)) 20 | print(view[2]) 21 | 22 | attr2 = Attribute.from_json(attr.json) 23 | print(attr2.json) 24 | 25 | x = dict(x=5) 26 | temp_py_attr = Attribute( 27 | namespace="some", 28 | name="attr", 29 | hint="x", 30 | values=[AttributeValue.temporary_python_object(x)], 31 | ) 32 | 33 | x["y"] = 6 34 | 35 | o = temp_py_attr.values[0].as_temporary_python_object() 36 | print(o) 37 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /python/primitives/bbox/utils.py: -------------------------------------------------------------------------------- 1 | from savant_rs.primitives.geometry import RBBox, associate_bboxes, solely_owned_areas 2 | from savant_rs.utils import BBoxMetricType 3 | 4 | red = RBBox.ltrb(0.0, 2.0, 2.0, 4.0) 5 | green = RBBox.ltrb(1.0, 3.0, 5.0, 5.0) 6 | yellow = RBBox.ltrb(1.0, 1.0, 3.0, 6.0) 7 | purple = RBBox.ltrb(4.0, 0.0, 7.0, 2.0) 8 | 9 | areas = solely_owned_areas([red, green, yellow, purple], parallel=True) 10 | 11 | red = areas[0] 12 | green = areas[1] 13 | yellow = areas[2] 14 | purple = areas[3] 15 | 16 | assert red == 2.0 17 | assert green == 4.0 18 | assert yellow == 5.0 19 | assert purple == 6.0 20 | 21 | lp1 = RBBox.ltrb(0.0, 1.0, 2.0, 2.0) 22 | lp2 = RBBox.ltrb(5.0, 2.0, 8.0, 3.0) 23 | lp3 = RBBox.ltrb(100.0, 0.0, 6.0, 3.0) 24 | owner1 = RBBox.ltrb(1.0, 0.0, 6.0, 3.0) 25 | owner2 = RBBox.ltrb(6.0, 1.0, 9.0, 4.0) 26 | 27 | associations_iou = associate_bboxes( 28 | [lp1, lp2, lp3], [owner1, owner2], BBoxMetricType.IoU, 0.01 29 | ) 30 | 31 | lp1_associations = associations_iou[0] 32 | lp2_associations = associations_iou[1] 33 | lp3_associations = associations_iou[2] 34 | 35 | assert list(map(lambda t: t[0], lp1_associations)) == [0] 36 | assert list(map(lambda t: t[0], lp2_associations)) == [1, 0] 37 | assert lp3_associations == [] 38 | -------------------------------------------------------------------------------- /python/primitives/bench_obj_add_vs_create.py: -------------------------------------------------------------------------------- 1 | from timeit import timeit 2 | 3 | from savant_rs.primitives import ( 4 | IdCollisionResolutionPolicy, 5 | VideoFrame, 6 | VideoFrameContent, 7 | VideoObject, 8 | ) 9 | from savant_rs.primitives.geometry import BBox 10 | 11 | frame = VideoFrame( 12 | source_id="Test", 13 | framerate="30/1", 14 | width=1920, 15 | height=1080, 16 | content=VideoFrameContent.external("s3", "s3://some-bucket/some-key.jpeg"), 17 | codec="jpeg", 18 | keyframe=True, 19 | pts=0, 20 | dts=None, 21 | duration=None, 22 | ) 23 | 24 | 25 | def add_object_fn(frame: VideoFrame): 26 | obj = VideoObject( 27 | id=0, 28 | namespace="some", 29 | label="person", 30 | detection_box=BBox(0.1, 0.2, 0.3, 0.4).as_rbbox(), 31 | confidence=0.5, 32 | attributes=[], 33 | track_id=None, 34 | track_box=None, 35 | ) 36 | frame.add_object(obj, IdCollisionResolutionPolicy.GenerateNewId) 37 | 38 | 39 | print(timeit(lambda: add_object_fn(frame), number=10000)) 40 | 41 | 42 | frame = VideoFrame( 43 | source_id="Test", 44 | framerate="30/1", 45 | width=1920, 46 | height=1080, 47 | content=VideoFrameContent.external("s3", "s3://some-bucket/some-key.jpeg"), 48 | codec="jpeg", 49 | keyframe=True, 50 | pts=0, 51 | dts=None, 52 | duration=None, 53 | ) 54 | 55 | 56 | def create_object_fn(frame: VideoFrame): 57 | frame.create_object( 58 | namespace="some", 59 | label="person", 60 | detection_box=BBox(0.1, 0.2, 0.3, 0.4).as_rbbox(), 61 | confidence=0.5, 62 | attributes=[], 63 | track_id=None, 64 | track_box=None, 65 | ) 66 | 67 | 68 | print(timeit(lambda: create_object_fn(frame), number=10000)) 69 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /python/primitives/draw_specification.py: -------------------------------------------------------------------------------- 1 | from savant_rs.draw_spec import ( 2 | BoundingBoxDraw, 3 | ColorDraw, 4 | DotDraw, 5 | LabelDraw, 6 | LabelPosition, 7 | LabelPositionKind, 8 | ObjectDraw, 9 | PaddingDraw, 10 | ) 11 | 12 | spec = ObjectDraw( 13 | bounding_box=BoundingBoxDraw( 14 | border_color=ColorDraw(red=100, blue=50, green=50, alpha=100), 15 | background_color=ColorDraw(red=0, blue=50, green=50, alpha=100), 16 | thickness=2, 17 | padding=PaddingDraw(left=5, top=5, right=5, bottom=5), 18 | ), 19 | label=LabelDraw( 20 | font_color=ColorDraw(red=100, blue=50, green=50, alpha=100), 21 | border_color=ColorDraw(red=100, blue=50, green=50, alpha=100), 22 | background_color=ColorDraw(red=0, blue=50, green=50, alpha=100), 23 | padding=PaddingDraw(left=5, top=5, right=5, bottom=5), 24 | position=LabelPosition( 25 | position=LabelPositionKind.TopLeftOutside, margin_x=0, margin_y=-20 26 | ), 27 | font_scale=2.5, 28 | thickness=2, 29 | format=["{model}", "{label}", "{confidence}", "{track_id}"], 30 | ), 31 | central_dot=DotDraw( 32 | color=ColorDraw(red=100, blue=50, green=50, alpha=100), radius=2 33 | ), 34 | blur=False, 35 | ) 36 | 37 | print(spec.bounding_box.border_color.rgba) 38 | print(spec.bounding_box.background_color.rgba) 39 | print(spec) 40 | 41 | spec = ObjectDraw( 42 | bounding_box=None, 43 | label=None, 44 | central_dot=DotDraw( 45 | color=ColorDraw(red=100, blue=50, green=50, alpha=100), radius=2 46 | ), 47 | blur=True, 48 | ) 49 | 50 | print(spec) 51 | 52 | spec = ObjectDraw( 53 | bounding_box=BoundingBoxDraw( 54 | border_color=ColorDraw(red=100, blue=50, green=50, alpha=100), 55 | ) 56 | ) 57 | 58 | new_spec = ObjectDraw( 59 | bounding_box=spec.bounding_box, 60 | label=spec.label, 61 | central_dot=spec.central_dot, 62 | blur=spec.blur, 63 | ) 64 | 65 | print(new_spec) 66 | 67 | spec = ObjectDraw(blur=True) 68 | 69 | new_spec = spec.copy() 70 | print(new_spec) 71 | -------------------------------------------------------------------------------- /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, save_message_to_bytes 4 | 5 | print("Savant version:", version()) 6 | 7 | e = EndOfStream("abc") 8 | 9 | m = e.to_message() 10 | s = save_message_to_bytes(m) 11 | new_m = load_message_from_bytes(s) 12 | assert new_m.is_end_of_stream() 13 | 14 | e = new_m.as_end_of_stream() 15 | assert e.source_id == "abc" 16 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 ( 5 | Message, 6 | load_message, 7 | load_message_from_bytebuffer, 8 | load_message_from_bytes, 9 | save_message, 10 | save_message_to_bytebuffer, 11 | save_message_to_bytes, 12 | ) 13 | 14 | f = gen_frame() 15 | m = Message.video_frame(f) 16 | t = timer() 17 | for _ in range(1_000): 18 | s = save_message(m) 19 | new_m = load_message(s) 20 | assert new_m.is_video_frame() 21 | 22 | print("Regular Save/Load", timer() - t) 23 | 24 | t = timer() 25 | for _ in range(1_000): 26 | s = save_message_to_bytebuffer(m, with_hash=False) 27 | new_m = load_message_from_bytebuffer(s) 28 | assert new_m.is_video_frame() 29 | 30 | print("ByteBuffer (no hash) Save/Load", timer() - t) 31 | 32 | t = timer() 33 | for _ in range(1_000): 34 | s = save_message_to_bytebuffer(m, with_hash=True) 35 | new_m = load_message_from_bytebuffer(s) 36 | assert new_m.is_video_frame() 37 | 38 | print("ByteBuffer (with hash) Save/Load", timer() - t) 39 | 40 | t = timer() 41 | for _ in range(1_000): 42 | s = save_message_to_bytes(m) 43 | new_m = load_message_from_bytes(s) 44 | assert new_m.is_video_frame() 45 | 46 | print("Python bytes Save/Load", timer() - t) 47 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /python/primitives/polygon_match.py: -------------------------------------------------------------------------------- 1 | from pprint import pprint 2 | from timeit import default_timer as timer 3 | 4 | from savant_rs.primitives.geometry import ( 5 | IntersectionKind, 6 | Point, 7 | PolygonalArea, 8 | Segment, 9 | ) 10 | 11 | area = PolygonalArea( 12 | [Point(-1, 1), Point(1, 1), Point(1, -1), Point(-1, -1)], ["up", None, "down", None] 13 | ) 14 | assert area.is_self_intersecting() == False 15 | 16 | bad_area = PolygonalArea( 17 | [Point(-1, -1), Point(1, 1), Point(1, -1), Point(-1, 1)], ["up", None, "down", None] 18 | ) 19 | assert bad_area.is_self_intersecting() == True 20 | 21 | bad_area2 = PolygonalArea( 22 | [Point(-1, -1), Point(1, 1), Point(1, 1), Point(-1, 1)], ["up", None, "down", None] 23 | ) 24 | assert bad_area2.is_self_intersecting() == True 25 | 26 | good_area2 = PolygonalArea( 27 | [Point(-1, -1), Point(1, 1), Point(-1, 1)], ["up", None, "down"] 28 | ) 29 | assert good_area2.is_self_intersecting() == False 30 | 31 | crosses_031 = Segment(Point(-2, 1), Point(2, 1)) 32 | crosses_013 = Segment(Point(2, 1), Point(-2, 1)) 33 | crosses_31 = Segment(Point(-2, 0), Point(2, 0)) 34 | crosses_20 = Segment(Point(0, -2), Point(0, 2)) 35 | leaves_vertex = Segment(Point(0, 0), Point(2, 2)) 36 | crosses_vertices = Segment(Point(-2, -2), Point(2, 2)) 37 | crosses_whole_edge = Segment(Point(-2, 1), Point(2, 1)) 38 | enters_vertex = Segment(Point(2, 2), Point(0, 0)) 39 | outside = Segment(Point(-2, 2), Point(2, 2)) 40 | inside = Segment(Point(-0.5, -0.5), Point(0.5, 0.5)) 41 | 42 | l = [ 43 | crosses_31, 44 | crosses_20, 45 | crosses_031, 46 | crosses_013, 47 | leaves_vertex, 48 | crosses_vertices, 49 | crosses_whole_edge, 50 | enters_vertex, 51 | outside, 52 | inside, 53 | ] 54 | 55 | t = timer() 56 | 57 | res = None 58 | for _ in range(10_0): 59 | res = area.crossed_by_segments(l) 60 | 61 | print("Spent", timer() - t) 62 | pprint(list(zip(l, res))) 63 | 64 | r = res[0] 65 | assert r.kind == IntersectionKind.Cross 66 | assert r.edges == [(3, None), (1, None)] 67 | 68 | r = res[1] 69 | assert r.kind == IntersectionKind.Cross 70 | assert r.edges == [(2, "down"), (0, "up")] 71 | 72 | r = res[2] 73 | assert r.kind == IntersectionKind.Cross 74 | assert r.edges == [(0, "up"), (3, None), (1, None)] 75 | 76 | r = res[3] 77 | assert r.kind == IntersectionKind.Cross 78 | print(r.edges) 79 | assert r.edges == [(1, None), (0, "up"), (3, None)] 80 | -------------------------------------------------------------------------------- /python/primitives/shutdown.py: -------------------------------------------------------------------------------- 1 | from savant_rs.primitives import Shutdown 2 | from savant_rs.utils.serialization import ( 3 | Message, 4 | load_message_from_bytes, 5 | save_message_to_bytes, 6 | ) 7 | 8 | e = Shutdown("abc") 9 | 10 | m = Message.shutdown(e) 11 | s = save_message_to_bytes(m) 12 | new_m = load_message_from_bytes(s) 13 | assert new_m.is_shutdown() 14 | 15 | e = new_m.as_shutdown() 16 | assert e.auth == "abc" 17 | -------------------------------------------------------------------------------- /python/primitives/user_data.py: -------------------------------------------------------------------------------- 1 | from savant_rs.primitives import AttributeValue, UserData 2 | from savant_rs.utils.serialization import ( 3 | Message, 4 | load_message_from_bytes, 5 | save_message_to_bytes, 6 | ) 7 | 8 | t = UserData("abc") 9 | t.set_persistent_attribute( 10 | namespace="some", 11 | name="attr", 12 | hint="x", 13 | is_hidden=False, 14 | values=[AttributeValue.float(1.0, confidence=0.5)], 15 | ) 16 | 17 | pb = t.to_protobuf() 18 | restored = UserData.from_protobuf(pb) 19 | assert t.json == restored.json 20 | 21 | print("Before") 22 | print(t.json_pretty) 23 | 24 | m = Message.user_data(t) 25 | s = save_message_to_bytes(m) 26 | new_m = load_message_from_bytes(s) 27 | assert new_m.is_user_data() 28 | 29 | t = new_m.as_user_data() 30 | assert t.source_id == "abc" 31 | 32 | print("After") 33 | print(t.json_pretty) 34 | -------------------------------------------------------------------------------- /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/video_frame_update.py: -------------------------------------------------------------------------------- 1 | from savant_rs.match_query import MatchQuery as Q 2 | from savant_rs.primitives import ( 3 | AttributeUpdatePolicy, 4 | ObjectUpdatePolicy, 5 | VideoFrameUpdate, 6 | ) 7 | from savant_rs.utils import gen_frame 8 | from savant_rs.utils.serialization import Message, load_message, save_message 9 | 10 | frame = gen_frame() 11 | update = VideoFrameUpdate() 12 | 13 | update.object_policy = ObjectUpdatePolicy.AddForeignObjects 14 | update.frame_attribute_policy = AttributeUpdatePolicy.ReplaceWithForeignWhenDuplicate 15 | update.object_attribute_policy = AttributeUpdatePolicy.ReplaceWithForeignWhenDuplicate 16 | 17 | objects = frame.access_objects(Q.idle()) 18 | 19 | for o in objects: 20 | update.add_object(o.detached_copy(), None) 21 | 22 | attributes = frame.attributes 23 | 24 | for namespace, label in attributes: 25 | attr = frame.get_attribute(namespace, label) 26 | update.add_frame_attribute(attr) 27 | 28 | print(update.json) 29 | print(update.json_pretty) 30 | 31 | pb = update.to_protobuf() 32 | restored = VideoFrameUpdate.from_protobuf(pb) 33 | assert update.json == restored.json 34 | 35 | m = Message.video_frame_update(update) 36 | binary = save_message(m) 37 | m2 = load_message(binary) 38 | -------------------------------------------------------------------------------- /python/requirements.txt: -------------------------------------------------------------------------------- 1 | requests ~= 2.32 2 | numpy ~= 2.2 3 | pyzmq ~= 26.2 4 | maturin ~= 1.8 5 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/utils/eval.py: -------------------------------------------------------------------------------- 1 | from savant_rs.match_query import register_env_resolver, register_utility_resolver 2 | from savant_rs.utils import eval_expr 3 | 4 | register_env_resolver() 5 | register_utility_resolver() 6 | 7 | print(eval_expr("1 + 1")) 8 | 9 | print(eval_expr("""p = env("PATH", ""); (is_string(p), p)""")) # uncached 10 | print(eval_expr("""p = env("PATH", ""); (is_string(p), p)""")) # cached 11 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 ( 8 | BlockingReader, 9 | BlockingWriter, 10 | ReaderConfigBuilder, 11 | WriterConfigBuilder, 12 | ) 13 | 14 | set_log_level(LogLevel.Info) 15 | 16 | socket_name = "tcp://127.0.0.1:3333" 17 | 18 | NUMBER = 1000 19 | BLOCK_SIZE = 1024 * 1024 20 | 21 | 22 | def server(): 23 | reader_config = ReaderConfigBuilder("router+connect:" + socket_name).build() 24 | reader = BlockingReader(reader_config) 25 | reader.start() 26 | reader.blacklist_source(b"unused-topic") 27 | assert reader.is_blacklisted(b"unused-topic") 28 | wait_time = 0 29 | for _ in range(NUMBER): 30 | wait = time() 31 | m = reader.receive() 32 | wait_time += time() - wait 33 | assert len(m.data(0)) == BLOCK_SIZE 34 | print("Reader time awaited", wait_time) 35 | 36 | 37 | frame = gen_frame() 38 | p1 = Thread(target=server) 39 | p1.start() 40 | 41 | buf = bytes(BLOCK_SIZE) 42 | 43 | # test late start up for bind socket 44 | sleep(0.1) 45 | 46 | writer_config = WriterConfigBuilder("dealer+bind:" + socket_name).build() 47 | writer = BlockingWriter(writer_config) 48 | writer.start() 49 | 50 | start = time() 51 | wait_time = 0 52 | for _ in range(NUMBER): 53 | m = Message.video_frame(frame) 54 | wait = time() 55 | writer.send_message("topic", m, buf) 56 | wait_time += time() - wait 57 | 58 | print("Writer time taken", time() - start, "awaited", wait_time) 59 | p1.join() 60 | -------------------------------------------------------------------------------- /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 ( 7 | Message, 8 | load_message_from_bytes, 9 | save_message_to_bytes, 10 | ) 11 | 12 | socket_name = "ipc:///tmp/test_hello" 13 | 14 | NUMBER = 1000 15 | BLOCK_SIZE = 1024 * 1024 16 | 17 | 18 | def server(): 19 | context = zmq.Context() 20 | socket = context.socket(zmq.ROUTER) 21 | socket.connect(socket_name) 22 | while True: 23 | message = socket.recv_multipart() 24 | if message[1] == b"end": 25 | print("Received end") 26 | break 27 | 28 | _ = load_message_from_bytes(message[1]) 29 | 30 | 31 | frame = gen_frame() 32 | p1 = Thread(target=server) 33 | p1.start() 34 | 35 | context = zmq.Context() 36 | socket = context.socket(zmq.DEALER) 37 | socket.bind(socket_name) 38 | 39 | buf_1024b = bytes(BLOCK_SIZE) 40 | 41 | start = time() 42 | wait_time = 0 43 | m = Message.video_frame(frame) 44 | for _ in range(NUMBER): 45 | s = save_message_to_bytes(m) 46 | socket.send_multipart([s, buf_1024b]) 47 | wait = time() 48 | wait_time += time() - wait 49 | 50 | print("Time taken", time() - start, wait_time) 51 | socket.send_multipart([b"end"]) 52 | p1.join() 53 | -------------------------------------------------------------------------------- /python/zmq/zmq_reqrep_native_async.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from time import time 3 | 4 | from savant_rs.utils import gen_frame 5 | from savant_rs.utils.serialization import Message 6 | from savant_rs.zmq import ( 7 | NonBlockingReader, 8 | NonBlockingWriter, 9 | ReaderConfigBuilder, 10 | WriterConfigBuilder, 11 | ) 12 | 13 | socket_name = "ipc:///tmp/test_hello" 14 | 15 | NUMBER = 1000 16 | BLOCK_SIZE = 1024 * 1024 17 | 18 | 19 | async def reader(): 20 | reader_config = ReaderConfigBuilder("rep+connect:" + socket_name).build() 21 | reader = NonBlockingReader(reader_config, 100) 22 | reader.start() 23 | counter = NUMBER 24 | while counter > 0: 25 | m = reader.try_receive() 26 | if m is None: 27 | await asyncio.sleep(0) 28 | else: 29 | assert len(m.data(0)) == BLOCK_SIZE 30 | 31 | if counter % 1000 == 0: 32 | print( 33 | "Read counter", 34 | counter, 35 | ", enqueued results", 36 | reader.enqueued_results(), 37 | ) 38 | 39 | counter -= 1 40 | 41 | 42 | async def writer(): 43 | writer_config = WriterConfigBuilder("req+bind:" + socket_name).build() 44 | writer = NonBlockingWriter(writer_config, 100) 45 | writer.start() 46 | 47 | frame = gen_frame() 48 | buf = bytes(BLOCK_SIZE) 49 | start = time() 50 | wait_time = 0 51 | counter = NUMBER 52 | while counter > 0: 53 | m = Message.video_frame(frame) 54 | wait = time() 55 | response = writer.send_message("topic", m, buf) 56 | while response.try_get() is None: 57 | await asyncio.sleep(0) 58 | 59 | if counter % 1000 == 0: 60 | print("Write counter", counter) 61 | 62 | counter -= 1 63 | wait_time += time() - wait 64 | 65 | print("Time taken", time() - start, wait_time) 66 | 67 | 68 | async def run_system(): 69 | await asyncio.gather(reader(), writer()) 70 | 71 | 72 | loop = asyncio.get_event_loop() 73 | try: 74 | loop.run_until_complete(run_system()) 75 | finally: 76 | loop.run_until_complete( 77 | loop.shutdown_asyncgens() 78 | ) # see: https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.loop.shutdown_asyncgens 79 | loop.close() 80 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | zmq 2 | numpy 3 | maturin[patchelf] ~= 1.8 4 | sphinx 5 | sphinx-rtd-theme 6 | sphinxcontrib-napoleon 7 | -------------------------------------------------------------------------------- /savant_core/benches/bench_bbox_utils.rs: -------------------------------------------------------------------------------- 1 | #![feature(test)] 2 | 3 | extern crate test; 4 | 5 | use rand::Rng; 6 | use savant_core::primitives::utils::solely_owned_areas; 7 | use savant_core::primitives::RBBox; 8 | use test::Bencher; 9 | 10 | fn bench_solely_owned_areas(bbox_count: usize, parallel: bool) { 11 | let pos_x_range = 0.0..1920.0; 12 | let pos_y_range = 0.0..1080.0; 13 | let width_range = 50.0..600.0; 14 | let height_range = 50.0..400.0; 15 | let mut rng = rand::rng(); 16 | let bboxes: Vec = (0..bbox_count) 17 | .map(|_| { 18 | RBBox::new( 19 | rng.random_range(pos_x_range.clone()), 20 | rng.random_range(pos_y_range.clone()), 21 | rng.random_range(width_range.clone()), 22 | rng.random_range(height_range.clone()), 23 | Some(0.0), 24 | ) 25 | }) 26 | .collect(); 27 | let bbox_refs = bboxes.iter().collect::>(); 28 | solely_owned_areas(&bbox_refs, parallel); 29 | } 30 | 31 | #[bench] 32 | fn bench_seq_solely_owned_areas_010(b: &mut Bencher) { 33 | b.iter(|| { 34 | bench_solely_owned_areas(10, false); 35 | }); 36 | } 37 | 38 | #[bench] 39 | fn bench_seq_solely_owned_areas_020(b: &mut Bencher) { 40 | b.iter(|| { 41 | bench_solely_owned_areas(20, false); 42 | }); 43 | } 44 | 45 | #[bench] 46 | fn bench_seq_solely_owned_areas_050(b: &mut Bencher) { 47 | b.iter(|| { 48 | bench_solely_owned_areas(50, false); 49 | }); 50 | } 51 | 52 | #[bench] 53 | fn bench_par_solely_owned_areas_010(b: &mut Bencher) { 54 | b.iter(|| { 55 | bench_solely_owned_areas(10, true); 56 | }); 57 | } 58 | 59 | #[bench] 60 | fn bench_par_solely_owned_areas_020(b: &mut Bencher) { 61 | b.iter(|| { 62 | bench_solely_owned_areas(20, true); 63 | }); 64 | } 65 | 66 | #[bench] 67 | fn bench_par_solely_owned_areas_050(b: &mut Bencher) { 68 | b.iter(|| { 69 | bench_solely_owned_areas(50, true); 70 | }); 71 | } 72 | -------------------------------------------------------------------------------- /savant_core/benches/bench_bboxes.rs: -------------------------------------------------------------------------------- 1 | #![feature(test)] 2 | 3 | extern crate test; 4 | 5 | use savant_core::primitives::RBBox; 6 | use test::Bencher; 7 | 8 | #[bench] 9 | fn bench_scale_90(b: &mut Bencher) { 10 | let bb1 = RBBox::new(0.0, 0.0, 10.0, 20.0, Some(0.0)); 11 | b.iter(|| { 12 | bb1.clone().scale(0.9, 0.7); 13 | }); 14 | } 15 | 16 | #[bench] 17 | fn bench_scale_generic(b: &mut Bencher) { 18 | let bb1 = RBBox::new(0.0, 0.0, 10.0, 20.0, Some(35.0)); 19 | b.iter(|| { 20 | bb1.clone().scale(0.9, 0.7); 21 | }); 22 | } 23 | 24 | #[bench] 25 | fn bench_get_area(b: &mut Bencher) { 26 | let bb1 = RBBox::new(0.0, 0.0, 10.0, 20.0, Some(35.0)); 27 | b.iter(|| { 28 | bb1.get_area(); 29 | }); 30 | } 31 | 32 | #[bench] 33 | fn bench_iou(b: &mut Bencher) { 34 | let bb1 = RBBox::new(0.0, 0.0, 10.0, 20.0, Some(0.0)); 35 | let bb2 = RBBox::new(0.0, 0.0, 20.0, 10.0, Some(0.0)); 36 | b.iter(|| { 37 | bb1.iou(&bb2).expect("iou failed"); 38 | }); 39 | } 40 | 41 | #[bench] 42 | fn bench_ios(b: &mut Bencher) { 43 | let bb1 = RBBox::new(0.0, 0.0, 10.0, 20.0, Some(0.0)); 44 | let bb2 = RBBox::new(0.0, 0.0, 20.0, 10.0, Some(0.0)); 45 | b.iter(|| { 46 | bb1.ios(&bb2).expect("ios failed"); 47 | }); 48 | } 49 | 50 | #[bench] 51 | fn bench_ioo(b: &mut Bencher) { 52 | let bb1 = RBBox::new(0.0, 0.0, 10.0, 20.0, Some(0.0)); 53 | let bb2 = RBBox::new(0.0, 0.0, 20.0, 10.0, Some(0.0)); 54 | b.iter(|| { 55 | bb1.ioo(&bb2).expect("ioo failed"); 56 | }); 57 | } 58 | -------------------------------------------------------------------------------- /savant_core/benches/bench_frame_save_load_pb.rs: -------------------------------------------------------------------------------- 1 | #![feature(test)] 2 | 3 | extern crate test; 4 | use savant_core::primitives::rust::VideoFrameProxy; 5 | use savant_core::protobuf::{from_pb, ToProtobuf}; 6 | use savant_core::test::gen_frame; 7 | use test::Bencher; 8 | 9 | #[bench] 10 | fn bench_save_load_video_frame_pb(b: &mut Bencher) { 11 | let frame = gen_frame(); 12 | b.iter(|| { 13 | let res = frame.to_pb().unwrap(); 14 | let _ = from_pb::(&res).unwrap(); 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /savant_core/benches/bench_label_filter.rs: -------------------------------------------------------------------------------- 1 | #![feature(test)] 2 | 3 | extern crate test; 4 | use test::Bencher; 5 | 6 | #[bench] 7 | fn bench_label_filter(b: &mut Bencher) { 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 | rule.matches(&["test".to_string(), "test2".to_string()]); 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /savant_core/benches/bench_message_save_load.rs: -------------------------------------------------------------------------------- 1 | #![feature(test)] 2 | 3 | extern crate test; 4 | 5 | use savant_core::match_query::MatchQuery; 6 | use savant_core::message::{load_message, save_message, Message}; 7 | use savant_core::primitives::eos::EndOfStream; 8 | use savant_core::primitives::frame_batch::VideoFrameBatch; 9 | use savant_core::primitives::frame_update::VideoFrameUpdate; 10 | use savant_core::primitives::object::ObjectOperations; 11 | use savant_core::primitives::WithAttributes; 12 | use savant_core::test::gen_frame; 13 | use test::Bencher; 14 | 15 | #[bench] 16 | fn bench_save_load_video_frame(b: &mut Bencher) { 17 | let message = Message::video_frame(&gen_frame()); 18 | b.iter(|| { 19 | let res = save_message(&message).unwrap(); 20 | let m = load_message(&res); 21 | assert!(m.is_video_frame()); 22 | }); 23 | } 24 | 25 | #[bench] 26 | fn bench_save_load_eos(b: &mut Bencher) { 27 | let eos = EndOfStream::new("test".to_string()); 28 | let message = Message::end_of_stream(eos); 29 | b.iter(|| { 30 | let res = save_message(&message).unwrap(); 31 | let m = load_message(&res); 32 | assert!(m.is_end_of_stream()); 33 | }); 34 | } 35 | 36 | #[bench] 37 | fn bench_save_load_batch(b: &mut Bencher) { 38 | let mut batch = VideoFrameBatch::new(); 39 | batch.add(1, gen_frame()); 40 | batch.add(2, gen_frame()); 41 | batch.add(3, gen_frame()); 42 | batch.add(4, gen_frame()); 43 | let message = Message::video_frame_batch(&batch); 44 | b.iter(|| { 45 | let res = save_message(&message).unwrap(); 46 | let m = load_message(&res); 47 | assert!(m.is_video_frame_batch()); 48 | }); 49 | } 50 | 51 | #[bench] 52 | fn bench_save_load_frame_update(b: &mut Bencher) { 53 | let f = gen_frame(); 54 | let mut update = VideoFrameUpdate::default(); 55 | for o in f.access_objects(&MatchQuery::Idle) { 56 | update.add_object(o.detached_copy(), None); 57 | } 58 | let attrs = f.get_attributes(); 59 | for (namespace, label) in attrs { 60 | update.add_frame_attribute(f.get_attribute(&namespace, &label).unwrap()); 61 | } 62 | 63 | let message = Message::video_frame_update(update); 64 | 65 | b.iter(|| { 66 | let res = save_message(&message).unwrap(); 67 | let m = load_message(&res); 68 | assert!(m.is_video_frame_update()); 69 | }); 70 | } 71 | -------------------------------------------------------------------------------- /savant_core/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("cargo:rustc-link-lib=dylib=zmq"); 3 | } 4 | -------------------------------------------------------------------------------- /savant_core/src/atomic_f32.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::sync::atomic::{AtomicU32, Ordering}; 3 | 4 | pub struct AtomicF32(AtomicU32); 5 | 6 | impl serde::Serialize for AtomicF32 { 7 | fn serialize(&self, serializer: S) -> Result 8 | where 9 | S: serde::Serializer, 10 | { 11 | self.get().serialize(serializer) 12 | } 13 | } 14 | 15 | impl<'de> serde::Deserialize<'de> for AtomicF32 { 16 | fn deserialize(deserializer: D) -> Result 17 | where 18 | D: serde::Deserializer<'de>, 19 | { 20 | let value = f32::deserialize(deserializer)?; 21 | Ok(AtomicF32::new(value)) 22 | } 23 | } 24 | 25 | impl PartialEq for AtomicF32 { 26 | fn eq(&self, other: &Self) -> bool { 27 | self.get() == other.get() 28 | } 29 | } 30 | 31 | impl Clone for AtomicF32 { 32 | fn clone(&self) -> Self { 33 | Self::new(self.get()) 34 | } 35 | } 36 | 37 | impl AtomicF32 { 38 | pub fn new(value: f32) -> Self { 39 | let as_u32 = value.to_bits(); 40 | Self(AtomicU32::new(as_u32)) 41 | } 42 | pub fn set(&self, value: f32) { 43 | let as_u32 = value.to_bits(); 44 | self.0.store(as_u32, Ordering::SeqCst) 45 | } 46 | pub fn get(&self) -> f32 { 47 | let as_u32 = self.0.load(Ordering::SeqCst); 48 | f32::from_bits(as_u32) 49 | } 50 | } 51 | 52 | impl From for AtomicF32 { 53 | fn from(value: f32) -> Self { 54 | Self::new(value) 55 | } 56 | } 57 | 58 | impl From for f32 { 59 | fn from(value: AtomicF32) -> Self { 60 | value.get() 61 | } 62 | } 63 | 64 | impl fmt::Debug for AtomicF32 { 65 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 66 | write!(f, "{}", self.get()) 67 | } 68 | } 69 | 70 | #[cfg(test)] 71 | mod tests { 72 | #[test] 73 | fn test_ser_deser() { 74 | let a = super::AtomicF32::new(3.14); 75 | let serialized = serde_json::to_string(&a).unwrap(); 76 | let deserialized: super::AtomicF32 = serde_json::from_str(&serialized).unwrap(); 77 | assert_eq!(a, deserialized); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /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/json_api.rs: -------------------------------------------------------------------------------- 1 | pub trait ToSerdeJsonValue { 2 | fn to_serde_json_value(&self) -> serde_json::Value; 3 | } 4 | -------------------------------------------------------------------------------- /savant_core/src/lib.rs: -------------------------------------------------------------------------------- 1 | use opentelemetry::global; 2 | use opentelemetry::global::BoxedTracer; 3 | use std::sync::OnceLock; 4 | use tokio::runtime::Runtime; 5 | 6 | pub mod atomic_f32; 7 | pub mod deadlock_detection; 8 | pub mod draw; 9 | pub mod eval_cache; 10 | pub mod eval_context; 11 | pub mod eval_resolvers; 12 | /// A trait to serialize various objects to json. 13 | pub mod json_api; 14 | pub mod macros; 15 | pub mod match_query; 16 | pub mod message; 17 | pub mod otlp; 18 | pub mod pipeline; 19 | pub mod primitives; 20 | pub mod protobuf; 21 | pub mod rwlock; 22 | pub mod symbol_mapper; 23 | pub mod telemetry; 24 | pub mod test; 25 | pub mod transport; 26 | pub mod utils; 27 | 28 | pub mod metrics; 29 | pub mod webserver; 30 | 31 | pub const EPS: f32 = 0.00001; 32 | 33 | static SHARED_ASYNC_TOKIO_RT: OnceLock = OnceLock::new(); 34 | 35 | pub fn get_or_init_async_runtime() -> &'static Runtime { 36 | SHARED_ASYNC_TOKIO_RT.get_or_init(|| { 37 | let rt = tokio::runtime::Builder::new_multi_thread() 38 | .enable_all() 39 | .build() 40 | .unwrap(); 41 | rt 42 | }) 43 | } 44 | 45 | #[inline] 46 | pub fn round_2_digits(v: f32) -> f32 { 47 | (v * 100.0).round() / 100.0 48 | } 49 | 50 | #[inline] 51 | pub fn version() -> String { 52 | env!("CARGO_PKG_VERSION").to_owned() 53 | } 54 | 55 | #[inline] 56 | pub fn fast_hash(bytes: &[u8]) -> u32 { 57 | crc32fast::hash(bytes) 58 | } 59 | 60 | #[inline] 61 | pub fn get_tracer() -> BoxedTracer { 62 | global::tracer("video_pipeline") 63 | } 64 | 65 | pub mod rust { 66 | pub use super::otlp::PropagatedContext; 67 | pub use super::pipeline::stats::FrameProcessingStatRecord; 68 | pub use super::pipeline::stats::FrameProcessingStatRecordType; 69 | pub use super::pipeline::stats::StageLatencyMeasurements; 70 | pub use super::pipeline::stats::StageLatencyStat; 71 | pub use super::pipeline::stats::StageProcessingStat; 72 | pub use super::pipeline::Pipeline; 73 | pub use super::pipeline::PipelineConfiguration; 74 | pub use super::pipeline::PipelineConfigurationBuilder; 75 | pub use super::pipeline::PipelineStagePayloadType; 76 | pub use super::symbol_mapper::RegistrationPolicy; 77 | pub use super::symbol_mapper::SymbolMapper; 78 | } 79 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /savant_core/src/otlp.rs: -------------------------------------------------------------------------------- 1 | use opentelemetry::propagation::{Extractor, Injector}; 2 | use opentelemetry::{global, Context}; 3 | use std::cell::RefCell; 4 | use std::collections::HashMap; 5 | 6 | thread_local! { 7 | static CURRENT_CONTEXTS: RefCell> = RefCell::new(vec![Context::default()]); 8 | } 9 | 10 | pub fn push_context(ctx: Context) { 11 | CURRENT_CONTEXTS.with(|contexts| { 12 | contexts.borrow_mut().push(ctx); 13 | }); 14 | } 15 | 16 | pub fn pop_context() { 17 | CURRENT_CONTEXTS.with(|contexts| { 18 | contexts.borrow_mut().pop(); 19 | }); 20 | } 21 | 22 | pub fn current_context() -> Context { 23 | CURRENT_CONTEXTS.with(|contexts| { 24 | let contexts = contexts.borrow(); 25 | contexts.last().unwrap().clone() 26 | }) 27 | } 28 | 29 | pub fn current_context_depth() -> usize { 30 | CURRENT_CONTEXTS.with(|contexts| contexts.borrow().len()) 31 | } 32 | 33 | pub fn with_current_context(f: F) -> R 34 | where 35 | F: FnOnce(&Context) -> R, 36 | { 37 | CURRENT_CONTEXTS.with(|contexts| { 38 | let contexts = contexts.borrow(); 39 | f(contexts.last().as_ref().unwrap()) 40 | }) 41 | } 42 | 43 | #[derive(Debug, Clone, Default)] 44 | pub struct PropagatedContext(pub HashMap); 45 | 46 | impl Injector for PropagatedContext { 47 | fn set(&mut self, key: &str, value: String) { 48 | self.0.insert(key.to_string(), value); 49 | } 50 | } 51 | 52 | impl Extractor for PropagatedContext { 53 | fn get(&self, key: &str) -> Option<&str> { 54 | let key = key.to_owned(); 55 | self.0.get(&key).map(|v| v.as_ref()) 56 | } 57 | 58 | fn keys(&self) -> Vec<&str> { 59 | self.0.keys().map(|k| k.as_ref()).collect() 60 | } 61 | } 62 | 63 | impl PropagatedContext { 64 | pub fn new() -> Self { 65 | Self::default() 66 | } 67 | 68 | pub fn inject(context: &Context) -> Self { 69 | global::get_text_map_propagator(|propagator| { 70 | let mut propagation_context = PropagatedContext::new(); 71 | propagator.inject_context(context, &mut propagation_context); 72 | propagation_context 73 | }) 74 | } 75 | 76 | pub fn extract(&self) -> Context { 77 | global::get_text_map_propagator(|propagator| propagator.extract(self)) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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_core/src/primitives/frame_batch.rs: -------------------------------------------------------------------------------- 1 | use crate::match_query::MatchQuery; 2 | use crate::primitives::frame::VideoFrameProxy; 3 | use crate::primitives::object::BorrowedVideoObject; 4 | use hashbrown::HashMap; 5 | 6 | const DEFAULT_BATCH_SIZE: usize = 64; 7 | 8 | #[derive(Debug, Clone, Default)] 9 | pub struct VideoFrameBatch { 10 | pub(crate) frames: HashMap, 11 | } 12 | 13 | impl VideoFrameBatch { 14 | pub fn exclude_all_temporary_attributes(&mut self) { 15 | self.frames.iter_mut().for_each(|(_, frame)| { 16 | frame.exclude_all_temporary_attributes(); 17 | }); 18 | } 19 | pub fn smart_copy(&self) -> Self { 20 | let frames = self 21 | .frames 22 | .iter() 23 | .map(|(id, frame)| (*id, frame.smart_copy())) 24 | .collect(); 25 | 26 | Self { frames } 27 | } 28 | 29 | pub fn access_objects( 30 | &self, 31 | q: &MatchQuery, 32 | ) -> hashbrown::HashMap> { 33 | self.frames 34 | .iter() 35 | .map(|(id, frame)| (*id, frame.access_objects(q))) 36 | .collect() 37 | } 38 | 39 | pub fn delete_objects(&mut self, q: &MatchQuery) { 40 | self.frames.iter_mut().for_each(|(_, frame)| { 41 | frame.delete_objects(q); 42 | }); 43 | } 44 | 45 | pub fn new() -> Self { 46 | Self { 47 | frames: HashMap::with_capacity(DEFAULT_BATCH_SIZE), 48 | } 49 | } 50 | 51 | pub fn with_capacity(capacity: usize) -> Self { 52 | Self { 53 | frames: HashMap::with_capacity(capacity), 54 | } 55 | } 56 | 57 | pub fn add(&mut self, id: i64, frame: VideoFrameProxy) { 58 | self.frames.insert(id, frame); 59 | } 60 | 61 | pub fn get(&self, id: i64) -> Option { 62 | self.frames.get(&id).cloned() 63 | } 64 | 65 | pub fn del(&mut self, id: i64) -> Option { 66 | self.frames.remove(&id) 67 | } 68 | 69 | pub fn frames(&self) -> &HashMap { 70 | &self.frames 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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_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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /savant_core/src/protobuf/serialize/video_frame_transcoding_method.rs: -------------------------------------------------------------------------------- 1 | use crate::primitives::frame::VideoFrameTranscodingMethod; 2 | use savant_protobuf::generated; 3 | 4 | impl From<&VideoFrameTranscodingMethod> 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/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 | -------------------------------------------------------------------------------- /savant_core/src/transport.rs: -------------------------------------------------------------------------------- 1 | pub mod zeromq; 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /savant_core/src/utils.rs: -------------------------------------------------------------------------------- 1 | pub mod default_once; 2 | pub mod iter; 3 | pub mod uuid_v7; 4 | use std::fmt::Write; 5 | 6 | pub fn bytes_to_hex_string(bytes: &[u8]) -> String { 7 | bytes.iter().fold(String::new(), |mut output, b| { 8 | let _ = write!(output, "{b:02X}"); 9 | output 10 | }) 11 | } 12 | -------------------------------------------------------------------------------- /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_core/src/webserver/kvs_subscription.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /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_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_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_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 | -------------------------------------------------------------------------------- /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_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 | -------------------------------------------------------------------------------- /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 {} not found", 60 | name 61 | ))); 62 | } 63 | Ok(()) 64 | } 65 | -------------------------------------------------------------------------------- /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::RBBox; 32 | 33 | use crate::primitives::eos::EndOfStream; 34 | 35 | use crate::primitives::point::Point; 36 | use crate::primitives::polygonal_area::PolygonalArea; 37 | use crate::primitives::segment::{Intersection, Segment}; 38 | use crate::primitives::shutdown::Shutdown; 39 | -------------------------------------------------------------------------------- /savant_core_py/src/primitives/bbox/utils.rs: -------------------------------------------------------------------------------- 1 | use crate::primitives::bbox::{BBoxMetricType, RBBox}; 2 | use crate::with_gil; 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 | with_gil!(|_| { savant_core::primitives::bbox::utils::solely_owned_areas(&boxes, parallel) }) 10 | } 11 | 12 | #[pyfunction] 13 | pub fn associate_bboxes( 14 | candidates: Vec, 15 | owners: Vec, 16 | metric: BBoxMetricType, 17 | threshold: f32, 18 | ) -> HashMap> { 19 | let candidates = candidates.iter().map(|b| &b.0).collect::>(); 20 | let owners = owners.iter().map(|b| &b.0).collect::>(); 21 | with_gil!(|_| { 22 | savant_core::primitives::bbox::utils::associate_bboxes( 23 | &candidates, 24 | &owners, 25 | metric.into(), 26 | threshold, 27 | ) 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /savant_core_py/src/primitives/message/loader.rs: -------------------------------------------------------------------------------- 1 | use crate::primitives::message::Message; 2 | use crate::release_gil; 3 | use crate::utils::byte_buffer::ByteBuffer; 4 | use pyo3::types::{PyBytes, PyBytesMethods}; 5 | use pyo3::{pyfunction, Bound}; 6 | 7 | /// Loads a message from a byte array. The function is optionally GIL-free. 8 | /// 9 | /// Parameters 10 | /// ---------- 11 | /// bytes : bytes 12 | /// The byte array to load the message from. 13 | /// no_gil : bool 14 | /// Whether to release the GIL while loading the message. 15 | /// 16 | /// Returns 17 | /// ------- 18 | /// savant_rs.primitives.Message 19 | /// The loaded message. 20 | /// 21 | #[pyfunction] 22 | #[pyo3(name = "load_message")] 23 | #[pyo3(signature = (bytes, no_gil = true))] 24 | pub fn load_message_gil(bytes: Vec, no_gil: bool) -> Message { 25 | release_gil!(no_gil, || { 26 | let m = savant_core::message::load_message(&bytes); 27 | Message(m) 28 | }) 29 | } 30 | 31 | /// Loads a message from a :class:`savant_rs.utils.ByteBuffer`. The function is GIL-free. 32 | /// 33 | /// Parameters 34 | /// ---------- 35 | /// bytes : :class:`savant_rs.utils.ByteBuffer` 36 | /// The byte array to load the message from. 37 | /// 38 | /// Returns 39 | /// ------- 40 | /// savant_rs.primitives.Message 41 | /// The loaded message. 42 | /// 43 | #[pyfunction] 44 | #[pyo3(name = "load_message_from_bytebuffer")] 45 | #[pyo3(signature = (buffer, no_gil = true))] 46 | pub fn load_message_from_bytebuffer_gil(buffer: &ByteBuffer, no_gil: bool) -> Message { 47 | release_gil!(no_gil, || Message(savant_core::message::load_message( 48 | buffer.bytes() 49 | ))) 50 | } 51 | 52 | /// Loads a message from a python bytes. The function is optionally GIL-free. 53 | /// 54 | /// Parameters 55 | /// ---------- 56 | /// bytes : bytes 57 | /// The byte buffer to load the message from. 58 | /// no_gil : bool 59 | /// Whether to release the GIL while loading the message. 60 | /// 61 | /// Returns 62 | /// ------- 63 | /// savant_rs.primitives.Message 64 | /// The loaded message. 65 | /// 66 | #[pyfunction] 67 | #[pyo3(name = "load_message_from_bytes")] 68 | #[pyo3(signature = (buffer, no_gil = true))] 69 | pub fn load_message_from_bytes_gil(buffer: &Bound<'_, PyBytes>, no_gil: bool) -> Message { 70 | let bytes = buffer.as_bytes(); 71 | release_gil!(no_gil, || Message(savant_core::message::load_message( 72 | bytes 73 | ))) 74 | } 75 | -------------------------------------------------------------------------------- /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_pyo3; 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_pyo3!(self.0.walk_objects(callable), PyRuntimeError) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /savant_core_py/src/primitives/pyobject.rs: -------------------------------------------------------------------------------- 1 | use crate::with_gil; 2 | use parking_lot::RwLock; 3 | use pyo3::{Py, PyAny, PyObject}; 4 | use std::collections::HashMap; 5 | use std::sync::Arc; 6 | 7 | pub trait PyObjectMeta: Send { 8 | fn get_py_objects_ref(&self) -> Arc>>; 9 | 10 | fn get_py_object_by_ref(&self, namespace: &str, name: &str) -> Option> { 11 | with_gil!(|py| { 12 | self.get_py_objects_ref() 13 | .read() 14 | .get(&(namespace.to_owned(), name.to_owned())) 15 | .map(|o| o.clone_ref(py)) 16 | }) 17 | } 18 | 19 | fn del_py_object(&self, namespace: &str, name: &str) -> Option { 20 | self.get_py_objects_ref() 21 | .write() 22 | .remove(&(namespace.to_owned(), name.to_owned())) 23 | } 24 | 25 | fn set_py_object(&self, namespace: &str, name: &str, pyobject: PyObject) -> Option { 26 | self.get_py_objects_ref() 27 | .write() 28 | .insert((namespace.to_owned(), name.to_owned()), pyobject) 29 | } 30 | 31 | fn clear_py_objects(&self) { 32 | self.get_py_objects_ref().write().clear(); 33 | } 34 | 35 | fn list_py_objects(&self) -> Vec<(String, String)> { 36 | self.get_py_objects_ref() 37 | .read() 38 | .keys() 39 | .map(|(namespace, name)| (namespace.clone(), name.clone())) 40 | .collect() 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /savant_core_py/src/test.rs: -------------------------------------------------------------------------------- 1 | pub mod utils { 2 | use crate::primitives::frame::VideoFrame; 3 | use crate::primitives::object::VideoObject; 4 | use pyo3::pyfunction; 5 | 6 | #[pyfunction] 7 | pub fn gen_empty_frame() -> 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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_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_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_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 | -------------------------------------------------------------------------------- /savant_gstreamer_elements/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | pyo3_build_config::add_extension_module_link_args(); 3 | } 4 | -------------------------------------------------------------------------------- /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_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_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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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_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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | pyo3_build_config::add_extension_module_link_args(); 3 | } 4 | -------------------------------------------------------------------------------- /savant_python/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["maturin>=1.8"] 3 | build-backend = "maturin" 4 | 5 | [project] 6 | dynamic = ['version'] 7 | name = "savant_rs" 8 | requires-python = ">=3.8" 9 | classifiers = [ 10 | "Programming Language :: Rust", 11 | "Programming Language :: Python :: Implementation :: CPython", 12 | "Programming Language :: Python :: Implementation :: PyPy", 13 | ] 14 | 15 | [tool.black] 16 | skip-string-normalization = true 17 | 18 | [tool.pylint.messages_control] 19 | max-line-length = 88 20 | 21 | [tool.maturin] 22 | python-source = "python" 23 | include = ["*"] 24 | features = ["pyo3/extension-module"] 25 | -------------------------------------------------------------------------------- /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/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/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/draw_spec/__init__.py: -------------------------------------------------------------------------------- 1 | from .draw_spec import * 2 | -------------------------------------------------------------------------------- /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/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_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/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_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/metrics/__init__.py: -------------------------------------------------------------------------------- 1 | from .metrics import * # type: ignore 2 | 3 | __all__ = metrics.__all__ # type: ignore 4 | -------------------------------------------------------------------------------- /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_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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /savant_python/python/savant_rs/primitives/geometry/__init__.py: -------------------------------------------------------------------------------- 1 | from .geometry import * # type: ignore 2 | 3 | __all__ = geometry.__all__ # type: ignore 4 | -------------------------------------------------------------------------------- /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_python/python/savant_rs/primitives/user_data.pyi: -------------------------------------------------------------------------------- 1 | from typing import List, Optional, Tuple 2 | 3 | from savant_rs.utils.serialization import Message 4 | 5 | from .attribute import Attribute 6 | from .attribute_value import AttributeValue 7 | 8 | __all__ = [ 9 | "UserData", 10 | ] 11 | 12 | class UserData: 13 | def __init__(self, source_id: str): ... 14 | @property 15 | def source_id(self) -> str: ... 16 | @property 17 | def json(self) -> str: ... 18 | def to_message(self) -> Message: ... 19 | @property 20 | def attributes(self) -> List[Tuple[str, str]]: ... 21 | def get_attribute(self, namespace: str, name: str) -> Optional[Attribute]: ... 22 | def find_attributes_with_ns(self, namespace: str) -> List[Tuple[str, str]]: ... 23 | def find_attributes_with_names(self, names: List[str]) -> List[Tuple[str, str]]: ... 24 | def find_attributes_with_hints( 25 | self, hints: List[Optional[str]] 26 | ) -> List[Tuple[str, str]]: ... 27 | def delete_attributes_with_ns(self, namespace: str): ... 28 | def delete_attributes_with_names(self, names: List[str]): ... 29 | def delete_attributes_with_hints(self, hints: List[Optional[str]]): ... 30 | def delete_attribute(self, namespace: str, name: str) -> Optional[Attribute]: ... 31 | def set_attribute(self, attribute: Attribute) -> Optional[Attribute]: ... 32 | def set_persistent_attribute( 33 | self, 34 | namespace: str, 35 | name: str, 36 | is_hidden: bool, 37 | hint: Optional[str], 38 | values: Optional[List[AttributeValue]], 39 | ): ... 40 | def set_temporary_attribute( 41 | self, 42 | namespace: str, 43 | name: str, 44 | is_hidden: bool, 45 | hint: Optional[str], 46 | values: Optional[List[AttributeValue]], 47 | ): ... 48 | def clear_attributes(self): ... 49 | @property 50 | def json_pretty(self) -> str: ... 51 | def to_protobuf(self, no_gil: bool = True) -> bytes: ... 52 | @classmethod 53 | def from_protobuf(cls, protobuf: bytes, no_gil: bool = True) -> "UserData": ... 54 | -------------------------------------------------------------------------------- /savant_python/python/savant_rs/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insight-platform/savant-rs/240fb8792a4dcfb8470137b55b9586821d28e5c6/savant_python/python/savant_rs/py.typed -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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_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_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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/webserver/kvs/__init__.py: -------------------------------------------------------------------------------- 1 | from .kvs import * # type: ignore 2 | 3 | __all__ = kvs.__all__ # type: ignore 4 | -------------------------------------------------------------------------------- /savant_python/python/savant_rs/webserver/kvs/kvs.pyi: -------------------------------------------------------------------------------- 1 | from typing import List, Optional, Union 2 | 3 | from savant_rs.primitives import Attribute 4 | 5 | __all__ = [ 6 | "set_attributes", 7 | "search_attributes", 8 | "search_keys", 9 | "del_attributes", 10 | "get_attribute", 11 | "del_attribute", 12 | "serialize_attributes", 13 | "deserialize_attributes", 14 | "KvsSetOperation", 15 | "KvsDeleteOperation", 16 | "KvsSubscription", 17 | ] 18 | 19 | def set_attributes(attributes: List[Attribute], ttl: Optional[int]) -> None: ... 20 | def search_attributes( 21 | ns: Optional[str], name: Optional[str], no_gil: bool 22 | ) -> List[Attribute]: ... 23 | 24 | # pub fn search_keys(ns: &Option, name: &Option) -> Vec<(String, String)> 25 | def search_keys(ns: Optional[str], name: Optional[str], no_gil: bool) -> List[str]: ... 26 | 27 | # pub fn del_attributes(ns: &Option, name: &Option) 28 | def del_attributes(ns: Optional[str], name: Optional[str], no_gil: bool) -> None: ... 29 | 30 | # pub fn get_attribute(ns: &str, name: &str) -> Option 31 | def get_attribute(ns: str, name: str) -> Optional[Attribute]: ... 32 | 33 | # pub fn del_attribute(ns: &str, name: &str) -> Option 34 | def del_attribute(ns: str, name: str) -> Optional[Attribute]: ... 35 | 36 | # pub fn serialize_attributes(attributes: Vec) -> PyResult 37 | def serialize_attributes(attributes: List[Attribute]) -> None: ... 38 | 39 | # pub fn deserialize_attributes(serialized: &Bound<'_, PyBytes>) -> PyResult> 40 | def deserialize_attributes(serialized: bytes) -> List[Attribute]: ... 41 | 42 | class KvsSetOperation: 43 | timestamp: int 44 | ttl: Optional[int] 45 | attributes: List[Attribute] 46 | 47 | class KvsDeleteOperation: 48 | timestamp: int 49 | attributes: List[Attribute] 50 | 51 | class KvsSubscription: 52 | def __init__(self, name: str, max_inflight_ops: int): ... 53 | def recv(self) -> Optional[Union[KvsSetOperation, KvsDeleteOperation]]: ... 54 | def try_recv(self) -> Optional[Union[KvsSetOperation, KvsDeleteOperation]]: ... 55 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /savant_python/python/savant_rs/zmq/__init__.py: -------------------------------------------------------------------------------- 1 | from .zmq import * # type: ignore 2 | 3 | __all__ = zmq.__all__ # type: ignore 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /services/common/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod job_writer; 2 | pub mod source; 3 | use savant_core::utils::bytes_to_hex_string; 4 | use std::str::from_utf8; 5 | use std::time::{SystemTime, UNIX_EPOCH}; 6 | 7 | pub fn topic_to_string(topic: &[u8]) -> String { 8 | from_utf8(topic) 9 | .map(String::from) 10 | .unwrap_or(bytes_to_hex_string(topic)) 11 | } 12 | 13 | pub fn systime_ms() -> u128 { 14 | let start = SystemTime::now(); 15 | let since_the_epoch = start 16 | .duration_since(UNIX_EPOCH) 17 | .expect("Time went backwards"); 18 | since_the_epoch.as_millis() 19 | } 20 | -------------------------------------------------------------------------------- /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" -------------------------------------------------------------------------------- /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 BSL-1.1 license. See [LICENSE](LICENSE) for more information. 38 | 39 | ### Obtaining Production-Use License 40 | 41 | To obtain a production-use license, please fill out the form 42 | at [In-Sight Licensing](https://forms.gle/kstX7BrgzqrSLCJ18). 43 | -------------------------------------------------------------------------------- /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 | tokio = { workspace = true } 22 | serde = { workspace = true } 23 | serde_json = { workspace = true } 24 | uuid = { workspace = true } 25 | actix-web = { workspace = true } 26 | -------------------------------------------------------------------------------- /services/replay/replay/assets/stub_imgs/smpte100_640x360.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insight-platform/savant-rs/240fb8792a4dcfb8470137b55b9586821d28e5c6/services/replay/replay/assets/stub_imgs/smpte100_640x360.jpeg -------------------------------------------------------------------------------- /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": { 43 | "source_id": "source_id" 44 | }, 45 | "source_cache_size": 1000, 46 | "inflight_ops": 100, 47 | "fix_ipc_permissions": 511 48 | } 49 | }, 50 | "out_stream": null, 51 | "storage": { 52 | "rocksdb": { 53 | "path": "${DB_PATH:-/tmp/rocksdb}", 54 | "data_expiration_ttl": { 55 | "secs": 60, 56 | "nanos": 0 57 | } 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /services/replay/replay/scripts/rest_api/new_job.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | query() { 4 | 5 | ANCHOR_KEYFRAME=$1 6 | 7 | cat <, 21 | pub shutdown: Mutex, 22 | } 23 | 24 | #[derive(Debug, Serialize)] 25 | enum ResponseMessage { 26 | #[serde(rename = "ok")] 27 | Ok, 28 | #[serde(rename = "jobs")] 29 | ListJobs(Vec<(String, JobConfiguration, JobStopCondition)>), 30 | #[serde(rename = "stopped_jobs")] 31 | ListStoppedJobs(Vec<(String, JobConfiguration, Option)>), 32 | #[serde(rename = "new_job")] 33 | NewJob(String), 34 | #[serde(rename = "keyframes")] 35 | FindKeyframes(String, Vec), 36 | #[serde(rename = "running")] 37 | StatusRunning, 38 | #[serde(rename = "finished")] 39 | StatusFinished, 40 | #[serde(rename = "error")] 41 | Error(String), 42 | } 43 | 44 | impl Responder for ResponseMessage { 45 | type Body = BoxBody; 46 | fn respond_to(self, _req: &actix_web::HttpRequest) -> HttpResponse { 47 | let body = serde_json::to_string(&self).unwrap(); 48 | let mut resp = match self { 49 | ResponseMessage::Error(_) => HttpResponse::InternalServerError(), 50 | _ => HttpResponse::Ok(), 51 | }; 52 | resp.content_type(ContentType::json()).body(body) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /services/replay/replay/src/web_service/list_jobs.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{get, web, Responder}; 2 | use log::info; 3 | use serde::Deserialize; 4 | 5 | use replaydb::service::JobManager; 6 | 7 | use crate::web_service::{JobService, ResponseMessage}; 8 | 9 | #[derive(Deserialize)] 10 | struct JobFilter { 11 | job: Option, 12 | } 13 | 14 | #[get("/job/{job}")] 15 | async fn list_job(js: web::Data, q: web::Path) -> impl Responder { 16 | list_jobs_int( 17 | js, 18 | JobFilter { 19 | job: Some(q.into_inner()), 20 | }, 21 | ) 22 | .await 23 | } 24 | 25 | #[get("/job")] 26 | async fn list_jobs(js: web::Data, q: web::Query) -> impl Responder { 27 | list_jobs_int(js, q.into_inner()).await 28 | } 29 | 30 | async fn list_jobs_int(js: web::Data, q: JobFilter) -> impl Responder { 31 | let mut js_bind = js.service.lock().await; 32 | 33 | let cleanup = js_bind.clean_stopped_jobs().await; 34 | if let Err(e) = cleanup { 35 | return ResponseMessage::Error(e.to_string()); 36 | } 37 | 38 | let jobs = js_bind 39 | .list_jobs() 40 | .into_iter() 41 | .map(|(uuid, c, s)| (uuid.to_string(), c, s)) 42 | .collect::>(); 43 | let jobs = if let Some(job) = &q.job { 44 | info!("Listing job: {}", job); 45 | jobs.into_iter() 46 | .filter(|(uuid, _, _)| uuid == job) 47 | .collect() 48 | } else { 49 | info!("Listing all currently running jobs"); 50 | jobs 51 | }; 52 | ResponseMessage::ListJobs(jobs) 53 | } 54 | 55 | #[get("/job/stopped")] 56 | async fn list_stopped_jobs(js: web::Data) -> impl Responder { 57 | let mut js_bind = js.service.lock().await; 58 | 59 | let cleanup = js_bind.clean_stopped_jobs().await; 60 | if let Err(e) = cleanup { 61 | return ResponseMessage::Error(e.to_string()); 62 | } 63 | 64 | let stopped_jobs = js_bind 65 | .list_stopped_jobs() 66 | .into_iter() 67 | .map(|(uuid, conf, res)| (uuid.to_string(), conf, res)) 68 | .collect::>(); 69 | ResponseMessage::ListStoppedJobs(stopped_jobs) 70 | } 71 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /services/replay/replaydb/src/job/configuration.rs: -------------------------------------------------------------------------------- 1 | use crate::job::{RoutingLabelsUpdateStrategy, STD_FPS}; 2 | use anyhow::bail; 3 | use derive_builder::Builder; 4 | use hashbrown::HashMap; 5 | use serde::{Deserialize, Serialize}; 6 | use std::time::Duration; 7 | 8 | #[derive(Debug, Serialize, Deserialize, Clone, Builder)] 9 | #[builder(default)] 10 | pub struct JobConfiguration { 11 | pub(crate) ts_sync: bool, 12 | pub(crate) skip_intermediary_eos: bool, 13 | pub(crate) send_eos: bool, 14 | pub(crate) stop_on_incorrect_ts: bool, 15 | pub(crate) ts_discrepancy_fix_duration: Duration, 16 | pub(crate) min_duration: Duration, 17 | pub(crate) max_duration: Duration, 18 | pub(crate) stored_stream_id: String, 19 | pub(crate) resulting_stream_id: String, 20 | pub(crate) routing_labels: RoutingLabelsUpdateStrategy, 21 | pub(crate) max_idle_duration: Duration, 22 | pub(crate) max_delivery_duration: Duration, 23 | pub(crate) send_metadata_only: bool, 24 | pub(crate) labels: Option>, 25 | } 26 | 27 | impl Default for JobConfiguration { 28 | fn default() -> Self { 29 | Self { 30 | ts_sync: false, 31 | skip_intermediary_eos: false, 32 | send_eos: false, 33 | stop_on_incorrect_ts: false, 34 | ts_discrepancy_fix_duration: Duration::from_secs_f64(1_f64 / STD_FPS), 35 | min_duration: Duration::from_secs_f64(1_f64 / STD_FPS), 36 | max_duration: Duration::from_secs_f64(1_f64 / STD_FPS), 37 | stored_stream_id: String::new(), 38 | resulting_stream_id: String::new(), 39 | routing_labels: RoutingLabelsUpdateStrategy::Bypass, 40 | max_idle_duration: Duration::from_secs(10), 41 | max_delivery_duration: Duration::from_secs(10), 42 | send_metadata_only: false, 43 | labels: None, 44 | } 45 | } 46 | } 47 | 48 | impl JobConfigurationBuilder { 49 | pub fn build_and_validate(&mut self) -> anyhow::Result { 50 | let c = self.build()?; 51 | if c.min_duration > c.max_duration { 52 | bail!("Min PTS delta is greater than max PTS delta!"); 53 | } 54 | if c.stored_stream_id.is_empty() || c.resulting_stream_id.is_empty() { 55 | bail!("Stored source id or resulting source id is empty!"); 56 | } 57 | Ok(c) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/src/service/configuration.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{bail, Result}; 2 | use savant_services_common::{ 3 | job_writer::{SinkConfiguration, SinkOptions}, 4 | source::SourceConfiguration, 5 | }; 6 | use serde::{Deserialize, Serialize}; 7 | use std::time::Duration; 8 | use twelf::{config, Layer}; 9 | 10 | #[derive(Debug, Serialize, Deserialize, Clone)] 11 | pub enum Storage { 12 | #[serde(rename = "rocksdb")] 13 | RocksDB { 14 | path: String, 15 | data_expiration_ttl: Duration, 16 | }, 17 | } 18 | 19 | #[derive(Debug, Serialize, Deserialize, Clone)] 20 | pub struct CommonConfiguration { 21 | pub management_port: u16, 22 | pub stats_period: Duration, 23 | pub pass_metadata_only: bool, 24 | pub job_writer_cache_max_capacity: u64, 25 | pub job_writer_cache_ttl: Duration, 26 | pub job_eviction_ttl: Duration, 27 | pub default_job_sink_options: Option, 28 | } 29 | 30 | #[config] 31 | #[derive(Debug, Serialize, Clone)] 32 | pub struct ServiceConfiguration { 33 | pub common: CommonConfiguration, 34 | pub in_stream: SourceConfiguration, 35 | pub out_stream: Option, 36 | pub storage: Storage, 37 | } 38 | 39 | impl ServiceConfiguration { 40 | pub(crate) fn validate(&self) -> Result<()> { 41 | if self.common.management_port <= 1024 { 42 | bail!("Management port must be set to a value greater than 1024!"); 43 | } 44 | Ok(()) 45 | } 46 | 47 | pub fn new(path: &str) -> Result { 48 | let conf = Self::with_layers(&[Layer::Json(path.into())])?; 49 | conf.validate()?; 50 | Ok(conf) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /services/replay/samples/file_restreaming/assets/stub_imgs/smpte100_1280x720.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insight-platform/savant-rs/240fb8792a4dcfb8470137b55b9586821d28e5c6/services/replay/samples/file_restreaming/assets/stub_imgs/smpte100_1280x720.jpeg -------------------------------------------------------------------------------- /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 | } 59 | } 60 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /services/router/assets/configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "ingres": [ 3 | { 4 | "name": "ingress1", 5 | "socket": { 6 | "url": "router+bind:tcp://127.0.0.1:6667", 7 | "options": { 8 | "receive_timeout": { 9 | "secs": 1, 10 | "nanos": 0 11 | }, 12 | "receive_hwm": 1000, 13 | "topic_prefix_spec": "none", 14 | "source_cache_size": 1000, 15 | "fix_ipc_permissions": 511, 16 | "inflight_ops": 100 17 | } 18 | }, 19 | "handler": "ingress_handler" 20 | } 21 | ], 22 | "egress": [ 23 | { 24 | "name": "egress1", 25 | "socket": { 26 | "url": "dealer+bind:tcp://127.0.0.1:3333" 27 | }, 28 | "high_watermark": 0.9, 29 | "matcher": "[label1] & [label2]", 30 | "source_mapper": "egress_source_handler", 31 | "topic_mapper": "egress_topic_handler" 32 | }, 33 | { 34 | "name": "egress2", 35 | "socket": { 36 | "url": "dealer+bind:tcp://127.0.0.1:3334" 37 | }, 38 | "high_watermark": 0.9, 39 | "matcher": "[label1] & ([label3] | [label2])", 40 | "source_mapper": "egress_source_handler", 41 | "topic_mapper": "egress_topic_handler" 42 | } 43 | 44 | ], 45 | "common": { 46 | "init": { 47 | "python_root": "${PYTHON_MODULE_ROOT:-services/router/assets/python}", 48 | "module_name": "module", 49 | "function_name": "init", 50 | "args": [ 51 | { 52 | "params": { 53 | "home_dir": "${HOME}", 54 | "user_name": "${USER}" 55 | } 56 | } 57 | ] 58 | }, 59 | "source_affinity_cache_size": 1000, 60 | "name_cache": { 61 | "ttl": { 62 | "secs": 10, 63 | "nanos": 0 64 | }, 65 | "size": 1000 66 | }, 67 | "idle_sleep": { 68 | "secs": 0, 69 | "nanos": 1000 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /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/router/assets/python/zmq_producer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import argparse 3 | import sys 4 | import time 5 | 6 | from savant_rs.logging import LogLevel, set_log_level 7 | from savant_rs.utils import gen_frame 8 | from savant_rs.utils.serialization import Message 9 | from savant_rs.zmq import WriterConfigBuilder, BlockingWriter, WriterResultSuccess 10 | 11 | set_log_level(LogLevel.Info) 12 | 13 | 14 | def main(): 15 | parser = argparse.ArgumentParser(description="ZMQ Message Producer") 16 | parser.add_argument( 17 | "--socket", 18 | required=True, 19 | help="ZMQ socket URI (e.g. dealer+connect:tcp://127.0.0.1:6666)", 20 | ) 21 | parser.add_argument( 22 | "--count", type=int, default=1000, help="Number of messages to send" 23 | ) 24 | parser.add_argument( 25 | "--block-size", 26 | type=int, 27 | default=128 * 1024, 28 | help="Size of each message in bytes", 29 | ) 30 | 31 | parser.add_argument( 32 | "--delay", 33 | type=int, 34 | default=0, 35 | help="Delay between messages in milliseconds", 36 | ) 37 | 38 | parser.add_argument( 39 | "--topic", 40 | default="topic", 41 | help="Topic to send messages to", 42 | ) 43 | 44 | args = parser.parse_args() 45 | 46 | # Generate test data 47 | frame = gen_frame() 48 | frame.keyframe = True 49 | buf = bytes(args.block_size) 50 | 51 | # Configure and start writer 52 | writer_config = WriterConfigBuilder(args.socket).build() 53 | writer = BlockingWriter(writer_config) 54 | writer.start() 55 | 56 | try: 57 | for i in range(args.count): 58 | if args.delay > 0: 59 | time.sleep(args.delay / 1000) 60 | 61 | m = Message.video_frame(frame) 62 | res = writer.send_message(args.topic, m, buf) 63 | if res.__class__ != WriterResultSuccess: 64 | print("Failed to send message") 65 | continue 66 | i += 1 67 | if (i + 1) % 100 == 0: 68 | print(f"Sent {i + 1} messages...") 69 | 70 | print(f"Sent {args.count} messages") 71 | 72 | except KeyboardInterrupt: 73 | print("\nProducer interrupted by user") 74 | 75 | 76 | if __name__ == "__main__": 77 | main() 78 | -------------------------------------------------------------------------------- /utils/install_protoc.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Exit on error, undefined variables, and pipe failures 4 | set -euo pipefail 5 | 6 | # Function to clean up temporary files and processes on exit 7 | cleanup() { 8 | # Add cleanup tasks here if needed 9 | exit "${1:-0}" 10 | } 11 | 12 | # Set up trap for script termination 13 | trap 'cleanup $?' EXIT 14 | trap 'cleanup 1' INT TERM 15 | 16 | ARCH=$(uname -m) 17 | 18 | PB_REL="https://github.com/protocolbuffers/protobuf/releases" 19 | 20 | # x86_64 21 | if [ "$ARCH" = "x86_64" ]; then 22 | curl -LO $PB_REL/download/v3.15.8/protoc-3.15.8-linux-x86_64.zip 23 | elif [ "$ARCH" = "aarch64" ]; then 24 | curl -LO $PB_REL/download/v3.15.8/protoc-3.15.8-linux-aarch_64.zip 25 | else 26 | echo "Unsupported architecture $ARCH" 27 | exit 1 28 | fi 29 | 30 | unzip -f *.zip 31 | cp bin/protoc /usr/bin 32 | chmod 755 /usr/bin/protoc 33 | 34 | -------------------------------------------------------------------------------- /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 | 15 | # Set up working directory 16 | WORK_DIR=$(mktemp -d) 17 | cd "${WORK_DIR}" 18 | 19 | # Detect architecture in a more robust way 20 | ARCH=$(dpkg --print-architecture) 21 | 22 | # Define versions as variables for easier maintenance 23 | PROTOC_VERSION="3.15.8" 24 | PB_REL="https://github.com/protocolbuffers/protobuf/releases" 25 | 26 | # Download and verify protoc based on architecture 27 | case "${ARCH}" in 28 | amd64) 29 | PROTOC_FILE="protoc-${PROTOC_VERSION}-linux-x86_64.zip" 30 | ;; 31 | arm64) 32 | PROTOC_FILE="protoc-${PROTOC_VERSION}-linux-aarch_64.zip" 33 | ;; 34 | *) 35 | echo "Unsupported architecture: ${ARCH}" 36 | exit 1 37 | ;; 38 | esac 39 | 40 | # Download with error checking 41 | if ! curl -LO --fail "${PB_REL}/download/v${PROTOC_VERSION}/${PROTOC_FILE}"; then 42 | echo "Failed to download protoc" 43 | exit 1 44 | fi 45 | 46 | # Verify and install protoc 47 | if ! unzip -q "${PROTOC_FILE}"; then 48 | echo "Failed to extract protoc" 49 | exit 1 50 | fi 51 | 52 | install -m 755 bin/protoc /usr/local/bin/ 53 | rm -rf "${WORK_DIR}" 54 | 55 | # Verify installation 56 | if ! command -v protoc >/dev/null 2>&1; then 57 | echo "protoc installation failed" 58 | exit 1 59 | fi 60 | 61 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /utils/services/router/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 | readonly TARGET_MODULE_DIR="/opt/python" 9 | 10 | # Create directories with specific permissions 11 | install -d -m 755 "${TARGET_LIB_DIR}" 12 | install -d -m 755 "${TARGET_BIN_DIR}" 13 | install -d -m 755 "${TARGET_ETC_DIR}" 14 | install -d -m 755 "${TARGET_MODULE_DIR}" 15 | 16 | # Find and validate Rust standard library 17 | RUST_STD_LIB=$(find / -name 'libstd-*.so' -type f -print -quit) 18 | if [ -z "${RUST_STD_LIB}" ]; then 19 | echo "Error: Could not find Rust standard library" >&2 20 | exit 1 21 | fi 22 | echo "Rust std lib: ${RUST_STD_LIB}" 23 | 24 | # Copy files with proper permissions and error checking 25 | if ! install -m 644 "${RUST_STD_LIB}" "${TARGET_LIB_DIR}/"; then 26 | echo "Error: Failed to copy Rust standard library" >&2 27 | exit 1 28 | fi 29 | 30 | # Copy dependency libraries 31 | if ! install -m 644 /tmp/build/release/deps/*.so "${TARGET_LIB_DIR}/"; then 32 | echo "Error: Failed to copy dependency libraries" >&2 33 | exit 1 34 | fi 35 | 36 | # Copy binary with executable permissions 37 | if ! install -m 755 /tmp/build/release/router "${TARGET_BIN_DIR}/"; then 38 | echo "Error: Failed to copy router binary" >&2 39 | exit 1 40 | fi 41 | 42 | # Copy binary with executable permissions 43 | if ! install -m 755 /tmp/build/release/savant_info "${TARGET_BIN_DIR}/"; then 44 | echo "Error: Failed to copy savant_info binary" >&2 45 | exit 1 46 | fi 47 | 48 | # Copy configuration file 49 | if ! install -m 644 /opt/savant-rs/services/router/assets/configuration.json "${TARGET_ETC_DIR}/configuration.json"; then 50 | echo "Error: Failed to copy configuration file" >&2 51 | exit 1 52 | fi 53 | 54 | # Copy Python modules 55 | if ! install -m 644 /opt/savant-rs/services/router/assets/python/module.py "${TARGET_MODULE_DIR}/"; then 56 | echo "Error: Failed to copy Python modules" >&2 57 | exit 1 58 | fi 59 | 60 | echo "All files copied successfully" 61 | --------------------------------------------------------------------------------