├── .cargo └── config.toml.release-build ├── .github ├── dependabot.yml └── workflows │ ├── release-plz.yml │ ├── release.yml │ ├── rust-clippy.yml │ ├── rust-fmt.yml │ ├── rust.yml │ └── typos.yml ├── .gitignore ├── .release-plz.toml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── Cross.toml ├── Dockerfile ├── LICENSE ├── README.md ├── _typos.toml ├── bin ├── CHANGELOG.md ├── CONSOLE_FRONTEND ├── Cargo.toml ├── certs │ ├── cluster.cert │ └── cluster.key ├── media_single.sh ├── public │ ├── .gitignore │ └── media │ │ ├── index.html │ │ ├── whep │ │ ├── index.html │ │ ├── whep.demo.js │ │ └── whep.js │ │ └── whip │ │ ├── index.html │ │ ├── whip.demo.js │ │ └── whip.js ├── src │ ├── errors.rs │ ├── http.rs │ ├── http │ │ ├── api_console.rs │ │ ├── api_console │ │ │ ├── cluster.rs │ │ │ ├── connector.rs │ │ │ └── user.rs │ │ ├── api_media.rs │ │ ├── api_media │ │ │ ├── rtpengine.rs │ │ │ ├── webrtc.rs │ │ │ ├── whep.rs │ │ │ └── whip.rs │ │ ├── api_metrics.rs │ │ ├── api_node.rs │ │ ├── api_token.rs │ │ └── utils │ │ │ ├── mod.rs │ │ │ ├── payload_protobuf.rs │ │ │ ├── payload_sdp.rs │ │ │ ├── remote_ip.rs │ │ │ ├── token.rs │ │ │ └── user_agent.rs │ ├── lib.rs │ ├── main.rs │ ├── node_metrics.rs │ ├── quinn │ │ ├── builder.rs │ │ ├── mod.rs │ │ ├── vnet.rs │ │ └── vsocket.rs │ ├── rpc.rs │ ├── seeds.rs │ ├── server.rs │ └── server │ │ ├── cert.rs │ │ ├── connector.rs │ │ ├── connector │ │ └── remote_rpc_handler.rs │ │ ├── console.rs │ │ ├── console │ │ ├── socket.rs │ │ └── storage.rs │ │ ├── gateway.rs │ │ ├── gateway │ │ ├── dest_selector.rs │ │ ├── ip_location.rs │ │ ├── local_rpc_handler.rs │ │ └── remote_rpc_handler.rs │ │ ├── media.rs │ │ ├── media │ │ ├── rpc_handler.rs │ │ └── runtime_worker.rs │ │ └── standalone.rs ├── standalone.sh ├── z0_connector_n4.sh ├── z0_console_n0.sh ├── z0_gate_n1.sh ├── z0_media_n2.sh ├── z0_media_n3.sh ├── z1_connector_n4.sh ├── z1_gate_n1.sh ├── z1_media_n2.sh └── z1_media_n3.sh ├── book.toml ├── deny.toml ├── docs ├── README.md ├── SUMMARY.md ├── contributor-guide │ ├── README.md │ ├── architecture.md │ ├── features │ │ ├── README.md │ │ ├── audio-mixer.md │ │ ├── authentication.md │ │ ├── cluster.md │ │ ├── recording.md │ │ └── simulcast-svc.md │ ├── getting-started.md │ ├── middlewares │ │ ├── README.md │ │ ├── logging.md │ │ ├── mix-minus.md │ │ └── whep.md │ ├── resource_clear.md │ ├── servers │ │ ├── README.md │ │ ├── connector.md │ │ ├── gateway.md │ │ └── media-server.md │ └── transports │ │ ├── README.md │ │ ├── rtp-engine.md │ │ ├── webrtc.md │ │ └── whip-whep.md ├── getting-started │ ├── README.md │ ├── faq.md │ ├── installation │ │ ├── README.md │ │ ├── auto-generate-node-id.md │ │ ├── console_screen.png │ │ ├── console_screen2.png │ │ ├── docker-compose.md │ │ ├── kubernetes.md │ │ ├── multi-zones.md │ │ ├── nat-traversal.md │ │ ├── network-discovery.md │ │ ├── single-zone.md │ │ └── standalone.md │ ├── quick-start │ │ ├── README.md │ │ ├── rtmp.md │ │ ├── rtp-engine.md │ │ ├── sample-application.md │ │ ├── webrtc-sdk.md │ │ └── whip-whep.md │ └── troubleshooting.md ├── imgs │ ├── architecture │ │ ├── endpoint.excalidraw.png │ │ ├── how-it-works.excalidraw.png │ │ ├── implement-layers.excalidraw.png │ │ ├── tasks.excalidraw.png │ │ └── why-it-fast.excalidraw.png │ ├── demo-monitor.png │ ├── demo-rtmp-config.png │ ├── demo-screen.jpg │ ├── features │ │ ├── audio-mixer.excalidraw.png │ │ └── sip.excalidraw.png │ ├── interconnected-network.drawio.svg │ ├── multi-zones-abstract.excalidraw.png │ ├── multi-zones.excalidraw.png │ ├── single-zone.excalidraw.png │ └── usecases │ │ ├── broadcast.excalidraw.png │ │ ├── cctv-extended.excalidraw.png │ │ └── video-conference.excalidraw.png ├── issue-template.md ├── pull-request-template.md └── user-guide │ ├── README.md │ ├── cluster-discovery.md │ ├── concepts.md │ ├── configuration.md │ ├── features │ ├── README.md │ ├── audio-mixer.md │ ├── authentication-and-multi-tenancy.md │ ├── cluster.md │ ├── extra_data-metadata.md │ ├── recording.md │ ├── simulcast-svc.md │ └── third-party-system-hook.md │ ├── integration.md │ ├── sdks.md │ ├── upgrade.md │ └── usage-examples.md ├── download-geodata.sh ├── mermaid-init.js ├── mermaid.min.js ├── packages ├── audio_mixer │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── media_codecs │ ├── CHANGELOG.md │ ├── Cargo.toml │ └── src │ │ ├── lib.rs │ │ ├── opus.rs │ │ ├── opus │ │ └── opus_wrap.rs │ │ ├── pcma.rs │ │ └── resample.rs ├── media_connector │ ├── Cargo.toml │ └── src │ │ ├── agent_service.rs │ │ ├── handler_service.rs │ │ ├── hooks.rs │ │ ├── hooks │ │ └── worker.rs │ │ ├── lib.rs │ │ ├── msg_queue.rs │ │ ├── sql_storage.rs │ │ └── sql_storage │ │ ├── entity.rs │ │ ├── entity │ │ ├── event.rs │ │ ├── peer.rs │ │ ├── peer_session.rs │ │ ├── room.rs │ │ └── session.rs │ │ ├── migration.rs │ │ └── migration │ │ ├── m20240626_0001_init.rs │ │ ├── m20240809_0001_change_node_id_i64.rs │ │ ├── m20240824_0001_add_room_destroy_and_record.rs │ │ └── m20240929_0001_add_multi_tenancy.rs ├── media_console_front │ ├── CHANGELOG.md │ ├── Cargo.toml │ ├── build.rs │ ├── react-app │ │ ├── .env.example │ │ ├── .gitignore │ │ ├── .prettierignore │ │ ├── .prettierrc │ │ ├── README.md │ │ ├── components.json │ │ ├── eslint.config.js │ │ ├── index.html │ │ ├── package.json │ │ ├── pnpm-lock.yaml │ │ ├── postcss.config.js │ │ ├── src │ │ │ ├── assets │ │ │ │ ├── index.ts │ │ │ │ ├── logo.svg │ │ │ │ └── sign-in-bg.webp │ │ │ ├── components │ │ │ │ ├── index.ts │ │ │ │ ├── pagination.tsx │ │ │ │ ├── text-copy.tsx │ │ │ │ ├── ui │ │ │ │ │ ├── accordion.tsx │ │ │ │ │ ├── alert-dialog.tsx │ │ │ │ │ ├── alert.tsx │ │ │ │ │ ├── aspect-ratio.tsx │ │ │ │ │ ├── avatar.tsx │ │ │ │ │ ├── badge.tsx │ │ │ │ │ ├── breadcrumb.tsx │ │ │ │ │ ├── button.tsx │ │ │ │ │ ├── calendar.tsx │ │ │ │ │ ├── card.tsx │ │ │ │ │ ├── carousel.tsx │ │ │ │ │ ├── chart.tsx │ │ │ │ │ ├── checkbox.tsx │ │ │ │ │ ├── collapsible.tsx │ │ │ │ │ ├── command.tsx │ │ │ │ │ ├── context-menu.tsx │ │ │ │ │ ├── dialog.tsx │ │ │ │ │ ├── drawer.tsx │ │ │ │ │ ├── dropdown-menu.tsx │ │ │ │ │ ├── form.tsx │ │ │ │ │ ├── hover-card.tsx │ │ │ │ │ ├── input-otp.tsx │ │ │ │ │ ├── input.tsx │ │ │ │ │ ├── label.tsx │ │ │ │ │ ├── menubar.tsx │ │ │ │ │ ├── navigation-menu.tsx │ │ │ │ │ ├── pagination.tsx │ │ │ │ │ ├── popover.tsx │ │ │ │ │ ├── progress.tsx │ │ │ │ │ ├── radio-group.tsx │ │ │ │ │ ├── resizable.tsx │ │ │ │ │ ├── scroll-area.tsx │ │ │ │ │ ├── select.tsx │ │ │ │ │ ├── separator.tsx │ │ │ │ │ ├── sheet.tsx │ │ │ │ │ ├── sidebar.tsx │ │ │ │ │ ├── skeleton.tsx │ │ │ │ │ ├── slider.tsx │ │ │ │ │ ├── sonner.tsx │ │ │ │ │ ├── switch.tsx │ │ │ │ │ ├── table.tsx │ │ │ │ │ ├── tabs.tsx │ │ │ │ │ ├── textarea.tsx │ │ │ │ │ ├── toast.tsx │ │ │ │ │ ├── toaster.tsx │ │ │ │ │ ├── toggle-group.tsx │ │ │ │ │ ├── toggle.tsx │ │ │ │ │ └── tooltip.tsx │ │ │ │ └── zone │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── zone-detail-card.tsx │ │ │ │ │ └── zone-detail-section.tsx │ │ │ ├── config │ │ │ │ └── env.ts │ │ │ ├── containers │ │ │ │ ├── auth │ │ │ │ │ ├── index.ts │ │ │ │ │ └── sign-in │ │ │ │ │ │ └── index.tsx │ │ │ │ ├── index.ts │ │ │ │ ├── summary │ │ │ │ │ └── index.tsx │ │ │ │ ├── visualization │ │ │ │ │ ├── graph.ts │ │ │ │ │ └── index.tsx │ │ │ │ └── zones │ │ │ │ │ ├── detail │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── events │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── list │ │ │ │ │ ├── components │ │ │ │ │ │ ├── create-zone.tsx │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── zone-item.tsx │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── peers │ │ │ │ │ ├── components │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── logs-peer-item.tsx │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── rooms │ │ │ │ │ └── index.tsx │ │ │ │ │ └── sessions │ │ │ │ │ ├── components │ │ │ │ │ ├── index.ts │ │ │ │ │ └── logs-peer-item.tsx │ │ │ │ │ └── index.tsx │ │ │ ├── hooks │ │ │ │ ├── api │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── use-auth.ts │ │ │ │ │ ├── use-connectors │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── types.ts │ │ │ │ │ ├── use-networks │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ └── types.ts │ │ │ │ │ └── use-zones │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── types.ts │ │ │ │ ├── index.ts │ │ │ │ ├── use-api.ts │ │ │ │ ├── use-logout.ts │ │ │ │ ├── use-menu.tsx │ │ │ │ ├── use-mobile.tsx │ │ │ │ └── use-toast.ts │ │ │ ├── index.css │ │ │ ├── jotai │ │ │ │ └── index.ts │ │ │ ├── layouts │ │ │ │ ├── app-sidebar.tsx │ │ │ │ ├── header.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── nav-main.tsx │ │ │ │ ├── nav-secondary.tsx │ │ │ │ └── nav-user.tsx │ │ │ ├── lib │ │ │ │ └── utils.ts │ │ │ ├── main.tsx │ │ │ ├── providers │ │ │ │ ├── app-provider.tsx │ │ │ │ ├── index.ts │ │ │ │ ├── private-provider.tsx │ │ │ │ ├── react-query-provider.tsx │ │ │ │ └── theme-provider.tsx │ │ │ ├── routes │ │ │ │ └── index.tsx │ │ │ ├── types │ │ │ │ └── index.ts │ │ │ ├── utils │ │ │ │ ├── common.ts │ │ │ │ ├── constants.ts │ │ │ │ ├── cookies.ts │ │ │ │ ├── index.ts │ │ │ │ └── storage.ts │ │ │ └── vite-env.d.ts │ │ ├── tsconfig.app.json │ │ ├── tsconfig.app.tsbuildinfo │ │ ├── tsconfig.json │ │ ├── tsconfig.node.json │ │ ├── tsconfig.node.tsbuildinfo │ │ └── vite.config.ts │ └── src │ │ ├── dev_proxy.rs │ │ └── lib.rs ├── media_core │ ├── CHANGELOG.md │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── cluster.rs │ │ ├── cluster │ │ ├── id_generator.rs │ │ ├── room.rs │ │ └── room │ │ │ ├── audio_mixer.rs │ │ │ ├── audio_mixer │ │ │ ├── manual.rs │ │ │ ├── publisher.rs │ │ │ └── subscriber.rs │ │ │ ├── media_track.rs │ │ │ ├── media_track │ │ │ ├── publisher.rs │ │ │ └── subscriber.rs │ │ │ ├── message_channel.rs │ │ │ ├── message_channel │ │ │ ├── publisher.rs │ │ │ └── subscriber.rs │ │ │ └── metadata.rs │ │ ├── endpoint.rs │ │ ├── endpoint │ │ ├── internal.rs │ │ └── internal │ │ │ ├── bitrate_allocator.rs │ │ │ ├── bitrate_allocator │ │ │ ├── egress.rs │ │ │ └── ingress.rs │ │ │ ├── local_track.rs │ │ │ ├── local_track │ │ │ ├── packet_selector.rs │ │ │ ├── packet_selector │ │ │ │ ├── video_h264_sim.rs │ │ │ │ ├── video_single.rs │ │ │ │ ├── video_vp8_sim.rs │ │ │ │ └── video_vp9_svc.rs │ │ │ └── voice_activity.rs │ │ │ └── remote_track.rs │ │ ├── errors.rs │ │ ├── lib.rs │ │ └── transport.rs ├── media_gateway │ ├── Cargo.toml │ └── src │ │ ├── agent_service.rs │ │ ├── lib.rs │ │ ├── store.rs │ │ ├── store │ │ └── service.rs │ │ └── store_service.rs ├── media_record │ ├── CHANGELOG.md │ ├── Cargo.toml │ ├── bin │ │ ├── convert_record_cli.rs │ │ └── convert_record_worker.rs │ └── src │ │ ├── convert.rs │ │ ├── convert │ │ ├── codec │ │ │ ├── mod.rs │ │ │ ├── vpx_demuxer.rs │ │ │ └── vpx_writer.rs │ │ ├── composer.rs │ │ ├── composer │ │ │ ├── audio_mixer.rs │ │ │ ├── audio_mixer │ │ │ │ └── mixer_buffer.rs │ │ │ └── video_composer.rs │ │ ├── transmuxer.rs │ │ └── transmuxer │ │ │ ├── summary.rs │ │ │ └── track_writer.rs │ │ ├── lib.rs │ │ ├── raw_record.rs │ │ ├── raw_record │ │ ├── chunk_reader.rs │ │ ├── chunk_writer.rs │ │ ├── peer_reader.rs │ │ ├── room_reader.rs │ │ └── session_reader.rs │ │ ├── session.rs │ │ ├── storage │ │ ├── disk.rs │ │ ├── memory.rs │ │ └── mod.rs │ │ └── worker.rs ├── media_runner │ ├── CHANGELOG.md │ ├── Cargo.toml │ └── src │ │ ├── lib.rs │ │ └── worker.rs ├── media_secure │ ├── Cargo.toml │ └── src │ │ ├── jwt.rs │ │ └── lib.rs ├── media_utils │ ├── Cargo.toml │ ├── benches │ │ └── map_bench.rs │ └── src │ │ ├── count.rs │ │ ├── embed_files.rs │ │ ├── f16.rs │ │ ├── indexmap_2d.rs │ │ ├── lib.rs │ │ ├── select.rs │ │ ├── select │ │ ├── select2.rs │ │ └── select3.rs │ │ ├── seq_extend.rs │ │ ├── seq_rewrite.rs │ │ ├── state.rs │ │ ├── time.rs │ │ ├── ts_rewrite.rs │ │ └── uri.rs ├── multi_tenancy │ ├── Cargo.toml │ └── src │ │ ├── lib.rs │ │ └── store.rs ├── protocol │ ├── Cargo.toml │ ├── README.md │ ├── build.rs │ ├── build_templates │ │ └── service.teml │ ├── proto │ │ ├── README.md │ │ ├── cluster │ │ │ ├── connector.proto │ │ │ └── gateway.proto │ │ ├── record │ │ │ └── file_rec.proto │ │ ├── sdk │ │ │ ├── features.mixer.proto │ │ │ ├── features.proto │ │ │ ├── gateway.proto │ │ │ └── session.proto │ │ ├── shared.proto │ │ └── sync.sh │ └── src │ │ ├── cluster.rs │ │ ├── connector.rs │ │ ├── endpoint.rs │ │ ├── endpoint │ │ ├── audio_mixer.rs │ │ └── track.rs │ │ ├── gateway.rs │ │ ├── lib.rs │ │ ├── media.rs │ │ ├── message_channel.rs │ │ ├── multi_tenancy.rs │ │ ├── protobuf │ │ ├── cluster_connector.rs │ │ ├── cluster_gateway.rs │ │ ├── features.mixer.rs │ │ ├── features.rs │ │ ├── gateway.rs │ │ ├── mod.rs │ │ ├── session.rs │ │ └── shared.rs │ │ ├── record.rs │ │ ├── rpc.rs │ │ ├── rpc │ │ └── quinn.rs │ │ ├── tokens.rs │ │ ├── transport.rs │ │ └── transport │ │ ├── rtpengine.rs │ │ ├── webrtc.rs │ │ ├── whep.rs │ │ └── whip.rs ├── transport_rtpengine │ ├── CHANGELOG.md │ ├── Cargo.toml │ └── src │ │ ├── lib.rs │ │ ├── transport.rs │ │ └── worker.rs └── transport_webrtc │ ├── CHANGELOG.md │ ├── Cargo.toml │ └── src │ ├── lib.rs │ ├── media │ ├── bit_read.rs │ ├── h264.rs │ ├── mod.rs │ ├── vp8.rs │ └── vp9.rs │ ├── shared_port.rs │ ├── transport.rs │ ├── transport │ ├── bwe_state.rs │ ├── webrtc.rs │ ├── webrtc │ │ ├── local_track.rs │ │ └── remote_track.rs │ ├── whep.rs │ └── whip.rs │ └── worker.rs ├── renovate.json ├── rust-toolchain.toml └── rustfmt.toml /.cargo/config.toml.release-build: -------------------------------------------------------------------------------- 1 | [net] 2 | git-fetch-with-cli = true 3 | 4 | [patch.crates-io] 5 | libsoxr-sys = { git = "https://github.com/giangndm/libsoxr-sys.git", rev = "3154931420e385ef2d9d707755412f4fbfb7519a" } 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "cargo" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/release-plz.yml: -------------------------------------------------------------------------------- 1 | name: Release-plz 2 | 3 | permissions: 4 | pull-requests: write 5 | contents: write 6 | 7 | on: 8 | push: 9 | branches: 10 | - master 11 | 12 | jobs: 13 | release-plz: 14 | name: Release-plz 15 | runs-on: ubuntu-22.04 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@v4 19 | with: 20 | fetch-depth: 0 21 | token: ${{ secrets.RELEASE_PLZ_TOKEN }} # PAT secret 22 | 23 | - name: Install Rust toolchain 24 | uses: dtolnay/rust-toolchain@stable 25 | 26 | - name: Install dev-tools 27 | run: sudo apt-get install -y --no-install-recommends pkg-config musl-dev musl-tools protobuf-compiler 28 | 29 | - name: Install deps 30 | run: sudo apt-get install -y --no-install-recommends libssl-dev libopus-dev libfdk-aac-dev libsoxr-dev 31 | 32 | - name: Run release-plz 33 | uses: MarcoIeni/release-plz-action@v0.5 34 | env: 35 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 36 | GITHUB_TOKEN: ${{ secrets.RELEASE_PLZ_TOKEN }} # PAT secret 37 | -------------------------------------------------------------------------------- /.github/workflows/rust-fmt.yml: -------------------------------------------------------------------------------- 1 | name: rust-fmt analyze 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [ "master" ] 9 | schedule: 10 | - cron: '29 19 * * 2' 11 | 12 | concurrency: 13 | # One build per PR, branch or tag 14 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 15 | cancel-in-progress: true 16 | 17 | env: 18 | CARGO_TERM_COLOR: always 19 | 20 | jobs: 21 | rust-fmt-analyze: 22 | name: Run rust-fmt analyzing 23 | runs-on: ubuntu-22.04 24 | steps: 25 | - name: Checkout code 26 | uses: actions/checkout@v4 27 | 28 | - uses: actions/cache@v3 29 | with: 30 | path: | 31 | ~/.cargo/bin/ 32 | ~/.cargo/registry/index/ 33 | ~/.cargo/registry/cache/ 34 | ~/.cargo/git/db/ 35 | target/ 36 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 37 | 38 | - name: cargo fmt 39 | run: cargo fmt --all -- --check 40 | -------------------------------------------------------------------------------- /.github/workflows/typos.yml: -------------------------------------------------------------------------------- 1 | name: Typos 2 | 3 | on: 4 | push: 5 | branches: ["master"] 6 | pull_request: 7 | branches: ["master"] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | typos: 14 | runs-on: ubuntu-22.04 15 | env: 16 | CARGO_TERM_COLOR: always 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Check spelling issues 20 | uses: crate-ci/typos@v1.21.0 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | tarpaulin-report.html 3 | .atm0s 4 | /maxminddb-data 5 | .vscode 6 | book 7 | *.db -------------------------------------------------------------------------------- /.release-plz.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | git_release_enable = false 3 | 4 | [[package]] 5 | name = "atm0s-media-server" 6 | git_release_enable = true 7 | git_release_name = "v{{ version }}" 8 | git_tag_name = "v{{ version }}" 9 | changelog_include = [ 10 | "atm0s-media-server-audio-mixer", 11 | "atm0s-media-server-utils", 12 | "atm0s-media-server-core", 13 | "atm0s-media-server-runner", 14 | "atm0s-media-server-protocol", 15 | "atm0s-media-server-console-front", 16 | "atm0s-media-server-connector", 17 | "atm0s-media-server-record", 18 | "atm0s-media-server-gateway", 19 | "atm0s-media-server-audio-mixer", 20 | "atm0s-media-server-secure", 21 | "atm0s-media-server-codecs", 22 | "atm0s-media-server-multi-tenancy", 23 | "atm0s-media-server-transport-webrtc", 24 | "atm0s-media-server-transport-rtpengine", 25 | ] 26 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Welcome to 8xFF! To learn more about contributing to the [8xFF Repo](README.md), check out the [Contributor's Doc](https://8xff.github.io/media-docs/). 4 | -------------------------------------------------------------------------------- /Cross.toml: -------------------------------------------------------------------------------- 1 | [build.env] 2 | passthrough = [ 3 | "RUST_BACKTRACE", 4 | "RUST_LOG", 5 | "LIBOPUS_STATIC", 6 | "LIBSOXR_STATIC", 7 | "OPENSSL_STATIC", 8 | "SKIP_BUILD_CONSOLE_FRONT" 9 | ] 10 | 11 | [target.aarch64-unknown-linux-gnu] 12 | pre-build = [ 13 | "apt-get update && apt-get --assume-yes install pkg-config protobuf-compiler libssl-dev" 14 | ] 15 | [target.aarch64-unknown-linux-musl] 16 | pre-build = [ 17 | "apt-get update && apt-get --assume-yes install pkg-config protobuf-compiler libssl-dev" 18 | ] 19 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:24.04 as base 2 | ARG TARGETPLATFORM 3 | COPY . /tmp 4 | WORKDIR /tmp 5 | 6 | RUN echo $TARGETPLATFORM 7 | RUN ls -R /tmp/ 8 | # move the binary to root based on platform 9 | RUN case $TARGETPLATFORM in \ 10 | "linux/amd64") BUILD=x86_64-unknown-linux-gnu ;; \ 11 | "linux/arm64") BUILD=aarch64-unknown-linux-gnu ;; \ 12 | *) exit 1 ;; \ 13 | esac; \ 14 | mv /tmp/$BUILD/atm0s-media-server-$BUILD /atm0s-media-server; \ 15 | mv /tmp/$BUILD/convert_record_cli-$BUILD /convert_record_cli; \ 16 | mv /tmp/$BUILD/convert_record_worker-$BUILD /convert_record_worker; \ 17 | chmod +x /atm0s-media-server; \ 18 | chmod +x /convert_record_cli; \ 19 | chmod +x /convert_record_worker 20 | 21 | FROM ubuntu:24.04 22 | 23 | # install wget & curl 24 | RUN apt update && apt install -y wget curl && apt clean && rm -rf /var/lib/apt/lists/* 25 | 26 | COPY maxminddb-data /maxminddb-data 27 | COPY --from=base /atm0s-media-server /atm0s-media-server 28 | COPY --from=base /convert_record_cli /convert_record_cli 29 | COPY --from=base /convert_record_worker /convert_record_worker 30 | ENTRYPOINT ["/atm0s-media-server"] 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 8xFF 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /_typos.toml: -------------------------------------------------------------------------------- 1 | [files] 2 | extend-exclude = ["*.js", "CHANGELOG.md", "Cargo.lock", "*.sdp", "*.cert", "*.key"] 3 | 4 | [default] 5 | extend-ignore-re = [ 6 | "eyJhbGciOiJIUzI1NiJ9.[A-Za-z0-9_-]+.[A-Za-z0-9_-]+", 7 | "pk.ey[A-Za-z0-9_-]+.[A-Za-z0-9_-]+", 8 | "H264_NALU_[A-Z_-]+", 9 | ] 10 | 11 | [default.extend-words] 12 | stap = "stap" -------------------------------------------------------------------------------- /bin/CONSOLE_FRONTEND: -------------------------------------------------------------------------------- 1 | 023efcb0a308e5e3df1849f0deee3400912325a0 2 | -------------------------------------------------------------------------------- /bin/certs/cluster.cert: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/8xFF/atm0s-media-server/9ea8c02d3bc9b946edea1d566020da94390c98ca/bin/certs/cluster.cert -------------------------------------------------------------------------------- /bin/certs/cluster.key: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/8xFF/atm0s-media-server/9ea8c02d3bc9b946edea1d566020da94390c98ca/bin/certs/cluster.key -------------------------------------------------------------------------------- /bin/media_single.sh: -------------------------------------------------------------------------------- 1 | RUST_LOG=atm0s_sdn_network=error,info \ 2 | RUST_BACKTRACE=1 \ 3 | cargo run -- \ 4 | --sdn-zone-id 0 \ 5 | --sdn-zone-node-id 1 \ 6 | --workers 1 \ 7 | --http-port 3000 \ 8 | media \ 9 | --enable-token-api \ 10 | --disable-gateway-agent \ 11 | --disable-connector-agent 12 | -------------------------------------------------------------------------------- /bin/public/.gitignore: -------------------------------------------------------------------------------- 1 | console 2 | -------------------------------------------------------------------------------- /bin/public/media/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | Whip broadcast 5 |
6 |
7 | Whep viewer 8 |
9 | 10 | -------------------------------------------------------------------------------- /bin/public/media/whep/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Whep 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 |
12 |
13 | 14 |
15 | 16 | -------------------------------------------------------------------------------- /bin/public/media/whep/whep.demo.js: -------------------------------------------------------------------------------- 1 | import { WHEPClient } from "./whep.js" 2 | 3 | window.start = async () => { 4 | console.log("Will start"); 5 | //Create peerconnection 6 | const pc = window.pc = new RTCPeerConnection(); 7 | 8 | //Add recv only transceivers 9 | pc.addTransceiver("audio", { direction: 'recvonly' }); 10 | pc.addTransceiver("video", { direction: 'recvonly' }); 11 | 12 | let stream = new MediaStream(); 13 | document.querySelector("video").srcObject = stream; 14 | pc.ontrack = (event) => { 15 | stream.addTrack(event.track); 16 | } 17 | 18 | //Create whep client 19 | const whep = new WHEPClient(); 20 | 21 | const url = "/whep/endpoint"; 22 | const token = document.getElementById("room-id").value; 23 | 24 | //Start viewing 25 | whep.view(pc, url, token); 26 | 27 | window.whep_instance = whep; 28 | } 29 | 30 | window.stop = async () => { 31 | if (window.whep_instance) { 32 | window.whep_instance.stop(); 33 | } 34 | 35 | document.getElementById("video").srcObject = null; 36 | } -------------------------------------------------------------------------------- /bin/public/media/whip/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Whip 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 |
12 |
13 | 14 |
15 | 16 | -------------------------------------------------------------------------------- /bin/public/media/whip/whip.demo.js: -------------------------------------------------------------------------------- 1 | import { WHIPClient } from "./whip.js"; 2 | 3 | window.start = async () => { 4 | console.log("Will start"); 5 | if (window.whip_instance) { 6 | window.whip_instance.stop(); 7 | } 8 | 9 | if (window.stream_instance) { 10 | window.stream_instance.getTracks().forEach((track) => track.stop()); 11 | } 12 | 13 | //Get mic+cam 14 | const stream = await navigator.mediaDevices.getUserMedia({ 15 | audio: true, 16 | video: true, 17 | }); 18 | 19 | document.getElementById("video").srcObject = stream; 20 | 21 | //Create peerconnection 22 | const pc = new RTCPeerConnection(); 23 | 24 | //Send all tracks 25 | for (const track of stream.getTracks()) { 26 | //You could add simulcast too here 27 | pc.addTransceiver(track, { 28 | direction: "sendonly", 29 | streams: [stream], 30 | sendEncodings: [ 31 | { rid: "0", active: true }, 32 | { rid: "1", active: true }, 33 | { rid: "2", active: true }, 34 | ], 35 | }); 36 | } 37 | 38 | //Create whip client 39 | const whip = new WHIPClient(); 40 | 41 | const url = "/whip/endpoint"; 42 | const token = document.getElementById("room-id").value; 43 | 44 | //Start publishing 45 | whip.publish(pc, url, token); 46 | 47 | window.whip_instance = whip; 48 | window.stream_instance = stream; 49 | }; 50 | 51 | window.stop = async () => { 52 | if (window.whip_instance) { 53 | window.whip_instance.stop(); 54 | } 55 | 56 | if (window.stream_instance) { 57 | window.stream_instance.getTracks().forEach((track) => track.stop()); 58 | } 59 | 60 | document.getElementById("video").srcObject = null; 61 | }; 62 | -------------------------------------------------------------------------------- /bin/src/errors.rs: -------------------------------------------------------------------------------- 1 | #[derive(num_enum::TryFromPrimitive, num_enum::IntoPrimitive, derive_more::Display)] 2 | #[repr(u32)] 3 | pub enum MediaServerError { 4 | GatewayRpcError = 0x00020001, 5 | InvalidConnId = 0x00020002, 6 | NodePoolEmpty = 0x00020003, 7 | MediaResError = 0x00020004, 8 | NotImplemented = 0x00020005, 9 | NodeTimeout = 0x00020006, 10 | } 11 | -------------------------------------------------------------------------------- /bin/src/http/api_console.rs: -------------------------------------------------------------------------------- 1 | use std::net::SocketAddr; 2 | 3 | use media_server_protocol::{ 4 | protobuf::cluster_connector::MediaConnectorServiceClient, 5 | rpc::quinn::{QuinnClient, QuinnStream}, 6 | }; 7 | use media_server_secure::{jwt::MediaConsoleSecureJwt, MediaConsoleSecure}; 8 | use poem::Request; 9 | use poem_openapi::{auth::ApiKey, SecurityScheme}; 10 | 11 | use crate::server::console_storage::StorageShared; 12 | 13 | pub mod cluster; 14 | pub mod connector; 15 | pub mod user; 16 | 17 | #[derive(Clone)] 18 | pub struct ConsoleApisCtx { 19 | pub secure: MediaConsoleSecureJwt, //TODO make it generic 20 | pub storage: StorageShared, 21 | pub connector: MediaConnectorServiceClient, 22 | } 23 | 24 | /// ApiKey authorization 25 | #[derive(SecurityScheme)] 26 | #[oai(ty = "api_key", key_name = "X-API-Key", key_in = "header", checker = "api_checker")] 27 | struct ConsoleAuthorization(()); 28 | 29 | async fn api_checker(req: &Request, api_key: ApiKey) -> Option<()> { 30 | let data = req.data::()?; 31 | data.secure.validate_token(&api_key.key).then_some(()) 32 | } 33 | -------------------------------------------------------------------------------- /bin/src/http/api_console/user.rs: -------------------------------------------------------------------------------- 1 | use super::{super::Response, ConsoleApisCtx}; 2 | use media_server_secure::MediaConsoleSecure; 3 | use poem::web::Data; 4 | use poem_openapi::{payload::Json, OpenApi}; 5 | 6 | #[derive(poem_openapi::Object)] 7 | pub struct UserLoginReq { 8 | pub secret: String, 9 | } 10 | 11 | #[derive(poem_openapi::Object)] 12 | pub struct UserLoginRes { 13 | pub token: String, 14 | } 15 | 16 | pub struct Apis; 17 | 18 | #[OpenApi] 19 | impl Apis { 20 | /// login with user credentials 21 | #[oai(path = "/login", method = "post")] 22 | async fn user_login(&self, Data(ctx): Data<&ConsoleApisCtx>, body: Json) -> Json> { 23 | if ctx.secure.validate_secret(&body.secret) { 24 | Json(Response { 25 | status: true, 26 | data: Some(UserLoginRes { token: ctx.secure.generate_token() }), 27 | ..Default::default() 28 | }) 29 | } else { 30 | Json(Response { 31 | status: false, 32 | error: Some("WRONG_SECRET".to_string()), 33 | ..Default::default() 34 | }) 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /bin/src/http/api_media.rs: -------------------------------------------------------------------------------- 1 | mod rtpengine; 2 | mod webrtc; 3 | mod whep; 4 | mod whip; 5 | 6 | pub use rtpengine::RtpengineApis; 7 | pub use webrtc::WebrtcApis; 8 | pub use whep::WhepApis; 9 | pub use whip::WhipApis; 10 | -------------------------------------------------------------------------------- /bin/src/http/api_metrics.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | 3 | use media_server_utils::get_all_counts; 4 | use poem_openapi::{payload::Json, OpenApi}; 5 | 6 | pub struct Apis; 7 | 8 | #[OpenApi] 9 | impl Apis { 10 | #[oai(path = "/counts", method = "get")] 11 | async fn get_counts(&self) -> Json> { 12 | Json(get_all_counts().into_iter().map(|(k, v)| (k.to_string(), v)).collect()) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /bin/src/http/api_node.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use atm0s_sdn::NodeAddr; 4 | use poem_openapi::{ 5 | payload::{Json, PlainText}, 6 | OpenApi, 7 | }; 8 | use tokio::sync::{mpsc::Sender, oneshot}; 9 | 10 | pub struct NodeApiCtx { 11 | pub address: NodeAddr, 12 | pub dump_tx: Sender>, 13 | } 14 | 15 | pub struct Apis { 16 | ctx: NodeApiCtx, 17 | } 18 | 19 | impl Apis { 20 | pub fn new(ctx: NodeApiCtx) -> Self { 21 | Self { ctx } 22 | } 23 | } 24 | 25 | #[OpenApi] 26 | impl Apis { 27 | #[oai(path = "/address", method = "get")] 28 | async fn get_address(&self) -> PlainText { 29 | PlainText(self.ctx.address.to_string()) 30 | } 31 | 32 | #[oai(path = "/router_dump", method = "get")] 33 | async fn get_router_dump(&self) -> Json { 34 | let (tx, rx) = oneshot::channel(); 35 | self.ctx.dump_tx.send(tx).await.expect("should send"); 36 | match tokio::time::timeout(Duration::from_millis(1000), rx).await { 37 | Ok(Ok(v)) => Json(serde_json::json!({ 38 | "status": true, 39 | "data": v 40 | })), 41 | Ok(Err(e)) => Json(serde_json::json!({ 42 | "status": false, 43 | "error": e.to_string() 44 | })), 45 | Err(_e) => Json(serde_json::json!({ 46 | "status": false, 47 | "error": "timeout" 48 | })), 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /bin/src/http/utils/mod.rs: -------------------------------------------------------------------------------- 1 | mod payload_protobuf; 2 | mod payload_sdp; 3 | mod remote_ip; 4 | mod token; 5 | mod user_agent; 6 | 7 | pub use payload_protobuf::*; 8 | pub use payload_sdp::*; 9 | pub use remote_ip::*; 10 | pub use token::*; 11 | pub use user_agent::*; 12 | -------------------------------------------------------------------------------- /bin/src/http/utils/remote_ip.rs: -------------------------------------------------------------------------------- 1 | use std::{net::IpAddr, ops::Deref}; 2 | 3 | use poem::{http::StatusCode, FromRequest}; 4 | 5 | #[derive(Debug)] 6 | pub struct RemoteIpAddr(pub IpAddr); 7 | 8 | impl<'a> FromRequest<'a> for RemoteIpAddr { 9 | async fn from_request(req: &'a poem::Request, _body: &mut poem::RequestBody) -> poem::Result { 10 | let headers = req.headers(); 11 | if let Some(remote_addr) = headers.get("X-Forwarded-For") { 12 | let remote_addr = remote_addr.to_str().map_err(|_| poem::Error::from_string("Bad Request", StatusCode::BAD_REQUEST))?; 13 | let remote_addr = remote_addr.split(',').next().ok_or(poem::Error::from_string("Bad Request", StatusCode::BAD_REQUEST))?; 14 | Ok(RemoteIpAddr(remote_addr.parse().map_err(|_| poem::Error::from_string("Invalid IP address", StatusCode::BAD_REQUEST))?)) 15 | } else if let Some(remote_addr) = headers.get("X-Real-IP") { 16 | let remote_addr = remote_addr.to_str().map_err(|_| poem::Error::from_string("Bad Request", StatusCode::BAD_REQUEST))?; 17 | Ok(RemoteIpAddr(remote_addr.parse().map_err(|_| poem::Error::from_string("Invalid IP address", StatusCode::BAD_REQUEST))?)) 18 | } else { 19 | match req.remote_addr().deref() { 20 | poem::Addr::SocketAddr(addr) => Ok(RemoteIpAddr(addr.ip())), 21 | _ => Err(poem::Error::from_string("Bad Request", StatusCode::BAD_REQUEST)), 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /bin/src/http/utils/token.rs: -------------------------------------------------------------------------------- 1 | use poem_openapi::{auth::Bearer, SecurityScheme}; 2 | 3 | #[derive(SecurityScheme)] 4 | #[oai(rename = "Token Authorization", ty = "bearer", key_in = "header", key_name = "Authorization")] 5 | pub struct TokenAuthorization(pub Bearer); 6 | -------------------------------------------------------------------------------- /bin/src/http/utils/user_agent.rs: -------------------------------------------------------------------------------- 1 | use poem::{http::StatusCode, FromRequest}; 2 | 3 | #[derive(Debug)] 4 | pub struct UserAgent(pub String); 5 | 6 | impl<'a> FromRequest<'a> for UserAgent { 7 | async fn from_request(req: &'a poem::Request, _body: &mut poem::RequestBody) -> poem::Result { 8 | let headers = req.headers(); 9 | let user_agent = headers.get("User-Agent").ok_or(poem::Error::from_string("Bad Request", StatusCode::BAD_REQUEST))?; 10 | let user_agent = user_agent.to_str().map_err(|_| poem::Error::from_string("Bad Request", StatusCode::BAD_REQUEST))?; 11 | Ok(UserAgent(user_agent.into())) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /bin/src/quinn/mod.rs: -------------------------------------------------------------------------------- 1 | mod builder; 2 | mod vnet; 3 | mod vsocket; 4 | 5 | pub use builder::{make_quinn_client, make_quinn_server}; 6 | pub use vnet::VirtualNetwork; 7 | -------------------------------------------------------------------------------- /bin/src/rpc.rs: -------------------------------------------------------------------------------- 1 | pub struct Rpc { 2 | pub req: Req, 3 | pub answer_tx: tokio::sync::oneshot::Sender, 4 | } 5 | 6 | impl Rpc { 7 | pub fn new(req: Req) -> (Self, tokio::sync::oneshot::Receiver) { 8 | let (answer_tx, answer_rx) = tokio::sync::oneshot::channel(); 9 | (Self { req, answer_tx }, answer_rx) 10 | } 11 | 12 | #[allow(unused)] 13 | pub fn res(self, res: Res) { 14 | let _ = self.answer_tx.send(res); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /bin/src/seeds.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use atm0s_sdn::{NodeAddr, NodeId}; 4 | use tokio::sync::mpsc::Sender; 5 | 6 | /// Fetch node addrs from the given url. 7 | /// The url should return a list of node addrs in JSON format or a single node addr. 8 | async fn fetch_node_addrs_from_api(url: &str) -> Result, String> { 9 | let resp = reqwest::get(url).await.map_err(|e| e.to_string())?; 10 | let content = resp.text().await.map_err(|e| e.to_string())?; 11 | if content.starts_with('[') { 12 | let node_addrs: Vec = serde_json::from_str(&content).map_err(|e| e.to_string())?; 13 | Ok(node_addrs.into_iter().flat_map(|addr| NodeAddr::from_str(&addr)).collect()) 14 | } else { 15 | Ok(vec![NodeAddr::from_str(&content).map_err(|e| e.to_string())?]) 16 | } 17 | } 18 | 19 | /// Refresh seeds from url and send them to seed_tx 20 | pub fn refresh_seeds(node_id: NodeId, seeds: &[NodeAddr], url: Option<&str>, seed_tx: Sender) { 21 | for seed in seeds.iter() { 22 | let _ = seed_tx.try_send(seed.clone()); 23 | } 24 | 25 | if let Some(url) = url { 26 | let seed_tx = seed_tx.clone(); 27 | let url = url.to_string(); 28 | tokio::spawn(async move { 29 | log::info!("Generate seeds from uri {}", url); 30 | match fetch_node_addrs_from_api(&url).await { 31 | Ok(seeds) => { 32 | log::info!("Generated seeds {:?}", seeds); 33 | for seed in seeds.into_iter().filter(|s| s.node_id() != node_id) { 34 | seed_tx.send(seed).await.expect("Should send seed"); 35 | } 36 | } 37 | Err(e) => { 38 | log::error!("Failed to fetch seeds from uri: {}", e); 39 | } 40 | } 41 | }); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /bin/src/server.rs: -------------------------------------------------------------------------------- 1 | use clap::Subcommand; 2 | 3 | #[cfg(feature = "cert_utils")] 4 | mod cert; 5 | #[cfg(feature = "connector")] 6 | pub mod connector; 7 | #[cfg(feature = "console")] 8 | pub mod console; 9 | #[cfg(feature = "gateway")] 10 | pub mod gateway; 11 | #[cfg(feature = "media")] 12 | pub mod media; 13 | #[cfg(feature = "standalone")] 14 | pub mod standalone; 15 | 16 | #[cfg(feature = "cert_utils")] 17 | pub use cert::run_cert_utils; 18 | #[cfg(feature = "connector")] 19 | pub use connector::run_media_connector; 20 | #[cfg(feature = "console")] 21 | pub use console::{run_console_server, storage as console_storage}; 22 | #[cfg(feature = "gateway")] 23 | pub use gateway::run_media_gateway; 24 | #[cfg(feature = "media")] 25 | pub use media::run_media_server; 26 | #[cfg(feature = "standalone")] 27 | pub use standalone::run_standalone; 28 | 29 | #[derive(Debug, Subcommand)] 30 | pub enum ServerType { 31 | #[cfg(feature = "console")] 32 | Console(console::Args), 33 | #[cfg(feature = "gateway")] 34 | Gateway(gateway::Args), 35 | #[cfg(feature = "connector")] 36 | Connector(connector::Args), 37 | #[cfg(feature = "media")] 38 | Media(media::Args), 39 | #[cfg(feature = "cert_utils")] 40 | Cert(cert::Args), 41 | #[cfg(feature = "standalone")] 42 | Standalone(standalone::Args), 43 | } 44 | -------------------------------------------------------------------------------- /bin/src/server/cert.rs: -------------------------------------------------------------------------------- 1 | use std::time::{SystemTime, UNIX_EPOCH}; 2 | 3 | use clap::Parser; 4 | 5 | /// A Certs util for quic, which generate der cert and key based on domain 6 | #[derive(Debug, Parser)] 7 | pub struct Args { 8 | /// Domains 9 | #[arg(env, long)] 10 | domains: Vec, 11 | } 12 | 13 | pub async fn run_cert_utils(args: Args) -> Result<(), Box> { 14 | let cert = rcgen::generate_simple_self_signed(args.domains)?; 15 | let start = SystemTime::now(); 16 | let since_the_epoch = start.duration_since(UNIX_EPOCH).expect("Time went backwards").as_millis(); 17 | std::fs::write(format!("./certificate-{}.cert", since_the_epoch), cert.cert.der())?; 18 | std::fs::write(format!("./certificate-{}.key", since_the_epoch), cert.key_pair.serialize_der())?; 19 | Ok(()) 20 | } 21 | -------------------------------------------------------------------------------- /bin/src/server/gateway/ip_location.rs: -------------------------------------------------------------------------------- 1 | use std::net::IpAddr; 2 | 3 | use maxminddb::Reader; 4 | 5 | pub struct Ip2Location { 6 | city_reader: Reader>, 7 | } 8 | 9 | impl Ip2Location { 10 | pub fn new(database_city: &str) -> Self { 11 | let city_reader = maxminddb::Reader::open_readfile(database_city).expect("Failed to open geoip database"); 12 | Self { city_reader } 13 | } 14 | 15 | pub fn get_location(&self, ip: &IpAddr) -> Option<(f32, f32)> { 16 | match self.city_reader.lookup::(*ip) { 17 | Ok(res) => { 18 | let location = res.location?; 19 | match (location.latitude, location.longitude) { 20 | (Some(lat), Some(lon)) => Some((lat as f32, lon as f32)), 21 | _ => None, 22 | } 23 | } 24 | Err(err) => { 25 | log::error!("cannot get location of ip {} {}", ip, err); 26 | None 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /bin/standalone.sh: -------------------------------------------------------------------------------- 1 | RUST_LOG=atm0s_sdn_network=error,info \ 2 | RUST_BACKTRACE=1 \ 3 | cargo run -- \ 4 | --sdn-zone-node-id 1 \ 5 | --workers 1 \ 6 | standalone \ 7 | --geo-db "../maxminddb-data/GeoLite2-City.mmdb" \ 8 | --max-cpu 100 \ 9 | --max-memory 100 \ 10 | --max-disk 100 11 | -------------------------------------------------------------------------------- /bin/z0_connector_n4.sh: -------------------------------------------------------------------------------- 1 | RUST_LOG=info \ 2 | RUST_BACKTRACE=1 \ 3 | cargo run -- \ 4 | --sdn-zone-id 0 \ 5 | --sdn-zone-node-id 4 \ 6 | --seeds-from-url "http://localhost:3000/api/node/address" \ 7 | connector \ 8 | --s3-uri "http://minioadmin:minioadmin@127.0.0.1:9000/record" 9 | -------------------------------------------------------------------------------- /bin/z0_console_n0.sh: -------------------------------------------------------------------------------- 1 | RUST_LOG=info \ 2 | RUST_BACKTRACE=1 \ 3 | cargo run -- \ 4 | --http-port 8080 \ 5 | --sdn-port 10000 \ 6 | --sdn-zone-id 0 \ 7 | --sdn-zone-node-id 0 \ 8 | --enable-private-ip \ 9 | --workers 2 \ 10 | console 11 | -------------------------------------------------------------------------------- /bin/z0_gate_n1.sh: -------------------------------------------------------------------------------- 1 | RUST_LOG=info \ 2 | RUST_BACKTRACE=1 \ 3 | cargo run -- \ 4 | --http-port 3000 \ 5 | --enable-private-ip \ 6 | --sdn-port 10001 \ 7 | --sdn-zone-id 0 \ 8 | --sdn-zone-node-id 1 \ 9 | --seeds-from-url "http://localhost:8080/api/cluster/seeds?zone_id=0&node_type=Gateway" \ 10 | --workers 2 \ 11 | gateway \ 12 | --lat 10 \ 13 | --lon 20 \ 14 | --max-memory 100 \ 15 | --max-disk 100 \ 16 | --geo-db "../maxminddb-data/GeoLite2-City.mmdb" 17 | -------------------------------------------------------------------------------- /bin/z0_media_n2.sh: -------------------------------------------------------------------------------- 1 | RUST_LOG=info \ 2 | RUST_BACKTRACE=1 \ 3 | cargo run -- \ 4 | --enable-private-ip \ 5 | --sdn-zone-id 0 \ 6 | --sdn-zone-node-id 2 \ 7 | --seeds-from-url "http://localhost:3000/api/node/address" \ 8 | --workers 2 \ 9 | media 10 | -------------------------------------------------------------------------------- /bin/z0_media_n3.sh: -------------------------------------------------------------------------------- 1 | RUST_LOG=info \ 2 | RUST_BACKTRACE=1 \ 3 | cargo run -- \ 4 | --enable-private-ip \ 5 | --sdn-zone-id 0 \ 6 | --sdn-zone-node-id 3 \ 7 | --seeds-from-url "http://localhost:3000/api/node/address" \ 8 | --workers 2 \ 9 | media 10 | -------------------------------------------------------------------------------- /bin/z1_connector_n4.sh: -------------------------------------------------------------------------------- 1 | RUST_LOG=info \ 2 | RUST_BACKTRACE=1 \ 3 | cargo run -- \ 4 | --sdn-zone-id 1 \ 5 | --sdn-zone-node-id 4 \ 6 | --seeds 256@/ip4/127.0.0.1/udp/11000 \ 7 | connector \ 8 | --s3-uri "http://minioadmin:minioadmin@127.0.0.1:9000/record" 9 | -------------------------------------------------------------------------------- /bin/z1_gate_n1.sh: -------------------------------------------------------------------------------- 1 | RUST_LOG=info \ 2 | RUST_BACKTRACE=1 \ 3 | cargo run -- \ 4 | --http-port 4000 \ 5 | --enable-private-ip \ 6 | --sdn-zone-id 1 \ 7 | --sdn-zone-node-id 1 \ 8 | --sdn-port 11000 \ 9 | --seeds-from-url "http://localhost:8080/api/cluster/seeds?zone_id=1&node_type=Gateway" \ 10 | --workers 2 \ 11 | gateway \ 12 | --lat 20 \ 13 | --lon 30 \ 14 | --max-memory 100 \ 15 | --max-disk 100 \ 16 | --geo-db "../maxminddb-data/GeoLite2-City.mmdb" 17 | -------------------------------------------------------------------------------- /bin/z1_media_n2.sh: -------------------------------------------------------------------------------- 1 | RUST_LOG=info \ 2 | RUST_BACKTRACE=1 \ 3 | cargo run -- \ 4 | --enable-private-ip \ 5 | --sdn-zone-id 1 \ 6 | --sdn-zone-node-id 2 \ 7 | --seeds-from-url "http://localhost:4000/api/node/address" \ 8 | --workers 2 \ 9 | media \ 10 | --webrtc-port-seed 11200 \ 11 | --enable-token-api 12 | -------------------------------------------------------------------------------- /bin/z1_media_n3.sh: -------------------------------------------------------------------------------- 1 | RUST_LOG=info \ 2 | RUST_BACKTRACE=1 \ 3 | cargo run -- \ 4 | --enable-private-ip \ 5 | --sdn-zone-id 1 \ 6 | --sdn-zone-node-id 3 \ 7 | --seeds-from-url "http://localhost:4000/api/node/address" \ 8 | --workers 2 \ 9 | media \ 10 | --webrtc-port-seed 11300 \ 11 | --enable-token-api 12 | -------------------------------------------------------------------------------- /book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | title = "Atm0s Media Server development and user guide" 3 | src = "docs" 4 | 5 | [preprocessor.mermaid] 6 | command = "mdbook-mermaid" 7 | 8 | [output.html] 9 | additional-js = ["mermaid.min.js", "mermaid-init.js"] -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Atm0s Media Server 2 | 3 | A decentralized media server designed to handle media streaming on a global scale, making it suitable for large-scale applications but with minimal cost. 4 | 5 | It is developed by 8xFF, a group of independent developers who are passionate about building a new generation of media server and network infrastructure with decentralization in mind. While we have received support from various companies and individuals, we are not affiliated with any specific company. 8xFF is a community-driven project, and we welcome anyone interested in contributing to join us. 6 | 7 | For a deep dive into the technical aspects of network architecture, please refer to our [Smart-Routing](https://github.com/8xFF/atm0s-sdn/blob/master/docs/smart_routing.md) 8 | 9 | [](https://www.youtube.com/embed/QF8ZJq9xuSU) 11 | 12 | (Above is a demo video of the version used by Bluesea Network) 13 | -------------------------------------------------------------------------------- /docs/contributor-guide/README.md: -------------------------------------------------------------------------------- 1 | # Contributor guide 2 | 3 | This document is intended for developers who want to contribute to atm0s-media-server. It contains information about the project, the codebase, development environment setup, and more. 4 | 5 | ## Table of contents 6 | 7 | - [Getting started](./getting-started.md) 8 | - [Architecture](./architecture.md) 9 | - [Features](./features/) 10 | - [Transports](./transports/) 11 | - [Middlewares](./middlewares/) 12 | - [Servers](./servers/) 13 | - [RFCs](./rfcs/) 14 | -------------------------------------------------------------------------------- /docs/contributor-guide/features/README.md: -------------------------------------------------------------------------------- 1 | # Features 2 | 3 | In this document, we will explore the implementation of key features of atm0s-media-server. Currently, we have the following features: 4 | 5 | | Feature | Status | 6 | | ------------------------------------- | ------ | 7 | | [Audio-mixer](./audio-mixer.md) | Alpha | 8 | | [Authentication](./authentication.md) | Alpha | 9 | | [Simulcast/SVC](./simulcast-svc.md) | Alpha | 10 | | [Recording](./recording.md) | TODO | 11 | | [Cluster](./cluster.md) | Alpha | 12 | -------------------------------------------------------------------------------- /docs/contributor-guide/features/authentication.md: -------------------------------------------------------------------------------- 1 | # Authentication 2 | 3 | We can extend with custom authentication by implementing the `SessionTokenSigner` trait. 4 | 5 | ```rust 6 | pub trait SessionTokenSigner { 7 | fn sign_media_session(&self, token: &MediaSessionToken) -> String; 8 | fn sign_conn_id(&self, conn_id: &MediaConnId) -> String; 9 | } 10 | 11 | pub trait SessionTokenVerifier { 12 | fn verify_media_session(&self, token: &str) -> Option; 13 | fn verify_conn_id(&self, token: &str) -> Option; 14 | } 15 | ``` 16 | 17 | We have a simple static secret signer and verifier in the [`cluster` crate](https://github.com/8xFF/atm0s-media-server/blob/master/packages/cluster/src/implement/secure/jwt_static.rs). 18 | -------------------------------------------------------------------------------- /docs/contributor-guide/features/cluster.md: -------------------------------------------------------------------------------- 1 | # Cluster 2 | 3 | Cluster feature is implemented following [RFC-0003-media-global-cluster](https://github.com/8xFF/rfcs/pull/3). 4 | More info can be found in user guide [here](../../user-guide/features/cluster.md). 5 | 6 | ## Implementation details 7 | 8 | The cluster module is implemented in the `cluster` package. It is responsible for managing the cluster of media servers. 9 | 10 | Each time new peer joined to media-server, cluster will create a new `ClusterEndpoint` to attach to the peer. 11 | 12 | The `ClusterEndpoint` is responsible for managing pubsub channels, and also room information for the peer. 13 | We use event based communication to interact with `ClusterEndpoint`: 14 | 15 | ```rust 16 | #[async_trait::async_trait] 17 | pub trait ClusterEndpoint: Send + Sync { 18 | fn on_event(&mut self, event: ClusterEndpointOutgoingEvent) -> Result<(), ClusterEndpointError>; 19 | async fn recv(&mut self) -> Result; 20 | } 21 | ``` 22 | -------------------------------------------------------------------------------- /docs/contributor-guide/features/recording.md: -------------------------------------------------------------------------------- 1 | # Recording 2 | 3 | TODO: write about recording queue and compose utils 4 | -------------------------------------------------------------------------------- /docs/contributor-guide/middlewares/README.md: -------------------------------------------------------------------------------- 1 | # Middlewares 2 | 3 | Middlewares are a way to extend the functionality of the media server. We already have some built-in middlewares, and you can also write your own middlewares. 4 | 5 | - Logging 6 | - Mix-minus 7 | - Whep auto attach 8 | -------------------------------------------------------------------------------- /docs/contributor-guide/middlewares/logging.md: -------------------------------------------------------------------------------- 1 | # Logging 2 | 3 | [Source code](https://github.com/8xFF/atm0s-media-server/blob/master/packages/endpoint/src/endpoint/middleware/logger.rs) 4 | 5 | This middleware will hook into endpoint's state change event and send it to the connector service. 6 | All log data types are encoded and decoded by protocol buffer (packages/protocol/src/media_endpoint_log.proto). 7 | 8 | Each log data will have some kind of data: 9 | 10 | - client info (ip, user-agent, ...) 11 | - event type (Routing, Connecting, Connected, Disconnected, Error ...) 12 | - metadata 13 | -------------------------------------------------------------------------------- /docs/contributor-guide/middlewares/mix-minus.md: -------------------------------------------------------------------------------- 1 | # Mix-minus 2 | 3 | [Source code](https://github.com/8xFF/atm0s-media-server/blob/master/packages/endpoint/src/endpoint/middleware/mix_minus.rs) 4 | 5 | The Mix-minus middleware will create some virtual output tracks and hook events when the endpoint's call switches the receiver to the output tracks. 6 | 7 | After that, the middleware will select the track with the highest volume and bind it to the output tracks. The middleware also hooks into the media packet event and outputs it to the output tracks, which will then be sent to the client. 8 | 9 | We have 2 modes: mix all tracks in a room and manual mix tracks. 10 | 11 | ## Mix all tracks in a room mode 12 | 13 | In this mode, the middleware will hook into the cluster track added or removed events and automatically subscribe or unsubscribe to the track. All audio data will be sent to the audio mixer, which will select the track with the highest volume and bind it to the output tracks. 14 | 15 | ## Manual mix tracks mode 16 | 17 | In this mode, the client will use the SDK to add or remove tracks to the audio mixer. The audio mixer will subscribe or unsubscribe to the track, and then select the track with the highest volume and bind it to the output tracks. 18 | 19 | ## Feature improvement 20 | 21 | Instead of subscribing to all audio tracks, we can have multiple layers of tracks (metadata, data, etc.). Initially, we only need to subscribe to the first metadata layer (which contains audio level information), and then when we need to mix, we can subscribe to more layers to get the actual audio data. 22 | -------------------------------------------------------------------------------- /docs/contributor-guide/middlewares/whep.md: -------------------------------------------------------------------------------- 1 | # Whep middleware 2 | 3 | Whep is implemented by reusing WebRTC transport, but it need to automatic binding between remote stream and receivers. 4 | 5 | [Source code](https://github.com/8xFF/atm0s-media-server/blob/master/servers/media-server/src/server/webrtc/middleware/whep_auto_attach.rs) 6 | 7 | We create a weep_auto_attach middleware which hooks into the cluster track added event, then automatically attaches to the track. When the attached track is removed, we will select from the remaining tracks if possible. 8 | -------------------------------------------------------------------------------- /docs/contributor-guide/resource_clear.md: -------------------------------------------------------------------------------- 1 | # Resource Clear 2 | 3 | This server is built on top of [sans-io-runtime](https://github.com/atm0s-org/sans-io-runtime), which is a runtime for building server applications. With this reason we don't use mechanism of `Drop` to release resource. Instead, we use a queue to manage the resource release. 4 | 5 | We have 2 types of tasks: 6 | 7 | - Self-managed tasks: for example, the endpoint task 8 | - Dependent tasks: for example, the cluster room task 9 | 10 | For self-managed tasks, the task itself determines when to kill itself. For dependent tasks, the task will be killed when all of its dependent tasks are un-linked. Each task type has a different way to handle task termination. 11 | 12 | ## Self-managed task 13 | 14 | Example: The endpoint task can automatically release resources when the client disconnects. 15 | 16 | ## Dependent task 17 | 18 | Example: The cluster room task needs to wait for all dependent tasks (endpoint track, mixer, or data channel) to be un-linked before destroying itself. 19 | -------------------------------------------------------------------------------- /docs/contributor-guide/servers/README.md: -------------------------------------------------------------------------------- 1 | # Servers 2 | 3 | The following sections describe the different servers that compose the atm0s-media-server. 4 | 5 | - [Gateway](gateway.md) 6 | - [Connector](connector.md) 7 | - [Media Server](media-server.md) 8 | -------------------------------------------------------------------------------- /docs/contributor-guide/servers/connector.md: -------------------------------------------------------------------------------- 1 | # Connector 2 | 3 | The connector server is a special server used for receiving event logs from all other media servers. We can have multiple connector servers, but we only need one connector server to make the system work. 4 | 5 | If we have multiple connector servers, the routing algorithm will send each event log to the best (closest) connector server. 6 | 7 | We can have multiple connector servers in each zone, and then the routing algorithm will send the event log to the connector server in the same zone. 8 | 9 | Each connector server is connected to a message queue, and external services can get event logs from the message queue. Currently, we only support NATS message queue and HTTP POST API endpoints. 10 | -------------------------------------------------------------------------------- /docs/contributor-guide/servers/gateway.md: -------------------------------------------------------------------------------- 1 | # Gateway 2 | 3 | The Gateway Server holds a list of media servers and is used to route requests to the correct media server. 4 | 5 | The routing logic is described below: 6 | 7 | - If the request is in the closest zone, then route it to the best media server in the current zone. 8 | - If the request is in a different zone, then route it to the other gateway that is closest to the user's zone. 9 | 10 | To implement the above approach, each media server will broadcast its information to all gateways in the same zone. This is done using the atm0s-sdn PubSub feature. 11 | Additionally, each gateway will also broadcast its information to all other gateways. This is also done using the atm0s-sdn PubSub feature. 12 | -------------------------------------------------------------------------------- /docs/contributor-guide/servers/media-server.md: -------------------------------------------------------------------------------- 1 | # Media Server 2 | 3 | The media server is implemented by waiting for client requests and then forking a task to run each request. Each task runs in a separate process. 4 | 5 | Because each transport has a different way of running tasks, we need to implement a different task runner for each transport, which is done in the servers/media-server/ directory. 6 | 7 | In the future, we are considering splitting the media-server into a library and a binary for easier use in other projects and for extending the logic. 8 | -------------------------------------------------------------------------------- /docs/contributor-guide/transports/README.md: -------------------------------------------------------------------------------- 1 | # Transports 2 | 3 | We have some types of transports: 4 | 5 | - [WebRTC](./webrtc.md) 6 | - [RTP-Engine](./rtp-engine.md) 7 | - [Whip-Whep](./whip-whep.md) 8 | 9 | Bellow transport will be implemented in next version: 10 | 11 | - [RTMP](./rtmp.md) 12 | - [Media over Quic](https://quic.video/) 13 | - [SRT](https://www.haivision.com/products/srt-secure-reliable-transport/) 14 | - [HLS](https://en.wikipedia.org/wiki/HTTP_Live_Streaming) 15 | 16 | If you don't find the transport you need, you can implement it by yourself by 17 | implementing the `Transport` traits. Please refer to 18 | [Architecture](../architecture.md) for more info. 19 | -------------------------------------------------------------------------------- /docs/contributor-guide/transports/rtp-engine.md: -------------------------------------------------------------------------------- 1 | # RTP Engine 2 | 3 | RTP Engine is implemented in 'packages/transport_rtpengine/' 4 | -------------------------------------------------------------------------------- /docs/contributor-guide/transports/webrtc.md: -------------------------------------------------------------------------------- 1 | # WebRTC 2 | 3 | WebRTC is using a SAN I/O library called Str0m. 4 | 5 | We have an internal part which takes care of the protocol and transport logic without coupling with I/O. To integrate with I/O and other parts, we have a wrapper called WebrtcTransport, which will process I/O and convert Str0m events to internal events and vice versa. 6 | 7 | Currently, we support UDP and SSLTCP. 8 | 9 | TODO: STUN client, TURN server 10 | -------------------------------------------------------------------------------- /docs/contributor-guide/transports/whip-whep.md: -------------------------------------------------------------------------------- 1 | # Whip-Whep 2 | 3 | Whip/Whep is a subset of the WebRTC protocol. Unlike the WebRTC SDK, Whip and Whep do not require manual handling of remote streams as it is automatically handled by the media server. 4 | 5 | ## Whip 6 | 7 | After whip client connect to media-server, server will create pre-defined tracks for the client. 8 | 9 | - Audio track name: audio_main 10 | - Video track name: video_main 11 | 12 | ## Whep 13 | 14 | To enable automatic binding between remote streams and receivers, we create a Whep middleware. 15 | -------------------------------------------------------------------------------- /docs/getting-started/README.md: -------------------------------------------------------------------------------- 1 | # Getting started 2 | 3 | - [Installation](./installation/) 4 | - [Quick Start](./quick-start/) 5 | - [FAQ](faq.md) 6 | - [Troubleshooting](troubleshooting.md) 7 | -------------------------------------------------------------------------------- /docs/getting-started/faq.md: -------------------------------------------------------------------------------- 1 | ## Frequently Asked Questions 2 | 3 | ### Who created this project? 4 | 5 | This project was created by 8xFF, a group of independent developers who are passionate about building a new generation of media server and network infrastructure with decentralization in mind. While we have received support from various companies and individuals, we are not affiliated with any specific company. 8xFF is a community-driven project, and we welcome anyone interested in contributing to join us. 6 | 7 | ### What license is this project under? 8 | 9 | This project is licensed under the MIT License. Please refer to the [LICENSE](/LICENSE) file for more details. 10 | 11 | ### What are the goals for this project? 12 | 13 | Our goals for this project can be summarized as follows: 14 | 15 | - **Goal 1**: Cluster: Create a global decentralized media server cluster with multiple zones. 16 | - **Goal 2**: Market: Develop a sharing marketplace for the media server, enabling resource sharing and monetization. This will help scale the media server cluster during peak times and reduce costs during off-peak times. In the feature marketplace fees will be used to fund the development of the project. 17 | - **Goal 3**: Serverless: Establish a network between users, where servers only act as fallbacks. This approach aims to significantly reduce infrastructure costs and scale to infinity. 18 | -------------------------------------------------------------------------------- /docs/getting-started/installation/auto-generate-node-id.md: -------------------------------------------------------------------------------- 1 | # Auto generate node_id 2 | 3 | Most of the time, we rarely deploy new zones, so the zone-id can be manually specified. However, for nodes inside a zone, we usually use cloud or docker for easy management. This leads to the problem of having to manually specify node_ids inside a zone, which is inconvenient and error-prone. 4 | 5 | So we have implemented several mechanisms to auto-generate node_ids from machine information. 6 | 7 | ## Auto generate node_id from local_ip 8 | 9 | The idea is simple: most cloud providers or bare-metal servers will assign a unique private IP to each machine, typically in the form of 10.10.10.x, 192.168.1.x, etc. 10 | 11 | We can use the last octet of the IP as the node_id. 12 | 13 | If the subnet is larger than /24, we still use the last 8 bits of the IP as the node_id, though this carries some risk of collision. In such cases, we recommend switching to a /24 subnet or using the NodeId pool. 14 | 15 | Example: 16 | ``` 17 | cargo run -- --sdn-zone-id 0 --sdn-zone-node-id-from-ip-prefix "10.10.10" console 18 | ``` 19 | 20 | ## NodeId pool 21 | 22 | Status: in progress -------------------------------------------------------------------------------- /docs/getting-started/installation/console_screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/8xFF/atm0s-media-server/9ea8c02d3bc9b946edea1d566020da94390c98ca/docs/getting-started/installation/console_screen.png -------------------------------------------------------------------------------- /docs/getting-started/installation/console_screen2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/8xFF/atm0s-media-server/9ea8c02d3bc9b946edea1d566020da94390c98ca/docs/getting-started/installation/console_screen2.png -------------------------------------------------------------------------------- /docs/getting-started/installation/docker-compose.md: -------------------------------------------------------------------------------- 1 | # Docker-compose (outdate, update needed) 2 | 3 | We can use [docker-compose](https://github.com/8xFF/atm0s-docker-compose) to deploy atm0s-media-server. 4 | -------------------------------------------------------------------------------- /docs/getting-started/installation/kubernetes.md: -------------------------------------------------------------------------------- 1 | # Kubernetes (outdate, update needed) 2 | 3 | You can install into kubernetes cluster by Helm chart 4 | 5 | ```bash 6 | helm repo add 8xff https://8xff.github.io/helm 7 | helm repo update 8 | helm install atm0s-media-stack 8xff/atm0s-media-stack --set gateway.host={host}.{example.com} --namespace atm0s-media --create-namespace 9 | ``` 10 | 11 | #TODO need to config for both single zone or multi-zones 12 | -------------------------------------------------------------------------------- /docs/getting-started/installation/multi-zones.md: -------------------------------------------------------------------------------- 1 | # Multi zones 2 | 3 | You can deploy a multi-zone cluster to scale up your cluster. Each zone is a single-zone cluster, and you can deploy many zones across the regions. 4 | 5 | In a multi-zone setup, the zones are interconnected. To achieve this, all gateway nodes are interconnected and each request will be routed to the closest zone's gateway node. 6 | 7 | ![Multi zones](../../imgs/multi-zones-abstract.excalidraw.png) 8 | 9 | The gateway nodes also take part in routing media data between zones in the fastest path possible; data will be relayed if the direct connection is bad. 10 | 11 | Note that you can deploy multi connectors in some zones to handle room and peer events. However, you need to handle these events yourself to ensure data consistency. 12 | 13 | ## Prerequisites 14 | 15 | - Choose a different zone id for each zone, it is 24bit unsigned number. 16 | - Select a secret for all zones. 17 | 18 | ## Deploying each zone, same as a single-zone cluster 19 | 20 | The deployment steps are the same as for a single-zone cluster with addition `--sdn-zone-id ZONE_ID` param. However, starting from second zone, you don't need to add console node, instead of that you can reuse single console node for all zones. 21 | -------------------------------------------------------------------------------- /docs/getting-started/installation/nat-traversal.md: -------------------------------------------------------------------------------- 1 | # NAT Traversal 2 | 3 | Some cloud providers (like AWS) route all traffic through a NAT gateway, which means that we don't have a public IP address on the VM. 4 | 5 | To work around this, we can use the `node_ip_alt` and `node_ip_alt_cloud` options to specify alternative IP addresses for the node. 6 | 7 | ## Using node_ip_alt 8 | 9 | The `node_ip_alt` option takes a list of IP addresses as input, and the node will bind to each of them. 10 | 11 | ## Using node_ip_alt_cloud 12 | 13 | The `node_ip_alt_cloud` option takes a cloud provider as input, and will automatically fetch the alternative IP address for the node. 14 | 15 | | Cloud Provider | Fetch URL | 16 | | -------------- | ----------------------------------------------------------------------------------------------- | 17 | | AWS | `http://169.254.169.254/latest/meta-data/local-ipv4` | 18 | | GCP | `http://metadata/computeMetadata/v1/instance/network-interfaces/0/access-configs/0/external-ip` | 19 | | Azure | `http://ipv4.icanhazip.com` | 20 | | Other | `http://ipv4.icanhazip.com` | 21 | -------------------------------------------------------------------------------- /docs/getting-started/quick-start/README.md: -------------------------------------------------------------------------------- 1 | # Quick Start 2 | 3 | This will help you to get started with the project. 4 | 5 | After installation, you can start testing the server with some ways: 6 | 7 | - Using embedded [Whip and Whep](./whip-whep.md) client 8 | - Using [RTMP broadcaster](./rtmp.md) like OBS 9 | - Using [WebRTC SDK](./webrtc-sdk.md) 10 | - Deploy [sample applications](./sample-application.md) 11 | -------------------------------------------------------------------------------- /docs/getting-started/quick-start/rtmp.md: -------------------------------------------------------------------------------- 1 | # RTMP 2 | -------------------------------------------------------------------------------- /docs/getting-started/quick-start/sample-application.md: -------------------------------------------------------------------------------- 1 | # Sample application 2 | 3 | If you just want to test atm0s-media-server, you can just use our sample application. 4 | 5 | | Sample | Description | Status | Repo | Maintainer | 6 | | ---------------- | ----------------------------------------------- | ------ | ----------------------------------------------------------------------- | ------------- | 7 | | Meet | Google Meet clone | Alpha | [atm0s-meet-sample](https://github.com/luongngocminh/atm0s-meet-sample) | luongngocminh | 8 | | Broadcaster | The broadcaster application | WIP | | | 9 | | SkypeOffice fork | The fork of SkyOffice, a Gather clone | WIP | | | 10 | | OTT App | The OTT application with contact and video call | WIP | | | 11 | -------------------------------------------------------------------------------- /docs/getting-started/quick-start/webrtc-sdk.md: -------------------------------------------------------------------------------- 1 | # WebRTC 2 | 3 | WebRTC is the most flexible way to interact with the cluster. In this method, we will interact with senders, consumers, and we can create applications with multiple streams. This method is suitable for most use cases. The details can be refer to [8xFF-RFC-0005](https://github.com/8xFF/rfcs/pull/5) 4 | 5 | | SDK | Status | Link | 6 | | ------------ | ------ | ---------------------------------------------------------------------- | 7 | | JavaScript | Alpha | [atm0s-media-sdk-js](https://github.com/8xFF/atm0s-media-sdk-js) | 8 | | React | Alpha | [atm0s-media-sdk-react](https://github.com/8xFF/atm0s-media-sdk-react) | 9 | | React Native | WIP | | 10 | | Flutter | TODO | | 11 | | Android | TODO | | 12 | | iOS | TODO | | 13 | 14 | ### Pregenerated token for default secret and room `demo`, any peer: 15 | 16 | ```jwt 17 | eyJhbGciOiJIUzI1NiJ9.eyJyb29tIjoiZGVtbyIsInBlZXIiOm51bGwsInByb3RvY29sIjoiV2VicnRjIiwicHVibGlzaCI6dHJ1ZSwic3Vic2NyaWJlIjp0cnVlLCJ0cyI6MTcwMzc1MjM1NTI2NH0.llwwbSwVTsyFgL_jYCdoPNVdOiC2jbtNb4uxxE-PU7A 18 | ``` 19 | -------------------------------------------------------------------------------- /docs/getting-started/troubleshooting.md: -------------------------------------------------------------------------------- 1 | # Troubleshooting 2 | 3 | If you find any issues, please open an issue on our [GitHub repository](https://github.com/8xFF/atm0s-media-server/issues) 4 | -------------------------------------------------------------------------------- /docs/imgs/architecture/endpoint.excalidraw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/8xFF/atm0s-media-server/9ea8c02d3bc9b946edea1d566020da94390c98ca/docs/imgs/architecture/endpoint.excalidraw.png -------------------------------------------------------------------------------- /docs/imgs/architecture/how-it-works.excalidraw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/8xFF/atm0s-media-server/9ea8c02d3bc9b946edea1d566020da94390c98ca/docs/imgs/architecture/how-it-works.excalidraw.png -------------------------------------------------------------------------------- /docs/imgs/architecture/implement-layers.excalidraw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/8xFF/atm0s-media-server/9ea8c02d3bc9b946edea1d566020da94390c98ca/docs/imgs/architecture/implement-layers.excalidraw.png -------------------------------------------------------------------------------- /docs/imgs/architecture/tasks.excalidraw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/8xFF/atm0s-media-server/9ea8c02d3bc9b946edea1d566020da94390c98ca/docs/imgs/architecture/tasks.excalidraw.png -------------------------------------------------------------------------------- /docs/imgs/architecture/why-it-fast.excalidraw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/8xFF/atm0s-media-server/9ea8c02d3bc9b946edea1d566020da94390c98ca/docs/imgs/architecture/why-it-fast.excalidraw.png -------------------------------------------------------------------------------- /docs/imgs/demo-monitor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/8xFF/atm0s-media-server/9ea8c02d3bc9b946edea1d566020da94390c98ca/docs/imgs/demo-monitor.png -------------------------------------------------------------------------------- /docs/imgs/demo-rtmp-config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/8xFF/atm0s-media-server/9ea8c02d3bc9b946edea1d566020da94390c98ca/docs/imgs/demo-rtmp-config.png -------------------------------------------------------------------------------- /docs/imgs/demo-screen.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/8xFF/atm0s-media-server/9ea8c02d3bc9b946edea1d566020da94390c98ca/docs/imgs/demo-screen.jpg -------------------------------------------------------------------------------- /docs/imgs/features/audio-mixer.excalidraw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/8xFF/atm0s-media-server/9ea8c02d3bc9b946edea1d566020da94390c98ca/docs/imgs/features/audio-mixer.excalidraw.png -------------------------------------------------------------------------------- /docs/imgs/features/sip.excalidraw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/8xFF/atm0s-media-server/9ea8c02d3bc9b946edea1d566020da94390c98ca/docs/imgs/features/sip.excalidraw.png -------------------------------------------------------------------------------- /docs/imgs/multi-zones-abstract.excalidraw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/8xFF/atm0s-media-server/9ea8c02d3bc9b946edea1d566020da94390c98ca/docs/imgs/multi-zones-abstract.excalidraw.png -------------------------------------------------------------------------------- /docs/imgs/multi-zones.excalidraw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/8xFF/atm0s-media-server/9ea8c02d3bc9b946edea1d566020da94390c98ca/docs/imgs/multi-zones.excalidraw.png -------------------------------------------------------------------------------- /docs/imgs/single-zone.excalidraw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/8xFF/atm0s-media-server/9ea8c02d3bc9b946edea1d566020da94390c98ca/docs/imgs/single-zone.excalidraw.png -------------------------------------------------------------------------------- /docs/imgs/usecases/broadcast.excalidraw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/8xFF/atm0s-media-server/9ea8c02d3bc9b946edea1d566020da94390c98ca/docs/imgs/usecases/broadcast.excalidraw.png -------------------------------------------------------------------------------- /docs/imgs/usecases/cctv-extended.excalidraw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/8xFF/atm0s-media-server/9ea8c02d3bc9b946edea1d566020da94390c98ca/docs/imgs/usecases/cctv-extended.excalidraw.png -------------------------------------------------------------------------------- /docs/imgs/usecases/video-conference.excalidraw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/8xFF/atm0s-media-server/9ea8c02d3bc9b946edea1d566020da94390c98ca/docs/imgs/usecases/video-conference.excalidraw.png -------------------------------------------------------------------------------- /docs/issue-template.md: -------------------------------------------------------------------------------- 1 | # Issue Template 2 | 3 | ## Description 4 | 5 | Please provide a clear and concise description of the issue. 6 | 7 | ## Steps to Reproduce 8 | 9 | 1. Step 1 10 | 2. Step 2 11 | 3. Step 3 12 | 13 | ## Expected Behavior 14 | 15 | Please describe what you expected to happen. 16 | 17 | ## Actual Behavior 18 | 19 | Please describe what actually happened. 20 | 21 | ## Screenshots 22 | 23 | If applicable, add screenshots to help explain the issue. 24 | 25 | ## Environment 26 | 27 | - Operating System: 28 | - Browser (if applicable): 29 | - Version (if applicable): 30 | 31 | ## Additional Information 32 | 33 | Add any other relevant information about the issue here. 34 | -------------------------------------------------------------------------------- /docs/pull-request-template.md: -------------------------------------------------------------------------------- 1 | ## Pull Request 2 | 3 | ### Description 4 | 5 | Please provide a brief description of the changes made in this pull request. 6 | 7 | ### Related Issue 8 | 9 | If this pull request is related to any issue, please mention it here. 10 | 11 | ### Checklist 12 | 13 | - [ ] I have tested the changes locally. 14 | - [ ] I have reviewed the code changes. 15 | - [ ] I have updated the documentation, if necessary. 16 | - [ ] I have added appropriate tests, if applicable. 17 | 18 | ### Screenshots 19 | 20 | If applicable, add screenshots to help explain the changes made. 21 | 22 | ### Additional Notes 23 | 24 | Add any additional notes or context about the pull request here. 25 | -------------------------------------------------------------------------------- /docs/user-guide/README.md: -------------------------------------------------------------------------------- 1 | # User guide 2 | 3 | This document is intended for users who want to use atm0s-media-server. It contains information about concepts, SDKs, configuration, features, integration, usage examples, and upgrade. 4 | 5 | ## Table of contents 6 | 7 | - [Concepts](concepts.md) 8 | - [SDKs](sdks.md) 9 | - [Configuration](configuration.md) 10 | - [Features](features/README.md) 11 | - [Integration](integration.md) 12 | - [Usage examples](usage-examples.md) 13 | - [Upgrade](upgrade.md) 14 | -------------------------------------------------------------------------------- /docs/user-guide/cluster-discovery.md: -------------------------------------------------------------------------------- 1 | # Cluster discovery 2 | 3 | We device cluster into some parts: seeds and zones 4 | 5 | ## Seeds 6 | 7 | Console nodes are act as some seeds node to other cluster can connect to and 8 | discover each other. 9 | 10 | Each some seconds, the console nodes will advertise a message with it address to 11 | all other console nodes. With this information, other console nodes can connect 12 | to each other. 13 | 14 | ## Zones 15 | 16 | Each zone is a single-zone cluster, and we can deploy many zones across the 17 | regions. Each zone must have at least one gateway node to connect to seeds and 18 | to other zones's gateway nodes. 19 | 20 | Other zone's node connect to all this zone's gateway nodes. 21 | 22 | Each some seconds, the console nodes also advertise a message to all gateway's 23 | nodes, which allow all gateway nodes to connect to all console nodes. Sam with 24 | console nodes, each gateway node will advertise it address to all other gateway 25 | in all all other zones with that, we can create a full connected network between 26 | gateway nodes, allow discovery best path between all zones. 27 | 28 | Gateway nodes also advertise it address to all same zone's media, connector 29 | nodes. 30 | 31 | # Summary 32 | 33 | Depend on node type, it will advertise address to estiblish entire network. 34 | -------------------------------------------------------------------------------- /docs/user-guide/features/README.md: -------------------------------------------------------------------------------- 1 | # Features 2 | 3 | In this document, we will explore the key features of atm0s-media-server. Currently, we have the following features: 4 | 5 | | Feature | Status | 6 | | ----------------------------------------------------------- | ------ | 7 | | [Audio-mixer](./audio-mixer.md) | Alpha | 8 | | [Auth/Multi tenancy](./authentication-and-multi-tenancy.md) | Alpha | 9 | | [Simulcast/SVC](./simulcast-svc.md) | Alpha | 10 | | [Recording](./recording.md) | TODO | 11 | | [Cluster](./cluster.md) | Alpha | 12 | | [extra_data-metadata](./extra_data-metadata.md) | Alpha | 13 | | [Third party event hook](./third-party-system-hook.md) | Alpha | 14 | -------------------------------------------------------------------------------- /docs/user-guide/features/audio-mixer.md: -------------------------------------------------------------------------------- 1 | # Audio Mixer 2 | 3 | ![Audio Mixer](../../imgs/features/audio-mixer.excalidraw.png) 4 | 5 | For maximum performance and flexibility, the audio mixer is implemented as a virtual mix-minus way. Instead of decoding then mixing with raw audio data, we chose another approach to avoid decoding and encoding, which is called virtual mix-minus. 6 | 7 | In this way, we will prepare a number of output tracks (typically choose 3), then select the track with the highest volume and bind it to the output tracks. This way is also very flexible, for example in spatial space application, when the user can select the closest speaker to the interested list and the media-server will select who is selected to output. 8 | 9 | We have 2 levels of audio mixer: 10 | 11 | - Mix all tracks in a room, which is used for normal audio conference or video conference 12 | - Manual mix tracks, which is used for spatial audio (by using the add_source and remove_source APIs in the SDK) 13 | -------------------------------------------------------------------------------- /docs/user-guide/features/authentication-and-multi-tenancy.md: -------------------------------------------------------------------------------- 1 | # Authentication and Multi Tenancy 2 | 3 | We support authentication using JWT tokens. Additionally, we provide support for multi-tenancy using JWT tokens. The `sub` claim in the JWT token is used to identify both the user and the tenant. 4 | 5 | When creating a session token, we include the `sub` claim in the JWT token. This claim is used to identify the tenant. 6 | 7 | For more information on extending authentication, please refer to the Contributor Guide documentation. 8 | -------------------------------------------------------------------------------- /docs/user-guide/features/extra_data-metadata.md: -------------------------------------------------------------------------------- 1 | # extra_data and metadata 2 | 3 | In atm0s-media, extra_data and metadata is used with same goal is providing addition data beside of only room and peer. We use 2 terms extra_data and metadata with some difference: 4 | 5 | - extra_data: a string which is embedded to peer token, which can not be changed by client 6 | - metadata: a string which is embedded to peer or track by client. In there peer metadata is set at join-room step, track metadata is set at publish track step. 7 | -------------------------------------------------------------------------------- /docs/user-guide/features/simulcast-svc.md: -------------------------------------------------------------------------------- 1 | # Simulcast/SVC 2 | 3 | Simulcast is a technique by which a WebRTC client encodes the same video stream twice in different resolutions and bitrates and send these to a router which then decides who receives which of the streams. Simulcast adds more flexibility to how media servers can operate at scale. 4 | 5 | For more information, you can refer to [Simulcast Streaming](https://bloggeek.me/webrtcglossary/simulcast/) 6 | 7 | `Warning: Only some browsers support simulcasts like: Google Chrome PC, Firefox PC, Safari PC` 8 | -------------------------------------------------------------------------------- /docs/user-guide/features/third-party-system-hook.md: -------------------------------------------------------------------------------- 1 | # Third party system hook 2 | 3 | A third-party system hook is a provider that sends internal events from the media server to other systems. The events sent through the hook contain session, peer, and track. 4 | 5 | ## Usage 6 | 7 | The `connector` node sends the hook. So, to enable the hook to provide, you need to use `--hook-uri` to pass the provider's URI when starting the node. 8 | 9 | ```bash 10 | RUST_LOG=info \ 11 | RUST_BACKTRACE=1 \ 12 | cargo run -- \ 13 | --sdn-zone-id 0 \ 14 | --sdn-zone-node-id 4 \ 15 | --seeds 1@/ip4/127.0.0.1/udp/10001 \ 16 | connector \ 17 | --hook-uri "http://localhost:30798/webhook" 18 | ``` 19 | 20 | ## Message format 21 | 22 | Message will sent to another system by using JSON (serde and serde_json) or Binary format which is generated from Protobuf, defined by HookEvent message: 23 | 24 | ```protobuf 25 | message HookEvent { 26 | uint32 node = 1; 27 | uint64 ts = 2; 28 | oneof event { 29 | RoomEvent room = 3; 30 | PeerEvent peer = 4; 31 | RecordEvent record = 5; 32 | } 33 | } 34 | ``` 35 | 36 | Example with Json: 37 | 38 | ```json 39 | { 40 | "node":1, 41 | "ts":1724605969302, 42 | "event":{ 43 | "Peer":{ 44 | "session_id":3005239549225289700, 45 | "event":{ 46 | "RouteBegin":{ 47 | "remote_ip":"127.0.0.1" 48 | } 49 | } 50 | } 51 | } 52 | ``` 53 | 54 | ## Supported Provider 55 | 56 | | provider | status | description | 57 | | -------- | -------------------- | ------------------------------------------------------- | 58 | | webhook | :white_check_mark: | Will send each event using Restful API with POST method | 59 | -------------------------------------------------------------------------------- /docs/user-guide/sdks.md: -------------------------------------------------------------------------------- 1 | # SDKs 2 | 3 | SDKs status: 4 | 5 | | SDK | Status | Repo | 6 | | ----------- | ------- | ---------------------------------------------------------------------------------------------- | 7 | | JavaScript | Done | [https://github.com/8xFF/atm0s-media-sdk-js](https://github.com/8xFF/atm0s-media-sdk-js) | 8 | | React | Done | [https://github.com/8xFF/atm0s-media-sdk-react](https://github.com/8xFF/atm0s-media-sdk-react) | 9 | | ReactNative | Working | | 10 | | Flutter | TODO | | 11 | | iOS | TODO | | 12 | | Android | TODO | | 13 | -------------------------------------------------------------------------------- /docs/user-guide/upgrade.md: -------------------------------------------------------------------------------- 1 | # Upgrade 2 | 3 | This document will guide you how to upgrade your cluster to new version. 4 | 5 | ## Upgrade media node 6 | 7 | TODO 8 | 9 | ## Upgrade gateway node 10 | 11 | TODO 12 | 13 | ## Upgrade connector node 14 | 15 | TODO 16 | -------------------------------------------------------------------------------- /download-geodata.sh: -------------------------------------------------------------------------------- 1 | mkdir -p maxminddb-data 2 | cd maxminddb-data 3 | rm -i GeoLite2-City.mmdb 4 | wget https://github.com/P3TERX/GeoLite.mmdb/raw/download/GeoLite2-City.mmdb || { echo "Download GeoLite2-City database failed"; exit 1; } -------------------------------------------------------------------------------- /mermaid-init.js: -------------------------------------------------------------------------------- 1 | mermaid.initialize({startOnLoad:true}); 2 | -------------------------------------------------------------------------------- /packages/audio_mixer/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "atm0s-media-server-audio-mixer" 3 | version = "0.2.0-alpha.1" 4 | authors = ["Giang Minh "] 5 | edition = "2021" 6 | license = "MIT" 7 | description = "Audio Mixer Component for Atm0s Media Server" 8 | 9 | [dependencies] 10 | log.workspace = true 11 | indexmap.workspace = true 12 | -------------------------------------------------------------------------------- /packages/media_codecs/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [0.1.0-alpha.2](https://github.com/8xFF/atm0s-media-server/compare/atm0s-media-server-codecs-v0.1.0-alpha.1...atm0s-media-server-codecs-v0.1.0-alpha.2) - 2025-02-26 11 | 12 | ### Added 13 | 14 | - simple nodes visualization in console (#509) 15 | -------------------------------------------------------------------------------- /packages/media_codecs/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "atm0s-media-server-codecs" 3 | version = "0.1.0-alpha.2" 4 | authors = ["Giang Minh "] 5 | edition = "2021" 6 | license = "MIT" 7 | description = "Media Codecs Component for Atm0s Media Server" 8 | 9 | [dependencies] 10 | libsoxr = { workspace = true, optional = true } 11 | opusic-sys = { workspace = true, optional = true } 12 | 13 | [features] 14 | default = ["opus", "pcma", "resample"] 15 | resample = ["libsoxr"] 16 | opus = ["opusic-sys"] 17 | pcma = ["resample"] 18 | -------------------------------------------------------------------------------- /packages/media_codecs/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! This module implement decode and encode logic for some codecs 3 | //! Currently all of codec will assume output raw audio in 48k audio 4 | //! 5 | 6 | #[cfg(feature = "opus")] 7 | pub mod opus; 8 | #[cfg(feature = "pcma")] 9 | pub mod pcma; 10 | #[cfg(feature = "resample")] 11 | pub mod resample; 12 | 13 | pub trait AudioDecoder { 14 | fn decode(&mut self, in_buf: &[u8], out_buf: &mut [i16]) -> Option; 15 | } 16 | 17 | pub trait AudioEncodder { 18 | fn encode(&mut self, in_buf: &[i16], out_buf: &mut [u8]) -> Option; 19 | } 20 | 21 | pub struct AudioTranscoder { 22 | decoder: Decoder, 23 | encoder: Encoder, 24 | tmp_buf: [i16; 960], 25 | } 26 | 27 | impl AudioTranscoder 28 | where 29 | Decoder: AudioDecoder, 30 | Encoder: AudioEncodder, 31 | { 32 | pub fn new(decoder: Decoder, encoder: Encoder) -> Self { 33 | Self { decoder, encoder, tmp_buf: [0; 960] } 34 | } 35 | 36 | pub fn transcode(&mut self, input: &[u8], output: &mut [u8]) -> Option { 37 | let raw_samples = self.decoder.decode(input, &mut self.tmp_buf)?; 38 | self.encoder.encode(&self.tmp_buf[0..raw_samples], output) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/media_codecs/src/opus.rs: -------------------------------------------------------------------------------- 1 | use opus_wrap::{Application, Channels}; 2 | 3 | use crate::{AudioDecoder, AudioEncodder}; 4 | 5 | #[allow(unused)] 6 | #[allow(clippy::redundant_field_names)] 7 | #[allow(clippy::len_zero)] 8 | #[allow(clippy::needless_lifetimes)] 9 | mod opus_wrap; 10 | 11 | pub struct OpusDecoder { 12 | decoder: opus_wrap::Decoder, 13 | } 14 | 15 | impl Default for OpusDecoder { 16 | fn default() -> Self { 17 | let decoder = opus_wrap::Decoder::new(48000, Channels::Mono).expect("Should create opus decoder"); 18 | Self { decoder } 19 | } 20 | } 21 | 22 | impl AudioDecoder for OpusDecoder { 23 | fn decode(&mut self, in_buf: &[u8], out_buf: &mut [i16]) -> Option { 24 | //TODO handle fec 25 | self.decoder.decode(in_buf, out_buf, false).ok() 26 | } 27 | } 28 | 29 | pub struct OpusEncoder { 30 | encoder: opus_wrap::Encoder, 31 | } 32 | 33 | impl Default for OpusEncoder { 34 | fn default() -> Self { 35 | let encoder = opus_wrap::Encoder::new(48000, Channels::Mono, Application::Voip).expect("Should create opus encoder"); 36 | Self { encoder } 37 | } 38 | } 39 | 40 | impl AudioEncodder for OpusEncoder { 41 | fn encode(&mut self, in_buf: &[i16], out_buf: &mut [u8]) -> Option { 42 | self.encoder.encode(in_buf, out_buf).ok() 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/media_codecs/src/resample.rs: -------------------------------------------------------------------------------- 1 | use libsoxr::{Datatype, IOSpec, QualityFlags, QualityRecipe, QualitySpec, Soxr}; 2 | 3 | fn create_soxr(from: u32, to: u32) -> Option { 4 | let io_spec = IOSpec::new(Datatype::Int16I, Datatype::Int16I); 5 | let quality_spec = QualitySpec::new(&QualityRecipe::VeryHigh, QualityFlags::HI_PREC_CLOCK); 6 | Soxr::create(from as f64, to as f64, 1, Some(&io_spec), Some(&quality_spec), None).ok() 7 | } 8 | 9 | pub struct Resampler { 10 | soxr: Soxr, 11 | } 12 | 13 | impl Default for Resampler { 14 | fn default() -> Self { 15 | Self { 16 | soxr: create_soxr(FROM, TO).expect("Should create soxr"), 17 | } 18 | } 19 | } 20 | 21 | impl Resampler { 22 | pub fn resample(&mut self, input: &[i16], output: &mut [i16]) -> Option { 23 | let (_used, generated) = self.soxr.process(Some(input), &mut output[0..(input.len() * TO as usize / FROM as usize)]).expect("Should process"); 24 | Some(generated) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/media_connector/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "atm0s-media-server-connector" 3 | version = "0.1.0-alpha.1" 4 | authors = ["Giang Minh "] 5 | edition = "2021" 6 | license = "MIT" 7 | description = "Media Connector Component for Atm0s Media Server" 8 | 9 | [dependencies] 10 | media-server-multi-tenancy = { workspace = true } 11 | media-server-protocol = { workspace = true } 12 | media-server-utils = { workspace = true } 13 | 14 | log = { workspace = true } 15 | clap = { workspace = true } 16 | serde = { workspace = true, features = ["derive"] } 17 | atm0s-sdn = { workspace = true } 18 | prost = { workspace = true } 19 | tokio = { workspace = true, features = ["sync"] } 20 | lru = { workspace = true } 21 | async-trait = { workspace = true } 22 | sea-orm-migration = { workspace = true } 23 | sea-orm = { workspace = true, features = [ 24 | "sqlx-sqlite", 25 | "sqlx-postgres", 26 | "sqlx-mysql", 27 | "runtime-tokio-rustls", 28 | ] } 29 | sea-query = { workspace = true } 30 | serde_json = { workspace = true } 31 | s3-presign = { workspace = true } 32 | uuid = { workspace = true, features = ["fast-rng", "v7"] } 33 | reqwest = { workspace = true, features = ["json"] } 34 | 35 | [dev-dependencies] 36 | tokio = { workspace = true, features = ["full"] } 37 | tracing-subscriber = { workspace = true } 38 | -------------------------------------------------------------------------------- /packages/media_connector/src/hooks.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use media_server_multi_tenancy::MultiTenancyStorage; 4 | use media_server_protocol::{multi_tenancy::AppId, protobuf::cluster_connector::HookEvent}; 5 | use tokio::sync::mpsc::UnboundedSender; 6 | use worker::HookWorker; 7 | 8 | pub use worker::HookBodyType; 9 | 10 | mod worker; 11 | 12 | pub struct ConnectorHookSender { 13 | workers_tx: Vec>, 14 | } 15 | 16 | impl ConnectorHookSender { 17 | pub fn new(workers: usize, hook_body_type: HookBodyType, app_storage: Arc) -> Self { 18 | let mut workers_tx = vec![]; 19 | for id in 0..workers { 20 | let (mut worker, tx) = HookWorker::new(hook_body_type, app_storage.clone()); 21 | workers_tx.push(tx); 22 | 23 | tokio::spawn(async move { 24 | log::info!("[ConnectorHookWorker {id}] started"); 25 | loop { 26 | if let Err(e) = worker.recv().await { 27 | log::error!("[ConnectorHookWorker {id}] error {e}"); 28 | break; 29 | } 30 | } 31 | log::info!("[ConnectorHookWorker {id}] ended"); 32 | }); 33 | } 34 | 35 | Self { workers_tx } 36 | } 37 | 38 | pub fn on_event(&self, app: AppId, event: HookEvent) { 39 | let index = event.ts as usize % self.workers_tx.len(); 40 | // TODO handle case worker crash 41 | self.workers_tx[index].send((app, event)).expect("Should send to worker"); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/media_connector/src/sql_storage/entity.rs: -------------------------------------------------------------------------------- 1 | pub mod event; 2 | pub mod peer; 3 | pub mod peer_session; 4 | pub mod room; 5 | pub mod session; 6 | -------------------------------------------------------------------------------- /packages/media_connector/src/sql_storage/entity/event.rs: -------------------------------------------------------------------------------- 1 | use sea_orm::entity::prelude::*; 2 | 3 | #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] 4 | #[sea_orm(table_name = "event")] 5 | pub struct Model { 6 | #[sea_orm(primary_key)] 7 | pub id: i32, 8 | pub node: i64, 9 | /// This is node timestamp 10 | pub node_ts: i64, 11 | pub session: i64, 12 | pub created_at: i64, 13 | pub event: String, 14 | pub meta: Option, 15 | } 16 | 17 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] 18 | pub enum Relation { 19 | #[sea_orm(belongs_to = "super::session::Entity", from = "Column::Session", to = "super::session::Column::Id")] 20 | Session, 21 | } 22 | 23 | impl Related for Entity { 24 | fn to() -> RelationDef { 25 | Relation::Session.def() 26 | } 27 | } 28 | 29 | impl ActiveModelBehavior for ActiveModel {} 30 | -------------------------------------------------------------------------------- /packages/media_connector/src/sql_storage/entity/peer.rs: -------------------------------------------------------------------------------- 1 | use sea_orm::entity::prelude::*; 2 | 3 | #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] 4 | #[sea_orm(table_name = "peer")] 5 | pub struct Model { 6 | #[sea_orm(primary_key)] 7 | pub id: i32, 8 | pub room: i32, 9 | pub peer: String, 10 | pub created_at: i64, 11 | } 12 | 13 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] 14 | pub enum Relation { 15 | #[sea_orm(belongs_to = "super::room::Entity", from = "Column::Room", to = "super::room::Column::Id")] 16 | Room, 17 | #[sea_orm(has_many = "super::peer_session::Entity")] 18 | Sessions, 19 | } 20 | 21 | impl Related for Entity { 22 | fn to() -> RelationDef { 23 | Relation::Room.def() 24 | } 25 | } 26 | 27 | impl Related for Entity { 28 | fn to() -> RelationDef { 29 | Relation::Sessions.def() 30 | } 31 | } 32 | 33 | impl ActiveModelBehavior for ActiveModel {} 34 | -------------------------------------------------------------------------------- /packages/media_connector/src/sql_storage/entity/peer_session.rs: -------------------------------------------------------------------------------- 1 | use sea_orm::entity::prelude::*; 2 | 3 | #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] 4 | #[sea_orm(table_name = "peer_session")] 5 | pub struct Model { 6 | #[sea_orm(primary_key)] 7 | pub id: i32, 8 | pub peer: i32, 9 | pub room: i32, 10 | pub session: i64, 11 | /// Record folder path 12 | pub record: Option, 13 | pub created_at: i64, 14 | /// This is node timestamp 15 | pub joined_at: i64, 16 | /// This is node timestamp 17 | pub leaved_at: Option, 18 | } 19 | 20 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] 21 | pub enum Relation { 22 | #[sea_orm(belongs_to = "super::peer::Entity", from = "Column::Peer", to = "super::peer::Column::Id")] 23 | Peer, 24 | #[sea_orm(belongs_to = "super::room::Entity", from = "Column::Room", to = "super::room::Column::Id")] 25 | Room, 26 | #[sea_orm(belongs_to = "super::session::Entity", from = "Column::Session", to = "super::session::Column::Id")] 27 | Session, 28 | } 29 | 30 | impl Related for Entity { 31 | fn to() -> RelationDef { 32 | Relation::Peer.def() 33 | } 34 | } 35 | 36 | impl Related for Entity { 37 | fn to() -> RelationDef { 38 | Relation::Room.def() 39 | } 40 | } 41 | 42 | impl Related for Entity { 43 | fn to() -> RelationDef { 44 | Relation::Session.def() 45 | } 46 | } 47 | 48 | impl ActiveModelBehavior for ActiveModel {} 49 | -------------------------------------------------------------------------------- /packages/media_connector/src/sql_storage/entity/room.rs: -------------------------------------------------------------------------------- 1 | use sea_orm::entity::prelude::*; 2 | 3 | #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] 4 | #[sea_orm(table_name = "room")] 5 | pub struct Model { 6 | #[sea_orm(primary_key)] 7 | pub id: i32, 8 | pub app: String, 9 | pub room: String, 10 | /// Record folder path 11 | pub record: Option, 12 | pub created_at: i64, 13 | /// This is node timestamp 14 | pub last_peer_leaved_at: Option, 15 | /// This is node timestamp 16 | pub destroyed_at: Option, 17 | } 18 | 19 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] 20 | pub enum Relation { 21 | #[sea_orm(has_many = "super::peer::Entity")] 22 | Peers, 23 | #[sea_orm(has_many = "super::peer_session::Entity")] 24 | PeerSessions, 25 | } 26 | 27 | impl Related for Entity { 28 | fn to() -> RelationDef { 29 | Relation::Peers.def() 30 | } 31 | } 32 | 33 | impl Related for Entity { 34 | fn to() -> RelationDef { 35 | Relation::PeerSessions.def() 36 | } 37 | } 38 | 39 | impl ActiveModelBehavior for ActiveModel {} 40 | -------------------------------------------------------------------------------- /packages/media_connector/src/sql_storage/entity/session.rs: -------------------------------------------------------------------------------- 1 | use sea_orm::entity::prelude::*; 2 | 3 | #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] 4 | #[sea_orm(table_name = "session")] 5 | pub struct Model { 6 | #[sea_orm(primary_key)] 7 | pub id: i64, 8 | pub app: String, 9 | pub created_at: i64, 10 | pub ip: Option, 11 | pub user_agent: Option, 12 | pub sdk: Option, 13 | } 14 | 15 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] 16 | pub enum Relation { 17 | #[sea_orm(has_many = "super::event::Entity")] 18 | Events, 19 | #[sea_orm(has_many = "super::peer_session::Entity")] 20 | Peers, 21 | } 22 | 23 | impl Related for Entity { 24 | fn to() -> RelationDef { 25 | Relation::Events.def() 26 | } 27 | } 28 | 29 | impl Related for Entity { 30 | fn to() -> RelationDef { 31 | Relation::Peers.def() 32 | } 33 | } 34 | 35 | impl ActiveModelBehavior for ActiveModel {} 36 | -------------------------------------------------------------------------------- /packages/media_connector/src/sql_storage/migration.rs: -------------------------------------------------------------------------------- 1 | use sea_orm_migration::{MigrationTrait, MigratorTrait}; 2 | 3 | mod m20240626_0001_init; 4 | mod m20240809_0001_change_node_id_i64; 5 | mod m20240824_0001_add_room_destroy_and_record; 6 | mod m20240929_0001_add_multi_tenancy; 7 | 8 | pub struct Migrator; 9 | 10 | #[async_trait::async_trait] 11 | impl MigratorTrait for Migrator { 12 | fn migrations() -> Vec> { 13 | vec![ 14 | Box::new(m20240626_0001_init::Migration), 15 | Box::new(m20240809_0001_change_node_id_i64::Migration), 16 | Box::new(m20240824_0001_add_room_destroy_and_record::Migration), 17 | Box::new(m20240929_0001_add_multi_tenancy::Migration), 18 | ] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/media_connector/src/sql_storage/migration/m20240809_0001_change_node_id_i64.rs: -------------------------------------------------------------------------------- 1 | use sea_orm::DatabaseBackend; 2 | use sea_orm_migration::prelude::*; 3 | 4 | #[derive(DeriveMigrationName)] 5 | pub struct Migration; 6 | 7 | #[async_trait::async_trait] 8 | impl MigrationTrait for Migration { 9 | async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { 10 | // because sqlite error with modify_column 11 | if manager.get_database_backend() != DatabaseBackend::Sqlite { 12 | manager 13 | .alter_table(Table::alter().table(Event::Table).modify_column(ColumnDef::new(Event::Node).big_integer()).to_owned()) 14 | .await?; 15 | } 16 | Ok(()) 17 | } 18 | 19 | async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { 20 | // because sqlite error with modify_column 21 | if manager.get_database_backend() != DatabaseBackend::Sqlite { 22 | manager 23 | .alter_table(Table::alter().table(Event::Table).modify_column(ColumnDef::new(Event::Node).unsigned()).to_owned()) 24 | .await?; 25 | } 26 | Ok(()) 27 | } 28 | } 29 | 30 | #[derive(Iden)] 31 | enum Event { 32 | Table, 33 | Node, 34 | } 35 | -------------------------------------------------------------------------------- /packages/media_connector/src/sql_storage/migration/m20240929_0001_add_multi_tenancy.rs: -------------------------------------------------------------------------------- 1 | use sea_orm_migration::prelude::*; 2 | 3 | #[derive(DeriveMigrationName)] 4 | pub struct Migration; 5 | 6 | #[async_trait::async_trait] 7 | impl MigrationTrait for Migration { 8 | async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { 9 | manager 10 | .alter_table(Table::alter().table(Room::Table).add_column(ColumnDef::new(Room::App).string().default("")).to_owned()) 11 | .await?; 12 | manager.create_index(Index::create().name("room_app").table(Room::Table).col(Room::App).to_owned()).await?; 13 | manager 14 | .alter_table(Table::alter().table(Session::Table).add_column(ColumnDef::new(Session::App).string().default("")).to_owned()) 15 | .await?; 16 | manager.create_index(Index::create().name("session_app").table(Session::Table).col(Session::App).to_owned()).await?; 17 | Ok(()) 18 | } 19 | 20 | async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { 21 | manager.drop_index(Index::drop().name("room_app").table(Room::Table).to_owned()).await?; 22 | manager.alter_table(Table::alter().table(Room::Table).drop_column(Room::App).to_owned()).await?; 23 | 24 | manager.drop_index(Index::drop().name("session_app").table(Session::Table).to_owned()).await?; 25 | manager.alter_table(Table::alter().table(Session::Table).drop_column(Session::App).to_owned()).await?; 26 | 27 | Ok(()) 28 | } 29 | } 30 | 31 | #[derive(Iden)] 32 | enum Room { 33 | Table, 34 | App, 35 | } 36 | 37 | #[derive(Iden)] 38 | enum Session { 39 | Table, 40 | App, 41 | } 42 | -------------------------------------------------------------------------------- /packages/media_console_front/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [0.1.0-alpha.3](https://github.com/8xFF/atm0s-media-server/compare/atm0s-media-server-console-front-v0.1.0-alpha.2...atm0s-media-server-console-front-v0.1.0-alpha.3) - 2025-03-02 11 | 12 | ### Other 13 | 14 | - migrate to tailwindcss v4, update layout, router ([#514](https://github.com/8xFF/atm0s-media-server/pull/514)) 15 | 16 | ## [0.1.0-alpha.2](https://github.com/8xFF/atm0s-media-server/compare/atm0s-media-server-console-front-v0.1.0-alpha.1...atm0s-media-server-console-front-v0.1.0-alpha.2) - 2025-02-26 17 | 18 | ### Added 19 | 20 | - simple nodes visualization in console (#509) 21 | -------------------------------------------------------------------------------- /packages/media_console_front/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "atm0s-media-server-console-front" 3 | version = "0.1.0-alpha.3" 4 | authors = ["Giang Minh "] 5 | edition = "2021" 6 | license = "MIT" 7 | description = "Console Frontend Component for Atm0s Media Server" 8 | 9 | [dependencies] 10 | media-server-utils = { workspace = true, features = ["embed-files"] } 11 | 12 | poem = { workspace = true, features = [] } 13 | rust-embed = { workspace = true, features = ["compression"] } 14 | reqwest = { workspace = true } 15 | log = { workspace = true } 16 | -------------------------------------------------------------------------------- /packages/media_console_front/build.rs: -------------------------------------------------------------------------------- 1 | use std::{env, process::Command}; 2 | 3 | fn main() { 4 | // Build Vite project when compiling in release mode 5 | if !cfg!(debug_assertions) && env::var("SKIP_BUILD_CONSOLE_FRONT").is_err() { 6 | Command::new("pnpm") 7 | .current_dir(format!("{}/react-app", env!("CARGO_MANIFEST_DIR"))) 8 | .args(["install"]) 9 | .stdout(std::process::Stdio::inherit()) 10 | .stderr(std::process::Stdio::inherit()) 11 | .status() 12 | .expect("Failed to install Vite project"); 13 | Command::new("pnpm") 14 | .current_dir(format!("{}/react-app", env!("CARGO_MANIFEST_DIR"))) 15 | .args(["run", "build"]) 16 | .stdout(std::process::Stdio::inherit()) 17 | .stderr(std::process::Stdio::inherit()) 18 | .status() 19 | .expect("Failed to build Vite project"); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/media_console_front/react-app/.env.example: -------------------------------------------------------------------------------- 1 | VITE_APP_API_URL= 2 | -------------------------------------------------------------------------------- /packages/media_console_front/react-app/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | .env 27 | -------------------------------------------------------------------------------- /packages/media_console_front/react-app/.prettierignore: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /packages/media_console_front/react-app/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": false, 4 | "printWidth": 125, 5 | "bracketSameLine": false, 6 | "embeddedLanguageFormatting": "auto", 7 | "htmlWhitespaceSensitivity": "css", 8 | "insertPragma": false, 9 | "jsxSingleQuote": false, 10 | "proseWrap": "preserve", 11 | "quoteProps": "as-needed", 12 | "requirePragma": false, 13 | "tabWidth": 2, 14 | "trailingComma": "es5", 15 | "vueIndentScriptAndStyle": false, 16 | "plugins": ["prettier-plugin-organize-imports", "prettier-plugin-tailwindcss", "prettier-plugin-packagejson"], 17 | "tailwindFunctions": ["cn", "tv", "cx", "clsx", "cva"] 18 | } 19 | -------------------------------------------------------------------------------- /packages/media_console_front/react-app/README.md: -------------------------------------------------------------------------------- 1 | # React + TypeScript + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | 10 | ## Expanding the ESLint configuration 11 | 12 | If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: 13 | 14 | - Configure the top-level `parserOptions` property like this: 15 | 16 | ```js 17 | export default tseslint.config({ 18 | languageOptions: { 19 | // other options... 20 | parserOptions: { 21 | project: ['./tsconfig.node.json', './tsconfig.app.json'], 22 | tsconfigRootDir: import.meta.dirname, 23 | }, 24 | }, 25 | }) 26 | ``` 27 | 28 | - Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked` 29 | - Optionally add `...tseslint.configs.stylisticTypeChecked` 30 | - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config: 31 | 32 | ```js 33 | // eslint.config.js 34 | import react from 'eslint-plugin-react' 35 | 36 | export default tseslint.config({ 37 | // Set the react version 38 | settings: { react: { version: '18.3' } }, 39 | plugins: { 40 | // Add the react plugin 41 | react, 42 | }, 43 | rules: { 44 | // other rules... 45 | // Enable its recommended rules 46 | ...react.configs.recommended.rules, 47 | ...react.configs['jsx-runtime'].rules, 48 | }, 49 | }) 50 | ``` 51 | -------------------------------------------------------------------------------- /packages/media_console_front/react-app/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "src/index.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } 22 | -------------------------------------------------------------------------------- /packages/media_console_front/react-app/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import reactHooks from 'eslint-plugin-react-hooks' 3 | import reactRefresh from 'eslint-plugin-react-refresh' 4 | import globals from 'globals' 5 | import tseslint from 'typescript-eslint' 6 | 7 | export default tseslint.config( 8 | { ignores: ['dist'] }, 9 | { 10 | extends: [js.configs.recommended, ...tseslint.configs.recommended], 11 | files: ['**/*.{ts,tsx}'], 12 | languageOptions: { 13 | ecmaVersion: 2020, 14 | globals: globals.browser, 15 | }, 16 | plugins: { 17 | 'react-hooks': reactHooks, 18 | 'react-refresh': reactRefresh, 19 | }, 20 | rules: { 21 | ...reactHooks.configs.recommended.rules, 22 | '@typescript-eslint/no-empty-object-type': 'off', 23 | '@typescript-eslint/no-unused-vars': 'off', 24 | '@typescript-eslint/no-explicit-any': 'off', 25 | }, 26 | } 27 | ) 28 | -------------------------------------------------------------------------------- /packages/media_console_front/react-app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Atm0s Media Server 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /packages/media_console_front/react-app/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | '@tailwindcss/postcss': {}, 4 | }, 5 | } 6 | -------------------------------------------------------------------------------- /packages/media_console_front/react-app/src/assets/index.ts: -------------------------------------------------------------------------------- 1 | import ImgLogo from './logo.svg' 2 | import ImgSignInBg from './sign-in-bg.webp' 3 | 4 | export { ImgLogo, ImgSignInBg } 5 | -------------------------------------------------------------------------------- /packages/media_console_front/react-app/src/assets/sign-in-bg.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/8xFF/atm0s-media-server/9ea8c02d3bc9b946edea1d566020da94390c98ca/packages/media_console_front/react-app/src/assets/sign-in-bg.webp -------------------------------------------------------------------------------- /packages/media_console_front/react-app/src/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './pagination' 2 | export * from './text-copy' 3 | export * from './zone' 4 | -------------------------------------------------------------------------------- /packages/media_console_front/react-app/src/components/text-copy.tsx: -------------------------------------------------------------------------------- 1 | import { useToast } from '@/hooks/use-toast' 2 | import { CopyIcon } from 'lucide-react' 3 | import { useMemo } from 'react' 4 | import { useCopyToClipboard } from 'usehooks-ts' 5 | 6 | type Props = { 7 | value: string 8 | } 9 | 10 | export const TextCopy: React.FC = ({ value }) => { 11 | const { toast } = useToast() 12 | const [, onCopy] = useCopyToClipboard() 13 | const filterAddr = useMemo(() => { 14 | const arr = value.split('/') 15 | return `${arr[0]}/${arr[1]}/${arr[2]}/${arr[3]}/${arr[4]}/${arr[5]}/.../${arr[arr.length - 2]}/${arr[arr.length - 1]}` 16 | }, [value]) 17 | return ( 18 |
19 | {filterAddr} 20 | 24 | onCopy(value).then(() => { 25 | toast({ 26 | title: 'Copied', 27 | duration: 2000, 28 | }) 29 | }) 30 | } 31 | /> 32 |
33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /packages/media_console_front/react-app/src/components/ui/alert.tsx: -------------------------------------------------------------------------------- 1 | import { cva, type VariantProps } from 'class-variance-authority' 2 | import * as React from 'react' 3 | 4 | import { cn } from '@/lib/utils' 5 | 6 | const alertVariants = cva( 7 | '[&>svg]:text-foreground relative w-full rounded-lg border px-4 py-3 text-sm [&>svg]:absolute [&>svg]:top-4 [&>svg]:left-4 [&>svg+div]:translate-y-[-3px] [&>svg~*]:pl-7', 8 | { 9 | variants: { 10 | variant: { 11 | default: 'bg-background text-foreground', 12 | destructive: 'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive', 13 | }, 14 | }, 15 | defaultVariants: { 16 | variant: 'default', 17 | }, 18 | } 19 | ) 20 | 21 | const Alert = React.forwardRef & VariantProps>( 22 | ({ className, variant, ...props }, ref) => ( 23 |
24 | ) 25 | ) 26 | Alert.displayName = 'Alert' 27 | 28 | const AlertTitle = React.forwardRef>( 29 | ({ className, ...props }, ref) => ( 30 |
31 | ) 32 | ) 33 | AlertTitle.displayName = 'AlertTitle' 34 | 35 | const AlertDescription = React.forwardRef>( 36 | ({ className, ...props }, ref) =>
37 | ) 38 | AlertDescription.displayName = 'AlertDescription' 39 | 40 | export { Alert, AlertDescription, AlertTitle } 41 | -------------------------------------------------------------------------------- /packages/media_console_front/react-app/src/components/ui/aspect-ratio.tsx: -------------------------------------------------------------------------------- 1 | import * as AspectRatioPrimitive from '@radix-ui/react-aspect-ratio' 2 | 3 | const AspectRatio = AspectRatioPrimitive.Root 4 | 5 | export { AspectRatio } 6 | -------------------------------------------------------------------------------- /packages/media_console_front/react-app/src/components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | import * as AvatarPrimitive from '@radix-ui/react-avatar' 2 | import * as React from 'react' 3 | 4 | import { cn } from '@/lib/utils' 5 | 6 | const Avatar = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >(({ className, ...props }, ref) => ( 10 | 15 | )) 16 | Avatar.displayName = AvatarPrimitive.Root.displayName 17 | 18 | const AvatarImage = React.forwardRef< 19 | React.ElementRef, 20 | React.ComponentPropsWithoutRef 21 | >(({ className, ...props }, ref) => ( 22 | 23 | )) 24 | AvatarImage.displayName = AvatarPrimitive.Image.displayName 25 | 26 | const AvatarFallback = React.forwardRef< 27 | React.ElementRef, 28 | React.ComponentPropsWithoutRef 29 | >(({ className, ...props }, ref) => ( 30 | 35 | )) 36 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName 37 | 38 | export { Avatar, AvatarFallback, AvatarImage } 39 | -------------------------------------------------------------------------------- /packages/media_console_front/react-app/src/components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import { cva, type VariantProps } from 'class-variance-authority' 2 | import * as React from 'react' 3 | 4 | import { cn } from '@/lib/utils' 5 | 6 | const badgeVariants = cva( 7 | 'focus:ring-ring inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:ring-2 focus:ring-offset-2 focus:outline-hidden', 8 | { 9 | variants: { 10 | variant: { 11 | default: 'bg-primary text-primary-foreground hover:bg-primary/80 border-transparent shadow-sm', 12 | secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80 border-transparent', 13 | destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/80 border-transparent shadow-sm', 14 | outline: 'text-foreground', 15 | }, 16 | }, 17 | defaultVariants: { 18 | variant: 'default', 19 | }, 20 | } 21 | ) 22 | 23 | export interface BadgeProps extends React.HTMLAttributes, VariantProps {} 24 | 25 | function Badge({ className, variant, ...props }: BadgeProps) { 26 | return
27 | } 28 | 29 | export { Badge, badgeVariants } 30 | -------------------------------------------------------------------------------- /packages/media_console_front/react-app/src/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { cn } from '@/lib/utils' 4 | 5 | const Card = React.forwardRef>(({ className, ...props }, ref) => ( 6 |
7 | )) 8 | Card.displayName = 'Card' 9 | 10 | const CardHeader = React.forwardRef>(({ className, ...props }, ref) => ( 11 |
12 | )) 13 | CardHeader.displayName = 'CardHeader' 14 | 15 | const CardTitle = React.forwardRef>(({ className, ...props }, ref) => ( 16 |
17 | )) 18 | CardTitle.displayName = 'CardTitle' 19 | 20 | const CardDescription = React.forwardRef>( 21 | ({ className, ...props }, ref) =>
22 | ) 23 | CardDescription.displayName = 'CardDescription' 24 | 25 | const CardContent = React.forwardRef>( 26 | ({ className, ...props }, ref) =>
27 | ) 28 | CardContent.displayName = 'CardContent' 29 | 30 | const CardFooter = React.forwardRef>(({ className, ...props }, ref) => ( 31 |
32 | )) 33 | CardFooter.displayName = 'CardFooter' 34 | 35 | export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } 36 | -------------------------------------------------------------------------------- /packages/media_console_front/react-app/src/components/ui/checkbox.tsx: -------------------------------------------------------------------------------- 1 | import * as CheckboxPrimitive from '@radix-ui/react-checkbox' 2 | import { Check } from 'lucide-react' 3 | import * as React from 'react' 4 | 5 | import { cn } from '@/lib/utils' 6 | 7 | const Checkbox = React.forwardRef< 8 | React.ElementRef, 9 | React.ComponentPropsWithoutRef 10 | >(({ className, ...props }, ref) => ( 11 | 19 | 20 | 21 | 22 | 23 | )) 24 | Checkbox.displayName = CheckboxPrimitive.Root.displayName 25 | 26 | export { Checkbox } 27 | -------------------------------------------------------------------------------- /packages/media_console_front/react-app/src/components/ui/collapsible.tsx: -------------------------------------------------------------------------------- 1 | import * as CollapsiblePrimitive from '@radix-ui/react-collapsible' 2 | 3 | const Collapsible = CollapsiblePrimitive.Root 4 | 5 | const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger 6 | 7 | const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent 8 | 9 | export { Collapsible, CollapsibleContent, CollapsibleTrigger } 10 | -------------------------------------------------------------------------------- /packages/media_console_front/react-app/src/components/ui/hover-card.tsx: -------------------------------------------------------------------------------- 1 | import * as HoverCardPrimitive from '@radix-ui/react-hover-card' 2 | import * as React from 'react' 3 | 4 | import { cn } from '@/lib/utils' 5 | 6 | const HoverCard = HoverCardPrimitive.Root 7 | 8 | const HoverCardTrigger = HoverCardPrimitive.Trigger 9 | 10 | const HoverCardContent = React.forwardRef< 11 | React.ElementRef, 12 | React.ComponentPropsWithoutRef 13 | >(({ className, align = 'center', sideOffset = 4, ...props }, ref) => ( 14 | 24 | )) 25 | HoverCardContent.displayName = HoverCardPrimitive.Content.displayName 26 | 27 | export { HoverCard, HoverCardContent, HoverCardTrigger } 28 | -------------------------------------------------------------------------------- /packages/media_console_front/react-app/src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { cn } from '@/lib/utils' 4 | 5 | const Input = React.forwardRef>(({ className, type, ...props }, ref) => { 6 | return ( 7 | 16 | ) 17 | }) 18 | Input.displayName = 'Input' 19 | 20 | export { Input } 21 | -------------------------------------------------------------------------------- /packages/media_console_front/react-app/src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | import * as LabelPrimitive from '@radix-ui/react-label' 2 | import { cva, type VariantProps } from 'class-variance-authority' 3 | import * as React from 'react' 4 | 5 | import { cn } from '@/lib/utils' 6 | 7 | const labelVariants = cva('text-sm leading-none font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70') 8 | 9 | const Label = React.forwardRef< 10 | React.ElementRef, 11 | React.ComponentPropsWithoutRef & VariantProps 12 | >(({ className, ...props }, ref) => ) 13 | Label.displayName = LabelPrimitive.Root.displayName 14 | 15 | export { Label } 16 | -------------------------------------------------------------------------------- /packages/media_console_front/react-app/src/components/ui/popover.tsx: -------------------------------------------------------------------------------- 1 | import * as PopoverPrimitive from '@radix-ui/react-popover' 2 | import * as React from 'react' 3 | 4 | import { cn } from '@/lib/utils' 5 | 6 | const Popover = PopoverPrimitive.Root 7 | 8 | const PopoverTrigger = PopoverPrimitive.Trigger 9 | 10 | const PopoverAnchor = PopoverPrimitive.Anchor 11 | 12 | const PopoverContent = React.forwardRef< 13 | React.ElementRef, 14 | React.ComponentPropsWithoutRef 15 | >(({ className, align = 'center', sideOffset = 4, ...props }, ref) => ( 16 | 17 | 27 | 28 | )) 29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName 30 | 31 | export { Popover, PopoverAnchor, PopoverContent, PopoverTrigger } 32 | -------------------------------------------------------------------------------- /packages/media_console_front/react-app/src/components/ui/progress.tsx: -------------------------------------------------------------------------------- 1 | import * as ProgressPrimitive from '@radix-ui/react-progress' 2 | import * as React from 'react' 3 | 4 | import { cn } from '@/lib/utils' 5 | 6 | const Progress = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >(({ className, value, ...props }, ref) => ( 10 | 15 | 19 | 20 | )) 21 | Progress.displayName = ProgressPrimitive.Root.displayName 22 | 23 | export { Progress } 24 | -------------------------------------------------------------------------------- /packages/media_console_front/react-app/src/components/ui/radio-group.tsx: -------------------------------------------------------------------------------- 1 | import * as RadioGroupPrimitive from '@radix-ui/react-radio-group' 2 | import { Circle } from 'lucide-react' 3 | import * as React from 'react' 4 | 5 | import { cn } from '@/lib/utils' 6 | 7 | const RadioGroup = React.forwardRef< 8 | React.ElementRef, 9 | React.ComponentPropsWithoutRef 10 | >(({ className, ...props }, ref) => { 11 | return 12 | }) 13 | RadioGroup.displayName = RadioGroupPrimitive.Root.displayName 14 | 15 | const RadioGroupItem = React.forwardRef< 16 | React.ElementRef, 17 | React.ComponentPropsWithoutRef 18 | >(({ className, ...props }, ref) => { 19 | return ( 20 | 28 | 29 | 30 | 31 | 32 | ) 33 | }) 34 | RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName 35 | 36 | export { RadioGroup, RadioGroupItem } 37 | -------------------------------------------------------------------------------- /packages/media_console_front/react-app/src/components/ui/scroll-area.tsx: -------------------------------------------------------------------------------- 1 | import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area' 2 | import * as React from 'react' 3 | 4 | import { cn } from '@/lib/utils' 5 | 6 | const ScrollArea = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >(({ className, children, ...props }, ref) => ( 10 | 11 | {children} 12 | 13 | 14 | 15 | )) 16 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName 17 | 18 | const ScrollBar = React.forwardRef< 19 | React.ElementRef, 20 | React.ComponentPropsWithoutRef 21 | >(({ className, orientation = 'vertical', ...props }, ref) => ( 22 | 33 | 34 | 35 | )) 36 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName 37 | 38 | export { ScrollArea, ScrollBar } 39 | -------------------------------------------------------------------------------- /packages/media_console_front/react-app/src/components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | import * as SeparatorPrimitive from '@radix-ui/react-separator' 2 | import * as React from 'react' 3 | 4 | import { cn } from '@/lib/utils' 5 | 6 | const Separator = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >(({ className, orientation = 'horizontal', decorative = true, ...props }, ref) => ( 10 | 17 | )) 18 | Separator.displayName = SeparatorPrimitive.Root.displayName 19 | 20 | export { Separator } 21 | -------------------------------------------------------------------------------- /packages/media_console_front/react-app/src/components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/lib/utils' 2 | 3 | function Skeleton({ className, ...props }: React.HTMLAttributes) { 4 | return
5 | } 6 | 7 | export { Skeleton } 8 | -------------------------------------------------------------------------------- /packages/media_console_front/react-app/src/components/ui/slider.tsx: -------------------------------------------------------------------------------- 1 | import * as SliderPrimitive from '@radix-ui/react-slider' 2 | import * as React from 'react' 3 | 4 | import { cn } from '@/lib/utils' 5 | 6 | const Slider = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >(({ className, ...props }, ref) => ( 10 | 15 | 16 | 17 | 18 | 19 | 20 | )) 21 | Slider.displayName = SliderPrimitive.Root.displayName 22 | 23 | export { Slider } 24 | -------------------------------------------------------------------------------- /packages/media_console_front/react-app/src/components/ui/sonner.tsx: -------------------------------------------------------------------------------- 1 | import { useTheme } from '@/providers' 2 | import { Toaster as Sonner } from 'sonner' 3 | 4 | type ToasterProps = React.ComponentProps 5 | 6 | const Toaster = ({ ...props }: ToasterProps) => { 7 | const { theme = 'system' } = useTheme() 8 | 9 | return ( 10 | 24 | ) 25 | } 26 | 27 | export { Toaster } 28 | -------------------------------------------------------------------------------- /packages/media_console_front/react-app/src/components/ui/switch.tsx: -------------------------------------------------------------------------------- 1 | import * as SwitchPrimitives from '@radix-ui/react-switch' 2 | import * as React from 'react' 3 | 4 | import { cn } from '@/lib/utils' 5 | 6 | const Switch = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >(({ className, ...props }, ref) => ( 10 | 18 | 23 | 24 | )) 25 | Switch.displayName = SwitchPrimitives.Root.displayName 26 | 27 | export { Switch } 28 | -------------------------------------------------------------------------------- /packages/media_console_front/react-app/src/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { cn } from '@/lib/utils' 4 | 5 | const Textarea = React.forwardRef>(({ className, ...props }, ref) => { 6 | return ( 7 |