├── .dockerignore ├── .editorconfig ├── .gitignore ├── .sqlx ├── query-442a3d608a84eea5be03de705148c5e374197684ee3434a18bb3853ccc99aa9e.json ├── query-62546d108f016726a829140914a5c88ca9f406ad7340002de172ff7168177b21.json └── query-dff0567e3d21d4662ba491aa5545f890325cd147a0e9ee32e9955b054b7e828d.json ├── Cargo.lock ├── Cargo.toml ├── LICENSE.md ├── README.md ├── crates ├── javelin-codec │ ├── Cargo.toml │ └── src │ │ ├── aac.rs │ │ ├── aac │ │ ├── adts.rs │ │ ├── common.rs │ │ ├── config.rs │ │ └── error.rs │ │ ├── avc.rs │ │ ├── avc │ │ ├── annexb.rs │ │ ├── avcc.rs │ │ ├── config.rs │ │ ├── error.rs │ │ └── nal.rs │ │ ├── error.rs │ │ ├── flv.rs │ │ ├── flv │ │ ├── error.rs │ │ ├── tag.rs │ │ └── tag │ │ │ ├── audio.rs │ │ │ └── video.rs │ │ ├── lib.rs │ │ ├── mpegts.rs │ │ └── mpegts │ │ ├── error.rs │ │ └── transport_stream.rs ├── javelin-core │ ├── Cargo.toml │ └── src │ │ ├── config.rs │ │ ├── lib.rs │ │ ├── session.rs │ │ └── session │ │ ├── instance.rs │ │ ├── manager.rs │ │ └── transport.rs ├── javelin-hls │ ├── Cargo.toml │ └── src │ │ ├── config.rs │ │ ├── file_cleaner.rs │ │ ├── lib.rs │ │ ├── m3u8.rs │ │ ├── service.rs │ │ └── writer.rs ├── javelin-rtmp │ ├── Cargo.toml │ └── src │ │ ├── config.rs │ │ ├── convert.rs │ │ ├── error.rs │ │ ├── lib.rs │ │ ├── peer.rs │ │ ├── proto.rs │ │ └── service.rs ├── javelin-srt │ ├── Cargo.toml │ └── src │ │ ├── config.rs │ │ ├── lib.rs │ │ ├── peer.rs │ │ └── service.rs ├── javelin-types │ ├── Cargo.toml │ └── src │ │ ├── data.rs │ │ ├── lib.rs │ │ ├── models.rs │ │ └── packet.rs └── javelin │ ├── Cargo.toml │ ├── database │ └── migrations │ │ └── 20240810235858_create_users.sql │ └── src │ ├── bin │ ├── cli.rs │ └── server.rs │ ├── database.rs │ └── lib.rs ├── deny.toml ├── dist ├── Dockerfile └── compose.yml ├── docs ├── CHANGELOG.md └── CONTRIBUTING.md ├── flake.lock ├── flake.nix ├── rust-toolchain.toml ├── rustfmt.toml └── tools └── docker /.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | !src/ 3 | !Cargo.toml 4 | !Cargo.lock 5 | 6 | !javelin-codec/ 7 | !javelin-codec/* 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # for the sake of sanity 2 | 3 | # topmost editorconfig file 4 | root = true 5 | 6 | # always use Unix-style newlines, 7 | # use 4 spaces for indentation 8 | # and use UTF-8 encoding 9 | [*] 10 | indent_style = space 11 | end_of_line = lf 12 | indent_size = 4 13 | charset = utf-8 14 | trim_trailing_whitespace = true 15 | insert_final_newline = true 16 | 17 | [*.yml] 18 | indent_size = 2 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | 4 | # CI 5 | /.cargo 6 | /.rustup 7 | 8 | # Editor/IDE 9 | /.idea 10 | /.vscode 11 | 12 | # Misc 13 | /data 14 | /config 15 | /.env* 16 | *.log 17 | -------------------------------------------------------------------------------- /.sqlx/query-442a3d608a84eea5be03de705148c5e374197684ee3434a18bb3853ccc99aa9e.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "SELECT name, key FROM users WHERE name = $1", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "name": "name", 8 | "ordinal": 0, 9 | "type_info": "Text" 10 | }, 11 | { 12 | "name": "key", 13 | "ordinal": 1, 14 | "type_info": "Text" 15 | } 16 | ], 17 | "parameters": { 18 | "Right": 1 19 | }, 20 | "nullable": [ 21 | false, 22 | false 23 | ] 24 | }, 25 | "hash": "442a3d608a84eea5be03de705148c5e374197684ee3434a18bb3853ccc99aa9e" 26 | } 27 | -------------------------------------------------------------------------------- /.sqlx/query-62546d108f016726a829140914a5c88ca9f406ad7340002de172ff7168177b21.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "INSERT INTO users (name, key) VALUES ($1, $2)", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Right": 2 8 | }, 9 | "nullable": [] 10 | }, 11 | "hash": "62546d108f016726a829140914a5c88ca9f406ad7340002de172ff7168177b21" 12 | } 13 | -------------------------------------------------------------------------------- /.sqlx/query-dff0567e3d21d4662ba491aa5545f890325cd147a0e9ee32e9955b054b7e828d.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "UPDATE users SET key = $1 WHERE name = $2", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Right": 2 8 | }, 9 | "nullable": [] 10 | }, 11 | "hash": "dff0567e3d21d4662ba491aa5545f890325cd147a0e9ee32e9955b054b7e828d" 12 | } 13 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = [ 4 | "./crates/javelin", 5 | "./crates/javelin-codec", 6 | "./crates/javelin-core", 7 | "./crates/javelin-hls", 8 | "./crates/javelin-rtmp", 9 | "./crates/javelin-types", 10 | "./crates/javelin-srt", 11 | ] 12 | 13 | 14 | [workspace.package] 15 | version = "0.4.0-dev.1" 16 | authors = ["Patrick Auernig"] 17 | edition = "2021" 18 | rust-version = "1.82.0" 19 | license-file = "LICENSE.md" 20 | readme = "README.md" 21 | repository = "https://gitlab.com/valeth/javelin.git" 22 | categories = ["multimedia", "multimedia::audio", "multimedia::video"] 23 | keywords = ["audio", "video"] 24 | 25 | 26 | [workspace.dependencies] 27 | anyhow = "1.0" 28 | async-trait = "0.1" 29 | axum = "0.7" 30 | bincode = "1.3" 31 | chrono = "0.4" 32 | futures = "0.3" 33 | thiserror = "1.0" 34 | tracing = "0.1" 35 | tracing-subscriber = "0.3" 36 | 37 | 38 | [workspace.dependencies.sqlx] 39 | version = "0.8" 40 | features = ["runtime-tokio"] 41 | 42 | [workspace.dependencies.bytes] 43 | version = "1.7" 44 | features = ["serde"] 45 | 46 | [workspace.dependencies.serde] 47 | version = "1.0" 48 | features = ["derive"] 49 | 50 | [workspace.dependencies.tokio] 51 | version = "1.39" 52 | default-features = false 53 | features = ["time", "macros"] 54 | 55 | [workspace.dependencies.javelin-core] 56 | version = "0.4.0-dev.1" 57 | path = "crates/javelin-core" 58 | 59 | [workspace.dependencies.javelin-codec] 60 | version = "0.4.0-dev.1" 61 | path = "crates/javelin-codec" 62 | 63 | [workspace.dependencies.javelin-types] 64 | version = "0.4.0-dev.1" 65 | path = "crates/javelin-types" 66 | 67 | [workspace.dependencies.javelin-rtmp] 68 | version = "0.4.0-dev.1" 69 | path = "crates/javelin-rtmp" 70 | 71 | [workspace.dependencies.javelin-hls] 72 | version = "0.4.0-dev.1" 73 | path = "crates/javelin-hls" 74 | 75 | [workspace.dependencies.javelin-srt] 76 | version = "0.4.0-dev.1" 77 | path = "crates/javelin-srt" 78 | 79 | 80 | [profile.release] 81 | opt-level = 3 82 | lto = false # some linking errors with lto = true 83 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Javelin Software License 2 | 3 | Copyright © 2024 Patrick Auernig 4 | 5 | Permission is hereby granted to copy, modify, redistribute, and use the software with the following restrictions: 6 | 7 | - Redistribution of the software has to be provided free of charge 8 | - You cannot sell, rent out, or otherwise profit of the software, or modified copies of the software 9 | - You shall not use the software to train any machine learning model, or other artificial intelligence products, in any way 10 | 11 | Any use of the software in violation of this license will terminate your rights under this license 12 | immediately for the current version, and any other versions of the software. 13 | 14 | This license shall be included in all copies, and modified copies, of the software. 15 | 16 | The software is provided "as is", without any warranty. 17 | The copyright holder shall not be held liable for any damages caused through the use of the software. 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Javelin 2 | 3 | > [!NOTE] 4 | > This project is still work in progress. 5 | > 6 | > Everything before version 1.0 is subject to change. 7 | > 8 | > Use at your own risk. 9 | 10 | 11 | ## Changelog 12 | 13 | See the [Changelog] for a detailed list of changes. 14 | 15 | 16 | ## Development 17 | 18 | Check out our [Contribution Guide] if you want to contribute. 19 | 20 | 21 | 22 | 23 | [Changelog]: docs/CHANGELOG.md 24 | [Contribution Guide]: docs/CONTRIBUTING.md 25 | -------------------------------------------------------------------------------- /crates/javelin-codec/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "javelin-codec" 3 | description = "Simple streaming server (codecs)" 4 | version.workspace = true 5 | authors.workspace = true 6 | edition.workspace = true 7 | rust-version.workspace = true 8 | license-file.workspace = true 9 | readme.workspace = true 10 | repository.workspace = true 11 | categories.workspace = true 12 | keywords.workspace = true 13 | publish = false 14 | 15 | 16 | [features] 17 | default = [] 18 | mpegts = ["mpeg2ts"] 19 | 20 | 21 | [dependencies] 22 | bytes.workspace = true 23 | tracing.workspace = true 24 | thiserror.workspace = true 25 | 26 | [dependencies.mpeg2ts] 27 | version = "0.3" 28 | optional = true 29 | -------------------------------------------------------------------------------- /crates/javelin-codec/src/aac.rs: -------------------------------------------------------------------------------- 1 | pub mod adts; 2 | pub mod common; 3 | pub mod config; 4 | pub mod error; 5 | 6 | 7 | use std::convert::TryInto; 8 | 9 | use tracing::warn; 10 | 11 | pub use self::adts::AudioDataTransportStream; 12 | use self::config::AudioSpecificConfiguration; 13 | pub use self::error::AacError; 14 | use crate::{FormatReader, FormatWriter, ReadFormat, WriteFormat}; 15 | 16 | 17 | pub struct Aac(Vec); 18 | 19 | impl From<&[u8]> for Aac { 20 | fn from(val: &[u8]) -> Self { 21 | Self(Vec::from(val)) 22 | } 23 | } 24 | 25 | impl From for Vec { 26 | fn from(val: Aac) -> Self { 27 | val.0 28 | } 29 | } 30 | 31 | pub struct Raw; 32 | 33 | impl ReadFormat for Raw { 34 | type Context = (); 35 | type Error = AacError; 36 | 37 | fn read_format(&self, input: &[u8], _ctx: &Self::Context) -> Result { 38 | Ok(input.into()) 39 | } 40 | } 41 | 42 | 43 | enum State { 44 | Initializing, 45 | Ready(AudioSpecificConfiguration), 46 | } 47 | 48 | pub struct AacCoder { 49 | state: State, 50 | } 51 | 52 | impl AacCoder { 53 | pub fn new() -> Self { 54 | Self::default() 55 | } 56 | 57 | pub fn set_asc(&mut self, asc: A) -> Result<(), AacError> 58 | where 59 | A: TryInto, 60 | { 61 | self.state = State::Ready(asc.try_into()?); 62 | Ok(()) 63 | } 64 | } 65 | 66 | impl Default for AacCoder { 67 | fn default() -> Self { 68 | Self { 69 | state: State::Initializing, 70 | } 71 | } 72 | } 73 | 74 | impl FormatReader for AacCoder { 75 | type Error = AacError; 76 | type Output = Aac; 77 | 78 | fn read_format( 79 | &mut self, 80 | format: Raw, 81 | input: &[u8], 82 | ) -> Result, Self::Error> { 83 | Ok(match &self.state { 84 | State::Initializing => { 85 | warn!("AAC reader was not initialized, trying to initialize from current payload"); 86 | self.set_asc(input)?; 87 | None 88 | } 89 | State::Ready(_) => Some(format.read_format(input, &())?), 90 | }) 91 | } 92 | } 93 | 94 | impl FormatWriter for AacCoder { 95 | type Error = AacError; 96 | type Input = Aac; 97 | 98 | fn write_format( 99 | &mut self, 100 | format: AudioDataTransportStream, 101 | input: Self::Input, 102 | ) -> Result, Self::Error> { 103 | Ok(match &self.state { 104 | State::Initializing => return Err(AacError::NotInitialized), 105 | State::Ready(asc) => format.write_format(input, asc)?, 106 | }) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /crates/javelin-codec/src/aac/adts.rs: -------------------------------------------------------------------------------- 1 | use bytes::BufMut; 2 | 3 | use super::config::AudioSpecificConfiguration; 4 | use crate::aac::error::AacError; 5 | use crate::aac::Aac; 6 | use crate::WriteFormat; 7 | 8 | 9 | // Bits | Description 10 | // ---- | ----------- 11 | // 12 | Sync word, constant 0xFFF 12 | // 1 | MPEG version 13 | // 2 | Layer, constant 0x00 14 | // 1 | Protection flag 15 | // 2 | Profile 16 | // 4 | MPEG-4 sampling frequency index 17 | // 1 | Private, constant 0x00 18 | // 3 | MPEG-4 channel configuration 19 | // 1 | Originality 20 | // 1 | Home 21 | // 1 | Copyrighted ID 22 | // 1 | Copyrighted ID start 23 | // 13 | Frame length 24 | // 11 | Buffer fullness 25 | // 2 | Number of AAC frames - 1 26 | // 16 | CRC if protection flag not set 27 | // 28 | // https://wiki.multimedia.cx/index.php/ADTS 29 | #[derive(Debug, Clone)] 30 | pub struct AudioDataTransportStream; 31 | 32 | impl AudioDataTransportStream { 33 | const PROTECTION_ABSENCE: u16 = 0x0001; 34 | const SYNCWORD: u16 = 0xFFF0; 35 | } 36 | 37 | impl WriteFormat for AudioDataTransportStream { 38 | type Context = AudioSpecificConfiguration; 39 | type Error = AacError; 40 | 41 | fn write_format(&self, input: Aac, ctx: &Self::Context) -> Result, Self::Error> { 42 | let payload: Vec = input.into(); 43 | let mut tmp = Vec::with_capacity(56 + payload.len()); 44 | 45 | // Syncword (12 bits), MPEG version (1 bit = 0), 46 | // layer (2 bits = 0) and protection absence (1 bit = 1) 47 | tmp.put_u16(Self::SYNCWORD | Self::PROTECTION_ABSENCE); 48 | 49 | // Profile (2 bits = 0), sampling frequency index (4 bits), 50 | // private (1 bit = 0) and channel configuration (1 bit) 51 | let object_type = ctx.object_type as u8; 52 | let profile = (object_type - 1) << 6; 53 | 54 | let sampling_frequency_index = u8::from(ctx.sampling_frequency_index) << 2; 55 | if sampling_frequency_index == 0x0F { 56 | return Err(AacError::ForbiddenSamplingFrequencyIndex( 57 | sampling_frequency_index, 58 | )); 59 | } 60 | 61 | let channel_configuration: u8 = ctx.channel_configuration.into(); 62 | let channel_configuration1 = (channel_configuration & 0x07) >> 2; 63 | tmp.put_u8(profile | sampling_frequency_index | channel_configuration1); 64 | 65 | // Channel configuration cont. (2 bits), originality (1 bit = 0), 66 | // home (1 bit = 0), copyrighted id (1 bit = 0) 67 | // copyright id start (1 bit = 0) and frame length (2 bits) 68 | let channel_configuration2 = (channel_configuration & 0x03) << 6; 69 | 70 | // Header is 7 bytes long if protection is absent, 71 | // 9 bytes otherwise (CRC requires 2 bytes). 72 | let frame_length = (payload.len() + 7) as u16; 73 | let frame_length1 = ((frame_length & 0x1FFF) >> 11) as u8; 74 | tmp.put_u8(channel_configuration2 | frame_length1); 75 | 76 | // Frame length cont. (11 bits) and buffer fullness (5 bits) 77 | let frame_length2 = (frame_length & 0x7FF) << 5; 78 | tmp.put_u16(frame_length2 | 0b0000_0000_0001_1111); 79 | 80 | // Buffer fullness cont. (6 bits) and number of AAC frames minus one (2 bits = 0) 81 | tmp.put_u8(0b1111_1100); 82 | 83 | tmp.extend(payload); 84 | 85 | Ok(tmp) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /crates/javelin-codec/src/aac/common.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryFrom; 2 | 3 | use super::AacError; 4 | 5 | #[derive(Debug, Clone, Copy)] 6 | pub struct SamplingFrequencyIndex(u8); 7 | 8 | impl From for u8 { 9 | fn from(val: SamplingFrequencyIndex) -> Self { 10 | val.0 11 | } 12 | } 13 | 14 | impl TryFrom for SamplingFrequencyIndex { 15 | type Error = AacError; 16 | 17 | fn try_from(val: u8) -> Result { 18 | match val { 19 | 0..=12 | 15 => Ok(Self(val)), 20 | _ => Err(AacError::UnsupportedFrequencyIndex(val)), 21 | } 22 | } 23 | } 24 | 25 | 26 | #[derive(Debug, Clone, Copy)] 27 | pub struct ChannelConfiguration(u8); 28 | 29 | impl From for u8 { 30 | fn from(val: ChannelConfiguration) -> Self { 31 | val.0 32 | } 33 | } 34 | 35 | impl TryFrom for ChannelConfiguration { 36 | type Error = AacError; 37 | 38 | fn try_from(val: u8) -> Result { 39 | match val { 40 | 0..=7 => Ok(Self(val)), 41 | _ => Err(AacError::UnsupportedChannelConfiguration(val)), 42 | } 43 | } 44 | } 45 | 46 | 47 | // See [MPEG-4 Audio Object Types][audio_object_types] 48 | // 49 | // [audio_object_types]: https://en.wikipedia.org/wiki/MPEG-4_Part_3#MPEG-4_Audio_Object_Types 50 | #[allow(clippy::enum_variant_names)] 51 | #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] 52 | pub enum AudioObjectType { 53 | AacMain = 1, 54 | AacLowComplexity = 2, 55 | AacScalableSampleRate = 3, 56 | AacLongTermPrediction = 4, 57 | } 58 | 59 | impl TryFrom for AudioObjectType { 60 | type Error = AacError; 61 | 62 | fn try_from(value: u8) -> Result { 63 | Ok(match value { 64 | 1 => Self::AacMain, 65 | 2 => Self::AacLowComplexity, 66 | 3 => Self::AacScalableSampleRate, 67 | 4 => Self::AacLongTermPrediction, 68 | _ => return Err(AacError::UnsupportedAudioFormat), 69 | }) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /crates/javelin-codec/src/aac/config.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryFrom; 2 | use std::io::Cursor; 3 | 4 | use bytes::Buf; 5 | 6 | use super::common::{AudioObjectType, ChannelConfiguration, SamplingFrequencyIndex}; 7 | use super::AacError; 8 | 9 | 10 | // Bits | Description 11 | // ---- | ----------- 12 | // 5 | Audio object type 13 | // 4 | Sampling frequency index 14 | // 4 | Channel configuration 15 | // AOT specific section 16 | // 1 | Frame length flag 17 | // 1 | Depends on core coder 18 | // 1 | Extension flag 19 | // 20 | #[derive(Debug, Clone)] 21 | pub struct AudioSpecificConfiguration { 22 | pub object_type: AudioObjectType, 23 | pub sampling_frequency_index: SamplingFrequencyIndex, 24 | pub sampling_frequency: Option, 25 | pub channel_configuration: ChannelConfiguration, 26 | pub frame_length_flag: bool, 27 | pub depends_on_core_coder: bool, 28 | pub extension_flag: bool, 29 | } 30 | 31 | impl TryFrom<&[u8]> for AudioSpecificConfiguration { 32 | type Error = AacError; 33 | 34 | fn try_from(val: &[u8]) -> Result { 35 | if val.len() < 2 { 36 | return Err(AacError::NotEnoughData("AAC audio specific config")); 37 | } 38 | 39 | let mut buf = Cursor::new(val); 40 | 41 | let header_a = buf.get_u8(); 42 | let header_b = buf.get_u8(); 43 | 44 | let object_type = AudioObjectType::try_from((header_a & 0xF8) >> 3)?; 45 | 46 | let sf_idx = ((header_a & 0x07) << 1) | (header_b >> 7); 47 | let sampling_frequency_index = SamplingFrequencyIndex::try_from(sf_idx)?; 48 | 49 | let channel_configuration = ChannelConfiguration::try_from((header_b >> 3) & 0x0F)?; 50 | let frame_length_flag = (header_b & 0x04) == 0x04; 51 | let depends_on_core_coder = (header_b & 0x02) == 0x02; 52 | let extension_flag = (header_b & 0x01) == 0x01; 53 | 54 | Ok(Self { 55 | object_type, 56 | sampling_frequency_index, 57 | sampling_frequency: None, 58 | channel_configuration, 59 | frame_length_flag, 60 | depends_on_core_coder, 61 | extension_flag, 62 | }) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /crates/javelin-codec/src/aac/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | 4 | #[derive(Debug, Error)] 5 | pub enum AacError { 6 | #[error("AAC coder not initialized")] 7 | NotInitialized, 8 | 9 | #[error("Not enough data: {0}")] 10 | NotEnoughData(&'static str), 11 | 12 | #[error("Unsupported audio object type")] 13 | UnsupportedAudioFormat, 14 | 15 | #[error("Reserved or unsupported frequency index {0}")] 16 | UnsupportedFrequencyIndex(u8), 17 | 18 | #[error("Reserved or unsupported channel configuration {0}")] 19 | UnsupportedChannelConfiguration(u8), 20 | 21 | #[error("Got forbidden sampling frequency index {0}")] 22 | ForbiddenSamplingFrequencyIndex(u8), 23 | } 24 | -------------------------------------------------------------------------------- /crates/javelin-codec/src/avc.rs: -------------------------------------------------------------------------------- 1 | pub mod annexb; 2 | pub mod avcc; 3 | pub mod config; 4 | mod error; 5 | pub mod nal; 6 | 7 | use std::convert::TryInto; 8 | use std::fmt::{self, Debug}; 9 | 10 | pub use self::annexb::AnnexB; 11 | pub use self::avcc::Avcc; 12 | use self::config::DecoderConfigurationRecord; 13 | pub use self::error::AvcError; 14 | use crate::{FormatReader, FormatWriter, ReadFormat, WriteFormat}; 15 | 16 | 17 | pub struct Avc(Vec); 18 | 19 | impl From> for Avc { 20 | fn from(val: Vec) -> Self { 21 | Self(val) 22 | } 23 | } 24 | 25 | impl From for Vec { 26 | fn from(val: Avc) -> Self { 27 | val.0 28 | } 29 | } 30 | 31 | 32 | #[derive(Debug, PartialEq, Eq)] 33 | enum State { 34 | Initializing, 35 | Ready, 36 | } 37 | 38 | impl Default for State { 39 | fn default() -> Self { 40 | Self::Initializing 41 | } 42 | } 43 | 44 | 45 | #[derive(Default)] 46 | pub struct AvcCoder { 47 | dcr: Option, 48 | state: State, 49 | } 50 | 51 | impl AvcCoder { 52 | pub fn new() -> Self { 53 | Self::default() 54 | } 55 | 56 | pub fn set_dcr(&mut self, dcr: D) -> Result<(), AvcError> 57 | where 58 | D: TryInto, 59 | { 60 | let dcr = dcr.try_into()?; 61 | self.dcr = Some(dcr); 62 | self.state = State::Ready; 63 | Ok(()) 64 | } 65 | } 66 | 67 | impl Debug for AvcCoder { 68 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 69 | f.debug_struct("AvcDecoder") 70 | .field("state", &self.state) 71 | .finish() 72 | } 73 | } 74 | 75 | impl FormatReader for AvcCoder { 76 | type Error = AvcError; 77 | type Output = Avc; 78 | 79 | fn read_format( 80 | &mut self, 81 | format: Avcc, 82 | input: &[u8], 83 | ) -> Result, Self::Error> { 84 | Ok(match &self.state { 85 | State::Initializing => { 86 | self.set_dcr(input) 87 | .map_err(|_| AvcError::DecoderInitializationFailed)?; 88 | None 89 | } 90 | State::Ready => { 91 | let dcr = self.dcr.as_ref().unwrap(); 92 | Some(format.read_format(input, dcr)?) 93 | } 94 | }) 95 | } 96 | } 97 | 98 | impl FormatWriter for AvcCoder { 99 | type Error = AvcError; 100 | type Input = Avc; 101 | 102 | fn write_format(&mut self, format: AnnexB, input: Self::Input) -> Result, Self::Error> { 103 | match &self.state { 104 | State::Initializing => Err(AvcError::NotInitialized), 105 | State::Ready => { 106 | let dcr = self.dcr.as_ref().unwrap(); 107 | Ok(format.write_format(input, dcr)?) 108 | } 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /crates/javelin-codec/src/avc/annexb.rs: -------------------------------------------------------------------------------- 1 | use tracing::debug; 2 | 3 | use crate::avc::config::DecoderConfigurationRecord; 4 | use crate::avc::error::AvcError; 5 | use crate::avc::{nal, Avc}; 6 | use crate::WriteFormat; 7 | 8 | 9 | pub struct AnnexB; 10 | 11 | impl AnnexB { 12 | const ACCESS_UNIT_DELIMITER: &'static [u8] = &[0x00, 0x00, 0x00, 0x01, 0x09, 0xF0]; 13 | const DELIMITER1: &'static [u8] = &[0x00, 0x00, 0x01]; 14 | const DELIMITER2: &'static [u8] = &[0x00, 0x00, 0x00, 0x01]; 15 | } 16 | 17 | impl WriteFormat for AnnexB { 18 | type Context = DecoderConfigurationRecord; 19 | type Error = AvcError; 20 | 21 | fn write_format(&self, input: Avc, ctx: &Self::Context) -> Result, Self::Error> { 22 | let mut out_buffer = Vec::new(); 23 | let mut aud_appended = false; 24 | let mut sps_and_pps_appended = false; 25 | let nalus: Vec = input.into(); 26 | 27 | for nalu in nalus { 28 | use nal::UnitType::*; 29 | 30 | match &nalu.kind { 31 | SequenceParameterSet | PictureParameterSet | AccessUnitDelimiter => continue, 32 | NonIdrPicture | SupplementaryEnhancementInformation => { 33 | if !aud_appended { 34 | out_buffer.extend(Self::ACCESS_UNIT_DELIMITER); 35 | aud_appended = true; 36 | } 37 | } 38 | IdrPicture => { 39 | if !aud_appended { 40 | out_buffer.extend(Self::ACCESS_UNIT_DELIMITER); 41 | aud_appended = true; 42 | } 43 | 44 | if !sps_and_pps_appended { 45 | if let Some(sps) = ctx.sps.first() { 46 | out_buffer.extend(Self::DELIMITER2); 47 | let tmp: Vec = sps.into(); 48 | out_buffer.extend(tmp); 49 | } 50 | 51 | if let Some(pps) = ctx.pps.first() { 52 | out_buffer.extend(Self::DELIMITER2); 53 | let tmp: Vec = pps.into(); 54 | out_buffer.extend(tmp); 55 | } 56 | 57 | sps_and_pps_appended = true; 58 | } 59 | } 60 | t => debug!("Received unhandled NALU type {:?}", t), 61 | } 62 | 63 | out_buffer.extend(Self::DELIMITER1); 64 | 65 | let nalu_data: Vec = nalu.into(); 66 | out_buffer.extend(nalu_data); 67 | } 68 | 69 | Ok(out_buffer) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /crates/javelin-codec/src/avc/avcc.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryFrom; 2 | use std::io::Cursor; 3 | 4 | use bytes::Buf; 5 | 6 | use crate::avc::config::DecoderConfigurationRecord; 7 | use crate::avc::error::AvcError; 8 | use crate::avc::{nal, Avc}; 9 | use crate::ReadFormat; 10 | 11 | 12 | pub struct Avcc; 13 | 14 | impl ReadFormat for Avcc { 15 | type Context = DecoderConfigurationRecord; 16 | type Error = AvcError; 17 | 18 | fn read_format(&self, input: &[u8], ctx: &Self::Context) -> Result { 19 | let mut buf = Cursor::new(input); 20 | let mut nal_units = Vec::new(); 21 | 22 | while buf.has_remaining() { 23 | let unit_size = ctx.nalu_size as usize; 24 | 25 | if buf.remaining() < unit_size { 26 | return Err(AvcError::NotEnoughData("NALU size")); 27 | } 28 | let nalu_length = buf.get_uint(unit_size) as usize; 29 | 30 | let nalu_data = buf 31 | .chunk() 32 | .get(..nalu_length) 33 | .ok_or(AvcError::NotEnoughData("NALU data"))? 34 | .to_owned(); 35 | 36 | buf.advance(nalu_length); 37 | 38 | let nal_unit = nal::Unit::try_from(&*nalu_data)?; 39 | nal_units.push(nal_unit); 40 | } 41 | 42 | Ok(nal_units.into()) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /crates/javelin-codec/src/avc/config.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryFrom; 2 | use std::io::Cursor; 3 | 4 | use bytes::Buf; 5 | 6 | use super::{nal, AvcError}; 7 | 8 | 9 | // Bits | Name 10 | // ---- | ---- 11 | // 8 | Version 12 | // 8 | Profile Indication 13 | // 8 | Profile Compatability 14 | // 8 | Level Indication 15 | // 6 | Reserved 16 | // 2 | NALU Length 17 | // 3 | Reserved 18 | // 5 | SPS Count 19 | // 16 | SPS Length 20 | // var | SPS 21 | // 8 | PPS Count 22 | // 16 | PPS Length 23 | // var | PPS 24 | #[derive(Debug, Clone)] 25 | pub struct DecoderConfigurationRecord { 26 | pub version: u8, 27 | pub profile_indication: u8, 28 | pub profile_compatability: u8, 29 | pub level_indication: u8, 30 | pub nalu_size: u8, 31 | pub sps: Vec, 32 | pub pps: Vec, 33 | } 34 | 35 | impl TryFrom<&[u8]> for DecoderConfigurationRecord { 36 | type Error = AvcError; 37 | 38 | fn try_from(bytes: &[u8]) -> Result { 39 | // FIXME: add checks before accessing buf, otherwise could panic 40 | let mut buf = Cursor::new(bytes); 41 | 42 | if buf.remaining() < 7 { 43 | return Err(AvcError::NotEnoughData("AVC configuration record")); 44 | } 45 | 46 | let version = buf.get_u8(); 47 | if version != 1 { 48 | return Err(AvcError::UnsupportedConfigurationRecordVersion(version)); 49 | } 50 | 51 | let profile_indication = buf.get_u8(); 52 | let profile_compatability = buf.get_u8(); 53 | let level_indication = buf.get_u8(); 54 | let nalu_size = (buf.get_u8() & 0x03) + 1; 55 | 56 | let sps_count = buf.get_u8() & 0x1F; 57 | let mut sps = Vec::new(); 58 | for _ in 0..sps_count { 59 | if buf.remaining() < 2 { 60 | return Err(AvcError::NotEnoughData("DCR SPS length")); 61 | } 62 | let sps_length = buf.get_u16() as usize; 63 | 64 | if buf.remaining() < sps_length { 65 | return Err(AvcError::NotEnoughData("DCR SPS data")); 66 | } 67 | let tmp = buf.chunk()[..sps_length].to_owned(); 68 | buf.advance(sps_length); 69 | 70 | sps.push(nal::Unit::try_from(&*tmp)?); 71 | } 72 | 73 | let pps_count = buf.get_u8(); 74 | let mut pps = Vec::new(); 75 | for _ in 0..pps_count { 76 | if buf.remaining() < 2 { 77 | return Err(AvcError::NotEnoughData("DCR PPS length")); 78 | } 79 | let pps_length = buf.get_u16() as usize; 80 | 81 | if buf.remaining() < pps_length { 82 | return Err(AvcError::NotEnoughData("DCR PPS data")); 83 | } 84 | let tmp = buf.chunk()[..pps_length].to_owned(); 85 | buf.advance(pps_length); 86 | 87 | pps.push(nal::Unit::try_from(&*tmp)?); 88 | } 89 | 90 | Ok(Self { 91 | version, 92 | profile_indication, 93 | profile_compatability, 94 | level_indication, 95 | nalu_size, 96 | sps, 97 | pps, 98 | }) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /crates/javelin-codec/src/avc/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | 4 | #[derive(Debug, Error)] 5 | pub enum AvcError { 6 | #[error("Failed to initialize the AVC decoder")] 7 | DecoderInitializationFailed, 8 | 9 | #[error("AVC coder not initialized")] 10 | NotInitialized, 11 | 12 | #[error("Not enough data: {0}")] 13 | NotEnoughData(&'static str), 14 | 15 | #[error("Unsupported configuration record version {0}")] 16 | UnsupportedConfigurationRecordVersion(u8), 17 | 18 | #[error("Unsupported or unknown NAL unit type {0}")] 19 | UnsupportedNalUnitType(u8), 20 | } 21 | -------------------------------------------------------------------------------- /crates/javelin-codec/src/avc/nal.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryFrom; 2 | use std::fmt; 3 | use std::io::Cursor; 4 | 5 | use bytes::{Buf, BufMut, Bytes, BytesMut}; 6 | 7 | use super::AvcError; 8 | 9 | 10 | #[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord)] 11 | pub enum UnitType { 12 | NonIdrPicture = 1, 13 | DataPartitionA = 2, 14 | DataPartitionB = 3, 15 | DataPartitionC = 4, 16 | IdrPicture = 5, 17 | SupplementaryEnhancementInformation = 6, 18 | SequenceParameterSet = 7, 19 | PictureParameterSet = 8, 20 | AccessUnitDelimiter = 9, 21 | SequenceEnd = 10, 22 | StreamEnd = 11, 23 | FillerData = 12, 24 | SequenceParameterSetExtension = 13, 25 | Prefix = 14, 26 | SequenceParameterSubset = 15, 27 | NotAuxiliaryCoded = 19, 28 | CodedSliceExtension = 20, 29 | } 30 | 31 | impl TryFrom for UnitType { 32 | type Error = AvcError; 33 | 34 | fn try_from(val: u8) -> Result { 35 | Ok(match val { 36 | 1 => UnitType::NonIdrPicture, 37 | 2 => UnitType::DataPartitionA, 38 | 3 => UnitType::DataPartitionB, 39 | 4 => UnitType::DataPartitionC, 40 | 5 => UnitType::IdrPicture, 41 | 6 => UnitType::SupplementaryEnhancementInformation, 42 | 7 => UnitType::SequenceParameterSet, 43 | 8 => UnitType::PictureParameterSet, 44 | 9 => UnitType::AccessUnitDelimiter, 45 | 10 => UnitType::SequenceEnd, 46 | 11 => UnitType::StreamEnd, 47 | 12 => UnitType::FillerData, 48 | 13 => UnitType::SequenceParameterSetExtension, 49 | 14 => UnitType::Prefix, 50 | 15 => UnitType::SequenceParameterSubset, 51 | 19 => UnitType::NotAuxiliaryCoded, 52 | 20 => UnitType::CodedSliceExtension, 53 | _ => return Err(AvcError::UnsupportedNalUnitType(val)), 54 | }) 55 | } 56 | } 57 | 58 | 59 | /// Network Abstraction Layer Unit (aka NALU) of a H.264 bitstream. 60 | #[derive(Clone, PartialEq, Eq)] 61 | pub struct Unit { 62 | pub kind: UnitType, 63 | ref_idc: u8, 64 | data: Bytes, // Raw Byte Sequence Payload (RBSP) 65 | } 66 | 67 | impl Unit { 68 | pub fn payload(&self) -> &[u8] { 69 | &self.data 70 | } 71 | } 72 | 73 | impl TryFrom<&[u8]> for Unit { 74 | type Error = AvcError; 75 | 76 | fn try_from(bytes: &[u8]) -> Result { 77 | let mut buf = Cursor::new(bytes); 78 | 79 | let header = buf.get_u8(); 80 | // FIXME: return error 81 | assert_eq!(header >> 7, 0); 82 | 83 | let ref_idc = (header >> 5) & 0x03; 84 | let kind = UnitType::try_from(header & 0x1F)?; 85 | let mut data = BytesMut::with_capacity(buf.remaining()); 86 | data.put(buf); 87 | let data = data.freeze(); 88 | 89 | Ok(Self { 90 | ref_idc, 91 | kind, 92 | data, 93 | }) 94 | } 95 | } 96 | 97 | impl From<&Unit> for Vec { 98 | fn from(val: &Unit) -> Self { 99 | let mut tmp = Vec::with_capacity(val.data.len() + 1); 100 | 101 | let header = (val.ref_idc << 5) | (val.kind as u8); 102 | tmp.put_u8(header); 103 | tmp.put(val.data.clone()); 104 | tmp 105 | } 106 | } 107 | 108 | impl From for Vec { 109 | fn from(val: Unit) -> Self { 110 | Self::from(&val) 111 | } 112 | } 113 | 114 | impl fmt::Debug for Unit { 115 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 116 | f.debug_struct("Unit").field("kind", &self.kind).finish() 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /crates/javelin-codec/src/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | use crate::aac::AacError; 4 | use crate::avc::AvcError; 5 | use crate::flv::FlvError; 6 | #[cfg(feature = "mpegts")] 7 | use crate::mpegts::TsError; 8 | 9 | 10 | #[derive(Error, Debug)] 11 | pub enum CodecError { 12 | #[error(transparent)] 13 | AvcError(#[from] AvcError), 14 | 15 | #[error(transparent)] 16 | AacError(#[from] AacError), 17 | 18 | #[error(transparent)] 19 | FlvError(#[from] FlvError), 20 | 21 | #[cfg(feature = "mpegts")] 22 | #[error(transparent)] 23 | TsError(#[from] TsError), 24 | } 25 | -------------------------------------------------------------------------------- /crates/javelin-codec/src/flv.rs: -------------------------------------------------------------------------------- 1 | pub mod error; 2 | pub mod tag; 3 | 4 | 5 | pub use self::error::FlvError; 6 | -------------------------------------------------------------------------------- /crates/javelin-codec/src/flv/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | 4 | #[derive(Error, Debug)] 5 | pub enum FlvError { 6 | #[error("Unsupported sampling rate value {0}")] 7 | UnsupportedSamplingRate(u8), 8 | 9 | #[error("Unsupported sample size value {0}")] 10 | UnsupportedSampleSize(u8), 11 | 12 | #[error("Received frame with unknown type {0}")] 13 | UnknownFrameType(u8), 14 | 15 | #[error("Received package with unknown type {0}")] 16 | UnknownPackageType(u8), 17 | 18 | #[error("Video format with id {0} is not supported")] 19 | UnsupportedVideoFormat(u8), 20 | 21 | #[error("Audio format with id {0} is not supported")] 22 | UnsupportedAudioFormat(u8), 23 | 24 | #[error("Not enough data: {0}")] 25 | NotEnoughData(&'static str), 26 | 27 | #[error(transparent)] 28 | IoError(#[from] std::io::Error), 29 | } 30 | -------------------------------------------------------------------------------- /crates/javelin-codec/src/flv/tag.rs: -------------------------------------------------------------------------------- 1 | pub mod audio; 2 | pub mod video; 3 | 4 | 5 | pub use audio::AudioData; 6 | pub use video::VideoData; 7 | -------------------------------------------------------------------------------- /crates/javelin-codec/src/flv/tag/audio.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryFrom; 2 | use std::fmt::{self, Debug}; 3 | use std::io::{Cursor, Read}; 4 | 5 | use bytes::{Buf, Bytes}; 6 | 7 | use crate::flv::error::FlvError; 8 | 9 | 10 | /// Frequency value in Hertz 11 | #[derive(Debug, Copy, Clone, PartialEq, Eq)] 12 | pub struct Frequency(u32); 13 | 14 | 15 | #[non_exhaustive] 16 | #[derive(Debug, Copy, Clone, PartialEq, Eq)] 17 | pub enum AudioFormat { 18 | Aac, 19 | } 20 | 21 | impl TryFrom for AudioFormat { 22 | type Error = FlvError; 23 | 24 | fn try_from(val: u8) -> Result { 25 | // only needs to have support for AAC right now 26 | if val == 10 { 27 | Ok(Self::Aac) 28 | } else { 29 | Err(FlvError::UnsupportedAudioFormat(val)) 30 | } 31 | } 32 | } 33 | 34 | 35 | #[derive(Debug, Copy, Clone, PartialEq, Eq)] 36 | pub enum AacPacketType { 37 | SequenceHeader, 38 | Raw, 39 | None, 40 | } 41 | 42 | impl TryFrom for AacPacketType { 43 | type Error = FlvError; 44 | 45 | fn try_from(val: u8) -> Result { 46 | Ok(match val { 47 | 0 => Self::SequenceHeader, 48 | 1 => Self::Raw, 49 | x => return Err(FlvError::UnknownPackageType(x)), 50 | }) 51 | } 52 | } 53 | 54 | 55 | // Field | Type 56 | // -------------------- | --- 57 | // Audio Format | u4 58 | // Sampling Rate | u4 59 | // Sampling Size | u2 60 | // Stereo Flag | u1 61 | // AAC Packet Type | u8 62 | // Body | [u8] 63 | #[derive(Clone)] 64 | pub struct AudioData { 65 | pub format: AudioFormat, 66 | pub sampling_rate: Frequency, 67 | pub sample_size: u8, 68 | pub stereo: bool, 69 | pub aac_packet_type: AacPacketType, 70 | pub body: Bytes, 71 | } 72 | 73 | impl AudioData { 74 | pub fn is_sequence_header(&self) -> bool { 75 | self.aac_packet_type == AacPacketType::SequenceHeader 76 | } 77 | } 78 | 79 | impl TryFrom<&[u8]> for AudioData { 80 | type Error = FlvError; 81 | 82 | fn try_from(bytes: &[u8]) -> Result { 83 | if bytes.len() < 2 { 84 | return Err(FlvError::NotEnoughData("FLV Audio Tag header")); 85 | } 86 | 87 | let mut buf = Cursor::new(bytes); 88 | 89 | let header = buf.get_u8(); 90 | let format = AudioFormat::try_from(header >> 4)?; 91 | let sampling_rate = try_convert_sampling_rate((header >> 2) & 0x02)?; 92 | let sample_size = try_convert_sample_size((header >> 1) & 0x01)?; 93 | let stereo = (header & 0x01) == 1; 94 | 95 | let aac_packet_type = if format == AudioFormat::Aac { 96 | AacPacketType::try_from(buf.get_u8())? 97 | } else { 98 | AacPacketType::None 99 | }; 100 | 101 | let mut body = Vec::new(); 102 | buf.read_to_end(&mut body)?; 103 | 104 | Ok(Self { 105 | format, 106 | sampling_rate, 107 | sample_size, 108 | stereo, 109 | aac_packet_type, 110 | body: body.into(), 111 | }) 112 | } 113 | } 114 | 115 | impl Debug for AudioData { 116 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 117 | f.debug_struct("AudioData") 118 | .field("format", &self.format) 119 | .field("sampling_rate", &self.sampling_rate) 120 | .field("sample_size", &self.sample_size) 121 | .field("stereo", &self.stereo) 122 | .field("aac_packet_type", &self.aac_packet_type) 123 | .finish() 124 | } 125 | } 126 | 127 | 128 | fn try_convert_sampling_rate(val: u8) -> Result { 129 | Ok(match val { 130 | 0 => Frequency(5500), 131 | 1 => Frequency(11000), 132 | 2 => Frequency(22000), 133 | 3 => Frequency(44000), 134 | x => return Err(FlvError::UnsupportedSamplingRate(x)), 135 | }) 136 | } 137 | 138 | fn try_convert_sample_size(val: u8) -> Result { 139 | Ok(match val { 140 | 0 => 8, 141 | 1 => 16, 142 | x => return Err(FlvError::UnsupportedSampleSize(x)), 143 | }) 144 | } 145 | -------------------------------------------------------------------------------- /crates/javelin-codec/src/flv/tag/video.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryFrom; 2 | use std::fmt::{self, Debug}; 3 | use std::io::{Cursor, Read}; 4 | 5 | use bytes::{Buf, Bytes}; 6 | 7 | use crate::flv::error::FlvError; 8 | 9 | 10 | #[derive(Debug, Clone, PartialEq, Eq)] 11 | pub enum FrameType { 12 | KeyFrame, 13 | InterFrame, 14 | DisposableInterFrame, 15 | GeneratedKeyframe, 16 | VideoInfoFrame, 17 | } 18 | 19 | impl TryFrom for FrameType { 20 | type Error = FlvError; 21 | 22 | fn try_from(val: u8) -> Result { 23 | Ok(match val { 24 | 1 => Self::KeyFrame, 25 | 2 => Self::InterFrame, 26 | 3 => Self::DisposableInterFrame, 27 | 4 => Self::GeneratedKeyframe, 28 | 5 => Self::VideoInfoFrame, 29 | x => return Err(FlvError::UnknownFrameType(x)), 30 | }) 31 | } 32 | } 33 | 34 | 35 | #[derive(Debug, Clone, PartialEq, Eq)] 36 | pub enum AvcPacketType { 37 | SequenceHeader, 38 | NalUnit, 39 | EndOfSequence, 40 | None, 41 | } 42 | 43 | impl TryFrom for AvcPacketType { 44 | type Error = FlvError; 45 | 46 | fn try_from(val: u8) -> Result { 47 | Ok(match val { 48 | 0 => Self::SequenceHeader, 49 | 1 => Self::NalUnit, 50 | 2 => Self::EndOfSequence, 51 | x => return Err(FlvError::UnknownPackageType(x)), 52 | }) 53 | } 54 | } 55 | 56 | 57 | // Field | Type 58 | // -------------------- | --- 59 | // Frame Type | u4 60 | // Codec ID | u4 61 | // AVC Packet Type | u8 62 | // Composition Time | i24 63 | // Body | [u8] 64 | #[derive(Clone)] 65 | pub struct VideoData { 66 | pub frame_type: FrameType, 67 | pub packet_type: AvcPacketType, 68 | pub composition_time: i32, 69 | pub body: Bytes, 70 | } 71 | 72 | impl VideoData { 73 | pub fn is_sequence_header(&self) -> bool { 74 | self.packet_type == AvcPacketType::SequenceHeader 75 | } 76 | 77 | pub fn is_keyframe(&self) -> bool { 78 | self.frame_type == FrameType::KeyFrame 79 | } 80 | } 81 | 82 | impl Debug for VideoData { 83 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 84 | f.debug_struct("Video") 85 | .field("frame_type", &self.frame_type) 86 | .field("packet_type", &self.packet_type) 87 | .field("composition_time", &self.composition_time) 88 | .finish() 89 | } 90 | } 91 | 92 | impl TryFrom<&[u8]> for VideoData { 93 | type Error = FlvError; 94 | 95 | fn try_from(bytes: &[u8]) -> Result { 96 | if bytes.len() < 5 { 97 | return Err(FlvError::NotEnoughData("FLV Video Tag header")); 98 | } 99 | 100 | let mut buf = Cursor::new(bytes); 101 | 102 | let header_a = buf.get_u8(); 103 | 104 | // Only support AVC payloads 105 | let codec_id = header_a & 0x0F; 106 | if codec_id != 7 { 107 | return Err(FlvError::UnsupportedVideoFormat(codec_id)); 108 | } 109 | 110 | let frame_type = FrameType::try_from(header_a >> 4)?; 111 | 112 | let header_b = buf.get_u32(); 113 | 114 | let packet_type = AvcPacketType::try_from((header_b >> 24) as u8)?; 115 | 116 | let composition_time = (header_b & 0x00_FF_FF_FF) as i32; 117 | 118 | let mut remaining = Vec::new(); 119 | buf.read_to_end(&mut remaining)?; 120 | 121 | Ok(Self { 122 | frame_type, 123 | packet_type, 124 | composition_time, 125 | body: remaining.into(), 126 | }) 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /crates/javelin-codec/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(unused_must_use)] 2 | #![warn(rust_2018_idioms)] 3 | 4 | pub mod aac; 5 | pub mod avc; 6 | pub mod error; 7 | pub mod flv; 8 | #[cfg(feature = "mpegts")] 9 | pub mod mpegts; 10 | 11 | pub use self::error::CodecError; 12 | 13 | 14 | /// Decode bytes into a specific format. 15 | pub trait ReadFormat { 16 | type Context; 17 | type Error; 18 | 19 | fn read_format(&self, input: &[u8], ctx: &Self::Context) -> Result; 20 | } 21 | 22 | /// Encode bytes from a specific format. 23 | pub trait WriteFormat { 24 | type Context; 25 | type Error; 26 | 27 | fn write_format(&self, input: I, ctx: &Self::Context) -> Result, Self::Error>; 28 | } 29 | 30 | pub trait FormatReader 31 | where 32 | F: ReadFormat, 33 | { 34 | type Output; 35 | type Error; 36 | 37 | fn read_format(&mut self, format: F, input: &[u8]) 38 | -> Result, Self::Error>; 39 | } 40 | 41 | pub trait FormatWriter 42 | where 43 | F: WriteFormat, 44 | { 45 | type Input; 46 | type Error; 47 | 48 | fn write_format(&mut self, format: F, input: Self::Input) -> Result, Self::Error>; 49 | } 50 | -------------------------------------------------------------------------------- /crates/javelin-codec/src/mpegts.rs: -------------------------------------------------------------------------------- 1 | mod error; 2 | pub mod transport_stream; 3 | 4 | pub use self::error::TsError; 5 | pub use self::transport_stream::TransportStream; 6 | -------------------------------------------------------------------------------- /crates/javelin-codec/src/mpegts/error.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | use thiserror::Error; 4 | 5 | #[derive(Error, Debug)] 6 | pub enum TsError { 7 | #[error("Failed to create TS file")] 8 | FileCreationFailed(#[from] io::Error), 9 | 10 | #[error("Failed to write TS file")] 11 | WriteError, 12 | 13 | #[error("Packet ID {0} is not valid")] 14 | InvalidPacketId(u16), 15 | 16 | #[error("Invalid timestamp {0}")] 17 | InvalidTimestamp(u64), 18 | 19 | #[error("Packet payload exceeded packet limit")] 20 | PayloadTooBig, 21 | 22 | #[error("Clock reference value of {0} exceeds maximum")] 23 | ClockValueOutOfRange(u64), 24 | } 25 | -------------------------------------------------------------------------------- /crates/javelin-codec/src/mpegts/transport_stream.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | use std::io::Cursor; 3 | use std::path::Path; 4 | 5 | use bytes::Buf; 6 | use mpeg2ts::pes::PesHeader; 7 | use mpeg2ts::time::{ClockReference, Timestamp}; 8 | use mpeg2ts::ts::{self, ContinuityCounter, Pid, TsHeader, TsPacket, TsPayload}; 9 | 10 | use super::TsError; 11 | 12 | 13 | const PMT_PID: u16 = 256; 14 | const VIDEO_ES_PID: u16 = 257; 15 | const AUDIO_ES_PID: u16 = 258; 16 | const PES_VIDEO_STREAM_ID: u8 = 224; 17 | const PES_AUDIO_STREAM_ID: u8 = 192; 18 | 19 | 20 | pub struct TransportStream { 21 | video_continuity_counter: ContinuityCounter, 22 | audio_continuity_counter: ContinuityCounter, 23 | packets: Vec, 24 | } 25 | 26 | impl TransportStream { 27 | pub fn new() -> Self { 28 | Self::default() 29 | } 30 | 31 | pub fn write_to_file

(&mut self, filename: P) -> Result<(), TsError> 32 | where 33 | P: AsRef, 34 | { 35 | use mpeg2ts::ts::{TsPacketWriter, WriteTsPacket}; 36 | 37 | let file = File::create(filename)?; 38 | let packets: Vec<_> = self.packets.drain(..).collect(); 39 | let mut writer = TsPacketWriter::new(file); 40 | 41 | writer 42 | .write_ts_packet(&default_pat_packet()) 43 | .map_err(|_| TsError::WriteError)?; 44 | 45 | writer 46 | .write_ts_packet(&default_pmt_packet()) 47 | .map_err(|_| TsError::WriteError)?; 48 | 49 | for packet in &packets { 50 | writer 51 | .write_ts_packet(packet) 52 | .map_err(|_| TsError::WriteError)?; 53 | } 54 | 55 | Ok(()) 56 | } 57 | 58 | pub fn push_video( 59 | &mut self, 60 | timestamp: u64, 61 | composition_time: u64, 62 | keyframe: bool, 63 | video: Vec, 64 | ) -> Result<(), TsError> { 65 | use mpeg2ts::es::StreamId; 66 | use mpeg2ts::ts::{payload, AdaptationField}; 67 | 68 | let mut header = default_ts_header(VIDEO_ES_PID)?; 69 | header.continuity_counter = self.video_continuity_counter; 70 | 71 | let mut buf = Cursor::new(video); 72 | let packet = { 73 | let data = { 74 | let pes_data = if buf.remaining() < 153 { 75 | buf.chunk() 76 | } else { 77 | &buf.chunk()[..153] 78 | }; 79 | make_raw_payload(pes_data)? 80 | }; 81 | buf.advance(data.len()); 82 | 83 | let pcr = make_clock_reference(timestamp * 90)?; 84 | 85 | let adaptation_field = if keyframe { 86 | Some(AdaptationField { 87 | discontinuity_indicator: false, 88 | random_access_indicator: true, 89 | es_priority_indicator: false, 90 | pcr: Some(pcr), 91 | opcr: None, 92 | splice_countdown: None, 93 | transport_private_data: Vec::new(), 94 | extension: None, 95 | }) 96 | } else { 97 | None 98 | }; 99 | 100 | let pts = make_timestamp((timestamp + composition_time) * 90)?; 101 | let dts = make_timestamp(timestamp * 90)?; 102 | 103 | TsPacket { 104 | header: header.clone(), 105 | adaptation_field, 106 | payload: Some(TsPayload::Pes(payload::Pes { 107 | header: PesHeader { 108 | stream_id: StreamId::new(PES_VIDEO_STREAM_ID), 109 | priority: false, 110 | data_alignment_indicator: false, 111 | copyright: false, 112 | original_or_copy: false, 113 | pts: Some(pts), 114 | dts: Some(dts), 115 | escr: None, 116 | }, 117 | pes_packet_len: 0, 118 | data, 119 | })), 120 | } 121 | }; 122 | 123 | self.packets.push(packet); 124 | header.continuity_counter.increment(); 125 | 126 | while buf.has_remaining() { 127 | let raw_payload = { 128 | let pes_data = if buf.remaining() < payload::Bytes::MAX_SIZE { 129 | buf.chunk() 130 | } else { 131 | &buf.chunk()[..payload::Bytes::MAX_SIZE] 132 | }; 133 | make_raw_payload(pes_data)? 134 | }; 135 | buf.advance(raw_payload.len()); 136 | 137 | let packet = TsPacket { 138 | header: header.clone(), 139 | adaptation_field: None, 140 | payload: Some(TsPayload::Raw(raw_payload)), 141 | }; 142 | 143 | self.packets.push(packet); 144 | header.continuity_counter.increment(); 145 | } 146 | 147 | self.video_continuity_counter = header.continuity_counter; 148 | 149 | Ok(()) 150 | } 151 | 152 | pub fn push_audio(&mut self, timestamp: u64, audio: Vec) -> Result<(), TsError> { 153 | use mpeg2ts::es::StreamId; 154 | use mpeg2ts::ts::payload; 155 | 156 | let mut buf = Cursor::new(audio); 157 | let data = { 158 | let pes_data = if buf.remaining() < 153 { 159 | buf.chunk() 160 | } else { 161 | &buf.chunk()[..153] 162 | }; 163 | make_raw_payload(pes_data)? 164 | }; 165 | buf.advance(data.len()); 166 | 167 | let mut header = default_ts_header(AUDIO_ES_PID)?; 168 | header.continuity_counter = self.audio_continuity_counter; 169 | 170 | let packet = TsPacket { 171 | header: header.clone(), 172 | adaptation_field: None, 173 | payload: Some(TsPayload::Pes(payload::Pes { 174 | header: PesHeader { 175 | stream_id: StreamId::new(PES_AUDIO_STREAM_ID), 176 | priority: false, 177 | data_alignment_indicator: false, 178 | copyright: false, 179 | original_or_copy: false, 180 | pts: Some(make_timestamp(timestamp * 90)?), 181 | dts: None, 182 | escr: None, 183 | }, 184 | pes_packet_len: 0, 185 | data, 186 | })), 187 | }; 188 | 189 | self.packets.push(packet); 190 | header.continuity_counter.increment(); 191 | 192 | while buf.has_remaining() { 193 | let raw_payload = { 194 | let pes_data = if buf.remaining() < payload::Bytes::MAX_SIZE { 195 | buf.chunk() 196 | } else { 197 | &buf.chunk()[..payload::Bytes::MAX_SIZE] 198 | }; 199 | make_raw_payload(pes_data)? 200 | }; 201 | buf.advance(raw_payload.len()); 202 | 203 | let packet = TsPacket { 204 | header: header.clone(), 205 | adaptation_field: None, 206 | payload: Some(TsPayload::Raw(raw_payload)), 207 | }; 208 | 209 | self.packets.push(packet); 210 | header.continuity_counter.increment(); 211 | } 212 | 213 | self.audio_continuity_counter = header.continuity_counter; 214 | 215 | Ok(()) 216 | } 217 | } 218 | 219 | impl Default for TransportStream { 220 | fn default() -> Self { 221 | Self { 222 | video_continuity_counter: ContinuityCounter::new(), 223 | audio_continuity_counter: ContinuityCounter::new(), 224 | packets: Vec::new(), 225 | } 226 | } 227 | } 228 | 229 | 230 | fn make_raw_payload(pes_data: &[u8]) -> Result { 231 | ts::payload::Bytes::new(pes_data).map_err(|_| TsError::PayloadTooBig) 232 | } 233 | 234 | 235 | fn make_timestamp(ts: u64) -> Result { 236 | Timestamp::new(ts).map_err(|_| TsError::InvalidTimestamp(ts)) 237 | } 238 | 239 | 240 | fn make_clock_reference(ts: u64) -> Result { 241 | ClockReference::new(ts).map_err(|_| TsError::ClockValueOutOfRange(ts)) 242 | } 243 | 244 | 245 | fn default_ts_header(pid: u16) -> Result { 246 | use mpeg2ts::ts::TransportScramblingControl; 247 | 248 | Ok(TsHeader { 249 | transport_error_indicator: false, 250 | transport_priority: false, 251 | pid: Pid::new(pid).map_err(|_| TsError::InvalidPacketId(pid))?, 252 | transport_scrambling_control: TransportScramblingControl::NotScrambled, 253 | continuity_counter: ContinuityCounter::new(), 254 | }) 255 | } 256 | 257 | 258 | fn default_pat_packet() -> TsPacket { 259 | use mpeg2ts::ts::payload::Pat; 260 | use mpeg2ts::ts::{ProgramAssociation, VersionNumber}; 261 | 262 | TsPacket { 263 | header: default_ts_header(0).unwrap(), 264 | adaptation_field: None, 265 | payload: Some(TsPayload::Pat(Pat { 266 | transport_stream_id: 1, 267 | version_number: VersionNumber::default(), 268 | table: vec![ProgramAssociation { 269 | program_num: 1, 270 | program_map_pid: Pid::new(PMT_PID).unwrap(), 271 | }], 272 | })), 273 | } 274 | } 275 | 276 | 277 | fn default_pmt_packet() -> TsPacket { 278 | use mpeg2ts::es::StreamType; 279 | use mpeg2ts::ts::payload::Pmt; 280 | use mpeg2ts::ts::{EsInfo, VersionNumber}; 281 | 282 | TsPacket { 283 | header: default_ts_header(PMT_PID).unwrap(), 284 | adaptation_field: None, 285 | payload: Some(TsPayload::Pmt(Pmt { 286 | program_num: 1, 287 | pcr_pid: Some(Pid::new(VIDEO_ES_PID).unwrap()), 288 | version_number: VersionNumber::default(), 289 | es_info: vec![ 290 | EsInfo { 291 | stream_type: StreamType::H264, 292 | elementary_pid: Pid::new(VIDEO_ES_PID).unwrap(), 293 | descriptors: vec![], 294 | }, 295 | EsInfo { 296 | stream_type: StreamType::AdtsAac, 297 | elementary_pid: Pid::new(AUDIO_ES_PID).unwrap(), 298 | descriptors: vec![], 299 | }, 300 | ], 301 | program_info: vec![], 302 | })), 303 | } 304 | } 305 | -------------------------------------------------------------------------------- /crates/javelin-core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "javelin-core" 3 | description = "Simple streaming server (shared core components)" 4 | version.workspace = true 5 | authors.workspace = true 6 | edition.workspace = true 7 | rust-version.workspace = true 8 | license-file.workspace = true 9 | readme.workspace = true 10 | repository.workspace = true 11 | categories.workspace = true 12 | keywords.workspace = true 13 | publish = false 14 | 15 | 16 | [dependencies] 17 | anyhow.workspace = true 18 | serde.workspace = true 19 | javelin-types.workspace = true 20 | tracing.workspace = true 21 | 22 | [dependencies.config] 23 | version = "0.14" 24 | default-features = false 25 | features = ["toml"] 26 | 27 | [dependencies.tokio] 28 | workspace = true 29 | default-features = false 30 | features = ["rt", "sync"] 31 | -------------------------------------------------------------------------------- /crates/javelin-core/src/config.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | use anyhow::Result; 4 | use serde::de::Deserialize; 5 | 6 | 7 | pub struct Config(config::Config); 8 | 9 | impl Config { 10 | pub fn try_from_path

(config_dir: P) -> Result 11 | where 12 | P: AsRef, 13 | { 14 | let path = config_dir.as_ref().join("javelin.toml"); 15 | 16 | let config = config::Config::builder() 17 | .add_source(config::File::from(path)) 18 | .build()?; 19 | 20 | Ok(Self(config)) 21 | } 22 | 23 | pub fn get<'de, V>(&self, key: &str) -> Result 24 | where 25 | V: Deserialize<'de>, 26 | { 27 | let value = self.0.get(key)?; 28 | Ok(value) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /crates/javelin-core/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod config; 2 | pub mod session; 3 | 4 | pub use config::Config; 5 | -------------------------------------------------------------------------------- /crates/javelin-core/src/session.rs: -------------------------------------------------------------------------------- 1 | mod instance; 2 | pub mod manager; 3 | mod transport; 4 | 5 | 6 | type Event = &'static str; 7 | type AppName = String; 8 | type StreamKey = String; 9 | 10 | pub use self::manager::Manager; 11 | pub use self::transport::{ 12 | trigger_channel, Handle, ManagerHandle, ManagerMessage, Message, Watcher, 13 | }; 14 | -------------------------------------------------------------------------------- /crates/javelin-core/src/session/instance.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use javelin_types::{packet, Packet}; 3 | use tracing::{error, info}; 4 | 5 | use super::transport::{IncomingBroadcast, Message, OutgoingBroadcast}; 6 | 7 | 8 | pub struct Session { 9 | incoming: IncomingBroadcast, 10 | outgoing: OutgoingBroadcast, 11 | metadata: Option, 12 | video_seq_header: Option, 13 | audio_seq_header: Option, 14 | closing: bool, 15 | } 16 | 17 | impl Session { 18 | #[allow(clippy::new_without_default)] 19 | pub fn new(incoming: IncomingBroadcast, outgoing: OutgoingBroadcast) -> Self { 20 | Self { 21 | incoming, 22 | outgoing, 23 | metadata: None, 24 | video_seq_header: None, 25 | audio_seq_header: None, 26 | closing: false, 27 | } 28 | } 29 | 30 | pub async fn run(mut self) { 31 | while !self.closing { 32 | if let Some(message) = self.incoming.recv().await { 33 | self.handle_message(message); 34 | } 35 | } 36 | } 37 | 38 | fn handle_message(&mut self, message: Message) { 39 | match message { 40 | Message::Packet(packet) => { 41 | self.set_cache(&packet) 42 | .expect("Failed to set session cache"); 43 | self.broadcast_packet(packet); 44 | } 45 | Message::GetInitData(responder) => { 46 | let response = ( 47 | self.metadata.clone(), 48 | self.video_seq_header.clone(), 49 | self.audio_seq_header.clone(), 50 | ); 51 | if responder.send(response).is_err() { 52 | error!("Failed to send init data"); 53 | } 54 | } 55 | Message::Disconnect => { 56 | self.closing = true; 57 | } 58 | } 59 | } 60 | 61 | fn broadcast_packet(&self, packet: Packet) { 62 | if self.outgoing.receiver_count() != 0 && self.outgoing.send(packet).is_err() { 63 | error!("Failed to broadcast packet"); 64 | } 65 | } 66 | 67 | fn set_cache(&mut self, packet: &Packet) -> Result<()> { 68 | match packet.content_type { 69 | packet::METADATA if self.metadata.is_none() => { 70 | self.metadata = Some(packet.clone()); 71 | } 72 | packet::FLV_VIDEO_H264 if self.video_seq_header.is_none() => { 73 | self.video_seq_header = Some(packet.clone()); 74 | } 75 | packet::FLV_AUDIO_AAC if self.audio_seq_header.is_none() => { 76 | self.audio_seq_header = Some(packet.clone()); 77 | } 78 | _ => (), 79 | } 80 | 81 | Ok(()) 82 | } 83 | } 84 | 85 | impl Drop for Session { 86 | fn drop(&mut self) { 87 | info!("Closing session"); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /crates/javelin-core/src/session/manager.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::sync::Arc; 3 | 4 | use anyhow::{bail, Result}; 5 | use javelin_types::models::UserRepository; 6 | use tokio::sync::{broadcast, mpsc, RwLock}; 7 | use tracing::{debug, error}; 8 | 9 | use super::instance::Session; 10 | use super::transport::{ 11 | Handle, ManagerHandle, ManagerMessage, ManagerReceiver, OutgoingBroadcast, Trigger, 12 | }; 13 | use super::{AppName, Event}; 14 | 15 | 16 | pub struct Manager 17 | where 18 | D: UserRepository + Send + Sync + 'static, 19 | { 20 | handle: ManagerHandle, 21 | incoming: ManagerReceiver, 22 | user_repo: D, 23 | sessions: Arc>>, 24 | triggers: Arc>>>, 25 | } 26 | 27 | impl Manager 28 | where 29 | D: UserRepository + Send + Sync + 'static, 30 | { 31 | pub fn new(user_repo: D) -> Self { 32 | let (handle, incoming) = mpsc::unbounded_channel(); 33 | let sessions = Arc::new(RwLock::new(HashMap::new())); 34 | let triggers = Arc::new(RwLock::new(HashMap::new())); 35 | 36 | Self { 37 | handle, 38 | incoming, 39 | sessions, 40 | triggers, 41 | user_repo, 42 | } 43 | } 44 | 45 | pub fn handle(&self) -> ManagerHandle { 46 | self.handle.clone() 47 | } 48 | 49 | async fn process_message(&mut self, message: ManagerMessage) -> Result<()> { 50 | match message { 51 | ManagerMessage::CreateSession((name, key, responder)) => { 52 | self.authenticate(&name, &key).await?; 53 | let (handle, incoming) = mpsc::unbounded_channel(); 54 | let (outgoing, _watcher) = broadcast::channel(64); 55 | let mut sessions = self.sessions.write().await; 56 | sessions.insert(name.clone(), (handle.clone(), outgoing.clone())); 57 | 58 | let triggers = self.triggers.read().await; 59 | if let Some(event_triggers) = triggers.get("create_session") { 60 | for trigger in event_triggers { 61 | trigger.send((name.clone(), outgoing.subscribe()))?; 62 | } 63 | } 64 | 65 | tokio::spawn(async move { 66 | Session::new(incoming, outgoing).run().await; 67 | }); 68 | 69 | if responder.send(handle).is_err() { 70 | bail!("Failed to send response"); 71 | } 72 | } 73 | ManagerMessage::JoinSession((name, responder)) => { 74 | let sessions = self.sessions.read().await; 75 | if let Some((handle, watcher)) = sessions.get(&name) { 76 | if responder 77 | .send((handle.clone(), watcher.subscribe())) 78 | .is_err() 79 | { 80 | bail!("Failed to send response"); 81 | } 82 | } 83 | } 84 | ManagerMessage::ReleaseSession(name) => { 85 | let mut sessions = self.sessions.write().await; 86 | sessions.remove(&name); 87 | } 88 | ManagerMessage::RegisterTrigger(event, trigger) => { 89 | debug!("Registering trigger for {}", event); 90 | let mut triggers = self.triggers.write().await; 91 | triggers.entry(event).or_insert_with(Vec::new).push(trigger); 92 | } 93 | } 94 | 95 | Ok(()) 96 | } 97 | 98 | pub async fn run(mut self) { 99 | while let Some(message) = self.incoming.recv().await { 100 | if let Err(err) = self.process_message(message).await { 101 | error!("{}", err); 102 | }; 103 | } 104 | } 105 | 106 | async fn authenticate(&self, app_name: &str, stream_key: &str) -> Result<()> { 107 | if stream_key.is_empty() { 108 | bail!("Stream key can not be empty"); 109 | } 110 | 111 | if !self.user_repo.user_has_key(app_name, stream_key).await? { 112 | bail!("Stream key {} not permitted for {}", stream_key, app_name); 113 | } 114 | 115 | Ok(()) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /crates/javelin-core/src/session/transport.rs: -------------------------------------------------------------------------------- 1 | use javelin_types::Packet; 2 | use tokio::sync::{broadcast, mpsc, oneshot}; 3 | 4 | use super::{AppName, Event, StreamKey}; 5 | 6 | 7 | pub type Responder

= oneshot::Sender

; 8 | 9 | // session manager 10 | pub enum ManagerMessage { 11 | CreateSession((AppName, StreamKey, Responder)), 12 | ReleaseSession(AppName), 13 | JoinSession((AppName, Responder<(Handle, Watcher)>)), 14 | RegisterTrigger(Event, Trigger), 15 | } 16 | 17 | pub type ManagerHandle = mpsc::UnboundedSender; 18 | pub(super) type ManagerReceiver = mpsc::UnboundedReceiver; 19 | 20 | 21 | pub type Trigger = mpsc::UnboundedSender<(String, Watcher)>; 22 | pub(super) type TriggerHandle = mpsc::UnboundedReceiver<(String, Watcher)>; 23 | 24 | pub fn trigger_channel() -> (Trigger, TriggerHandle) { 25 | mpsc::unbounded_channel() 26 | } 27 | 28 | 29 | // session instance 30 | pub enum Message { 31 | Packet(Packet), 32 | GetInitData(Responder<(Option, Option, Option)>), 33 | Disconnect, 34 | } 35 | 36 | pub type Handle = mpsc::UnboundedSender; 37 | pub(super) type IncomingBroadcast = mpsc::UnboundedReceiver; 38 | pub(super) type OutgoingBroadcast = broadcast::Sender; 39 | pub type Watcher = broadcast::Receiver; 40 | -------------------------------------------------------------------------------- /crates/javelin-hls/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "javelin-hls" 3 | description = "Simple streaming server (HLS)" 4 | version.workspace = true 5 | authors.workspace = true 6 | edition.workspace = true 7 | rust-version.workspace = true 8 | license-file.workspace = true 9 | readme.workspace = true 10 | repository.workspace = true 11 | categories.workspace = true 12 | keywords.workspace = true 13 | publish = false 14 | 15 | 16 | [dependencies] 17 | anyhow.workspace = true 18 | axum.workspace = true 19 | chrono.workspace = true 20 | futures.workspace = true 21 | javelin-types.workspace = true 22 | javelin-core.workspace = true 23 | m3u8-rs = "6.0" 24 | serde.workspace = true 25 | tempfile = "3.12" 26 | tracing.workspace = true 27 | 28 | [dependencies.tower-http] 29 | version = "0.6" 30 | features = ["fs"] 31 | 32 | # TODO: replace this crate with custom impl 33 | [dependencies.futures-delay-queue] 34 | version = "0.6" 35 | default-features = false 36 | features = ["tokio"] 37 | 38 | [dependencies.futures-intrusive] 39 | version = "0.5" 40 | 41 | [dependencies.javelin-codec] 42 | workspace = true 43 | features = ["mpegts"] 44 | 45 | [dependencies.tokio] 46 | workspace = true 47 | features = ["rt", "sync"] 48 | -------------------------------------------------------------------------------- /crates/javelin-hls/src/config.rs: -------------------------------------------------------------------------------- 1 | use std::net::SocketAddr; 2 | use std::path::PathBuf; 3 | 4 | use serde::Deserialize; 5 | 6 | 7 | #[derive(Debug, Clone, Deserialize)] 8 | pub struct Config { 9 | #[serde(default = "default_root_dir")] 10 | pub root_dir: PathBuf, 11 | 12 | #[serde(default = "default_enabled")] 13 | pub enabled: bool, 14 | 15 | #[serde(default)] 16 | pub web: WebConfig, 17 | } 18 | 19 | impl Default for Config { 20 | fn default() -> Self { 21 | Self { 22 | root_dir: default_root_dir(), 23 | enabled: default_enabled(), 24 | web: WebConfig::default(), 25 | } 26 | } 27 | } 28 | 29 | 30 | #[derive(Debug, Clone, Deserialize)] 31 | pub struct WebConfig { 32 | #[serde(default = "default_web_addr")] 33 | pub addr: SocketAddr, 34 | 35 | #[serde(default = "default_enabled")] 36 | pub enabled: bool, 37 | } 38 | 39 | impl Default for WebConfig { 40 | fn default() -> Self { 41 | Self { 42 | addr: default_web_addr(), 43 | enabled: default_enabled(), 44 | } 45 | } 46 | } 47 | 48 | 49 | fn default_root_dir() -> PathBuf { 50 | PathBuf::from("./data/hls") 51 | } 52 | 53 | fn default_web_addr() -> SocketAddr { 54 | SocketAddr::from(([0, 0, 0, 0], 8080)) 55 | } 56 | 57 | fn default_enabled() -> bool { 58 | true 59 | } 60 | -------------------------------------------------------------------------------- /crates/javelin-hls/src/file_cleaner.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::path::PathBuf; 3 | use std::time::Duration; 4 | 5 | use futures_delay_queue::delay_queue; 6 | use futures_intrusive::buffer::GrowingHeapBuf; 7 | use tokio::sync::mpsc; 8 | use tracing::{debug, error}; 9 | 10 | 11 | type Batch = Vec; 12 | type Message = (Duration, Batch); 13 | pub type Sender = mpsc::UnboundedSender; 14 | type Receiver = mpsc::UnboundedReceiver; 15 | type DelayQueue = futures_delay_queue::DelayQueue>; 16 | type DelayQueueReceiver = futures_delay_queue::Receiver; 17 | 18 | 19 | pub struct FileCleaner { 20 | queue: DelayQueue, 21 | queue_rx: DelayQueueReceiver, 22 | sender: Sender, 23 | receiver: Receiver, 24 | } 25 | 26 | impl FileCleaner { 27 | #[allow(clippy::new_without_default)] 28 | pub fn new() -> Self { 29 | let (sender, receiver) = mpsc::unbounded_channel(); 30 | 31 | let (queue, queue_rx) = delay_queue(); 32 | 33 | Self { 34 | queue, 35 | queue_rx, 36 | sender, 37 | receiver, 38 | } 39 | } 40 | 41 | pub async fn run(mut self) { 42 | while let Some((duration, files)) = self.receiver.recv().await { 43 | let timestamp = (duration / 100) * 150; 44 | debug!( 45 | "{} files queued for cleanup at {:?}", 46 | files.len(), 47 | timestamp 48 | ); 49 | self.queue.insert(files, timestamp); 50 | 51 | if let Some(expired) = self.queue_rx.receive().await { 52 | remove_files(&expired) 53 | } 54 | } 55 | } 56 | 57 | pub fn sender(&self) -> Sender { 58 | self.sender.clone() 59 | } 60 | } 61 | 62 | 63 | fn remove_files(paths: &[PathBuf]) { 64 | debug!("Cleaning up {} files", paths.len()); 65 | 66 | for path in paths { 67 | remove_file(path); 68 | } 69 | } 70 | 71 | fn remove_file(path: &PathBuf) { 72 | if let Err(why) = fs::remove_file(path) { 73 | error!("Failed to remove file '{}': {}", path.display(), why); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /crates/javelin-hls/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod config; 2 | mod file_cleaner; 3 | mod m3u8; 4 | pub mod service; 5 | mod writer; 6 | 7 | 8 | pub use self::service::Service; 9 | -------------------------------------------------------------------------------- /crates/javelin-hls/src/m3u8.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | #[cfg(unix)] 3 | use std::os::unix::fs::PermissionsExt; 4 | use std::path::PathBuf; 5 | use std::time::Duration; 6 | 7 | use anyhow::Result; 8 | use m3u8_rs::{MediaPlaylist, MediaSegment}; 9 | use tempfile::NamedTempFile; 10 | use tracing::error; 11 | 12 | use crate::file_cleaner; 13 | 14 | 15 | pub struct Playlist { 16 | file_path: PathBuf, 17 | current_duration: u64, 18 | cleanup_started: bool, 19 | playlist: MediaPlaylist, 20 | file_cleaner: file_cleaner::Sender, 21 | } 22 | 23 | impl Playlist { 24 | const DEFAULT_TARGET_DURATION: u64 = 6; 25 | const PLAYLIST_CACHE_DURATION_MS: u64 = 30000; 26 | 27 | pub fn new

(path: P, file_cleaner: file_cleaner::Sender) -> Self 28 | where 29 | P: Into, 30 | { 31 | let playlist = MediaPlaylist { 32 | version: Some(3), 33 | target_duration: Self::DEFAULT_TARGET_DURATION, 34 | media_sequence: 0, 35 | ..Default::default() 36 | }; 37 | 38 | Self { 39 | file_path: path.into(), 40 | current_duration: 0, 41 | cleanup_started: false, 42 | playlist, 43 | file_cleaner, 44 | } 45 | } 46 | 47 | pub fn set_target_duration(&mut self, duration: u64) { 48 | self.playlist.target_duration = (duration as f64 / 1000.0) as u64; 49 | } 50 | 51 | fn schedule_for_deletion(&mut self, amount: usize, delete_after: u64) { 52 | let segments_to_delete: Vec<_> = self.playlist.segments.drain(..amount).collect(); 53 | let paths: Vec<_> = segments_to_delete 54 | .iter() 55 | .map(|seg| { 56 | self.current_duration -= (seg.duration * 1000.0) as u64; 57 | self.file_path.parent().unwrap().join(&seg.uri) 58 | }) 59 | .collect(); 60 | 61 | self.playlist.media_sequence += paths.len() as u64; 62 | self.file_cleaner 63 | .send((Duration::from_millis(delete_after), paths)) 64 | .unwrap(); 65 | } 66 | 67 | pub fn add_media_segment(&mut self, uri: S, duration: u64) 68 | where 69 | S: Into, 70 | { 71 | let mut segment = MediaSegment::empty(); 72 | segment.duration = (duration as f64 / 1000.0) as f32; 73 | segment.title = Some("".into()); // adding empty title here, because implementation is broken 74 | segment.uri = uri.into(); 75 | 76 | 77 | if self.cleanup_started { 78 | self.schedule_for_deletion(1, Self::PLAYLIST_CACHE_DURATION_MS); 79 | } else if self.current_duration >= Self::PLAYLIST_CACHE_DURATION_MS { 80 | self.cleanup_started = true; 81 | } 82 | 83 | self.current_duration += duration; 84 | self.playlist.segments.push(segment); 85 | 86 | if let Err(why) = self.atomic_update() { 87 | error!("Failed to update playlist: {:?}", why); 88 | } 89 | } 90 | 91 | fn atomic_update(&mut self) -> Result<()> { 92 | let mut tmp_file = tempfile::Builder::new() 93 | .prefix(".playlist.m3u") 94 | .suffix(".tmp") 95 | .tempfile_in(self.hls_root())?; 96 | 97 | self.write_temporary_file(&mut tmp_file)?; 98 | fs::rename(tmp_file.path(), &self.file_path)?; 99 | 100 | Ok(()) 101 | } 102 | 103 | fn hls_root(&self) -> PathBuf { 104 | self.file_path 105 | .parent() 106 | .expect("No parent directory for playlist") 107 | .into() 108 | } 109 | 110 | fn write_temporary_file(&mut self, tmp_file: &mut NamedTempFile) -> Result<()> { 111 | self.playlist.write_to(tmp_file)?; 112 | 113 | #[cfg(unix)] 114 | { 115 | let mut perms = fs::metadata(tmp_file.path())?.permissions(); 116 | perms.set_mode(0o644); 117 | fs::set_permissions(tmp_file.path(), perms)?; 118 | } 119 | 120 | Ok(()) 121 | } 122 | } 123 | 124 | impl Drop for Playlist { 125 | fn drop(&mut self) { 126 | self.schedule_for_deletion(self.playlist.segments.len(), self.current_duration); 127 | self.playlist.end_list = true; 128 | 129 | if let Err(why) = self.atomic_update() { 130 | error!("Failed to write end tag to playlist: {:?}", why); 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /crates/javelin-hls/src/service.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::path::Path; 3 | 4 | use anyhow::{bail, Result}; 5 | use axum::Router; 6 | use javelin_core::session::{self, ManagerMessage}; 7 | use javelin_core::Config; 8 | use tokio::net::TcpListener; 9 | use tower_http::services::ServeDir; 10 | use tracing::{debug, error, info}; 11 | 12 | use crate::config::Config as HlsConfig; 13 | use crate::file_cleaner; 14 | use crate::writer::Writer; 15 | 16 | 17 | pub struct Service { 18 | config: HlsConfig, 19 | session_manager: session::ManagerHandle, 20 | } 21 | 22 | 23 | impl Service { 24 | pub fn new(session_manager: session::ManagerHandle, config: &Config) -> Self { 25 | let config = config.get("hls").unwrap_or_default(); 26 | Self { 27 | config, 28 | session_manager, 29 | } 30 | } 31 | 32 | pub async fn run(self) { 33 | let hls_root = self.config.root_dir.clone(); 34 | info!("HLS directory located at '{}'", hls_root.display()); 35 | 36 | if let Err(why) = directory_cleanup(&hls_root) { 37 | error!("{}", why); 38 | return; 39 | } 40 | 41 | let fcleaner = file_cleaner::FileCleaner::new(); 42 | let fcleaner_sender = fcleaner.sender(); 43 | tokio::spawn(async move { fcleaner.run().await }); 44 | 45 | if self.config.web.enabled { 46 | let addr = self.config.web.addr; 47 | 48 | let serve_dir = ServeDir::new(hls_root); 49 | let routes = Router::new().nest_service("/hls", serve_dir); 50 | 51 | tokio::spawn(async move { 52 | let listener = TcpListener::bind(addr).await.unwrap(); 53 | axum::serve(listener, routes).await.unwrap(); 54 | }); 55 | } 56 | 57 | let (trigger, mut trigger_handle) = session::trigger_channel(); 58 | 59 | if self 60 | .session_manager 61 | .send(ManagerMessage::RegisterTrigger("create_session", trigger)) 62 | .is_err() 63 | { 64 | error!("Failed to register session trigger"); 65 | return; 66 | } 67 | 68 | while let Some((app_name, watcher)) = trigger_handle.recv().await { 69 | match Writer::create(app_name, watcher, fcleaner_sender.clone(), &self.config) { 70 | Ok(writer) => { 71 | tokio::spawn(async move { writer.run().await.unwrap() }); 72 | } 73 | Err(why) => error!("Failed to create writer: {:?}", why), 74 | } 75 | } 76 | } 77 | } 78 | 79 | 80 | fn directory_cleanup>(path: P) -> Result<()> { 81 | let path = path.as_ref(); 82 | 83 | if path.exists() { 84 | debug!("Attempting cleanup of HLS directory"); 85 | 86 | if path.is_dir() { 87 | for entry in fs::read_dir(path)? { 88 | let child_path = entry?.path(); 89 | 90 | if child_path.is_dir() { 91 | fs::remove_dir_all(child_path)?; 92 | } else { 93 | fs::remove_file(child_path)?; 94 | } 95 | } 96 | } else { 97 | bail!("HLS root is not a directory") 98 | } 99 | 100 | info!("HLS directory purged"); 101 | } 102 | 103 | Ok(()) 104 | } 105 | -------------------------------------------------------------------------------- /crates/javelin-hls/src/writer.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryFrom; 2 | use std::fs; 3 | use std::path::{Path, PathBuf}; 4 | 5 | use anyhow::{bail, Result}; 6 | use chrono::Utc; 7 | use javelin_codec::aac::{self, AacCoder}; 8 | use javelin_codec::avc::{self, AvcCoder}; 9 | use javelin_codec::mpegts::TransportStream; 10 | use javelin_codec::{flv, FormatReader, FormatWriter}; 11 | use javelin_core::session; 12 | use javelin_types::{packet, Packet}; 13 | use tracing::{debug, error, info, warn}; 14 | 15 | use crate::config::Config; 16 | use crate::file_cleaner; 17 | use crate::m3u8::Playlist; 18 | 19 | 20 | pub struct Writer { 21 | watcher: session::Watcher, 22 | write_interval: u64, 23 | next_write: u64, 24 | last_keyframe: u64, 25 | keyframe_counter: usize, 26 | buffer: TransportStream, 27 | playlist: Playlist, 28 | stream_path: PathBuf, 29 | avc_coder: AvcCoder, 30 | aac_coder: AacCoder, 31 | } 32 | 33 | impl Writer { 34 | pub fn create( 35 | app_name: String, 36 | watcher: session::Watcher, 37 | fcleaner_sender: file_cleaner::Sender, 38 | config: &Config, 39 | ) -> Result { 40 | let write_interval = 2000; // milliseconds 41 | let next_write = write_interval; // milliseconds 42 | 43 | let hls_root = config.root_dir.clone(); 44 | let stream_path = hls_root.join(app_name); 45 | let playlist_path = stream_path.join("playlist.m3u8"); 46 | 47 | prepare_stream_directory(&stream_path)?; 48 | 49 | Ok(Self { 50 | watcher, 51 | write_interval, 52 | next_write, 53 | last_keyframe: 0, 54 | keyframe_counter: 0, 55 | buffer: TransportStream::new(), 56 | playlist: Playlist::new(playlist_path, fcleaner_sender), 57 | avc_coder: AvcCoder::new(), 58 | aac_coder: AacCoder::new(), 59 | stream_path, 60 | }) 61 | } 62 | 63 | pub async fn run(mut self) -> Result<()> { 64 | while let Ok(packet) = self.watcher.recv().await { 65 | if let Err(why) = self.handle_packet(packet) { 66 | error!("{:?}", why); 67 | } 68 | } 69 | 70 | Ok(()) 71 | } 72 | 73 | fn handle_video(&mut self, timestamp: T, bytes: &[u8]) -> Result<()> 74 | where 75 | T: Into, 76 | { 77 | let timestamp: u64 = timestamp.into(); 78 | 79 | let flv_packet = flv::tag::VideoData::try_from(bytes)?; 80 | let payload = &flv_packet.body; 81 | 82 | if flv_packet.is_sequence_header() { 83 | self.avc_coder.set_dcr(payload.as_ref())?; 84 | return Ok(()); 85 | } 86 | 87 | let keyframe = flv_packet.is_keyframe(); 88 | 89 | if keyframe { 90 | let keyframe_duration = timestamp - self.last_keyframe; 91 | 92 | if self.keyframe_counter == 1 { 93 | self.playlist.set_target_duration(keyframe_duration * 3); 94 | } 95 | 96 | if timestamp >= self.next_write { 97 | let filename = format!( 98 | "{}-{}.mpegts", 99 | Utc::now().timestamp(), 100 | self.keyframe_counter 101 | ); 102 | let path = self.stream_path.join(&filename); 103 | self.buffer.write_to_file(&path)?; 104 | self.playlist.add_media_segment(filename, keyframe_duration); 105 | self.next_write += self.write_interval; 106 | self.last_keyframe = timestamp; 107 | } 108 | 109 | self.keyframe_counter += 1; 110 | } 111 | 112 | let video = match self.avc_coder.read_format(avc::Avcc, payload)? { 113 | Some(avc) => self.avc_coder.write_format(avc::AnnexB, avc)?, 114 | None => return Ok(()), 115 | }; 116 | 117 | let comp_time = flv_packet.composition_time as u64; 118 | 119 | if let Err(why) = self 120 | .buffer 121 | .push_video(timestamp, comp_time, keyframe, video) 122 | { 123 | warn!("Failed to put data into buffer: {:?}", why); 124 | } 125 | 126 | Ok(()) 127 | } 128 | 129 | fn handle_audio(&mut self, timestamp: T, bytes: &[u8]) -> Result<()> 130 | where 131 | T: Into, 132 | { 133 | let timestamp: u64 = timestamp.into(); 134 | 135 | let flv = flv::tag::AudioData::try_from(bytes).unwrap(); 136 | 137 | if flv.is_sequence_header() { 138 | self.aac_coder.set_asc(flv.body.as_ref())?; 139 | return Ok(()); 140 | } 141 | 142 | if self.keyframe_counter == 0 { 143 | return Ok(()); 144 | } 145 | 146 | let audio = match self.aac_coder.read_format(aac::Raw, &flv.body)? { 147 | Some(raw_aac) => self 148 | .aac_coder 149 | .write_format(aac::AudioDataTransportStream, raw_aac)?, 150 | None => return Ok(()), 151 | }; 152 | 153 | if let Err(why) = self.buffer.push_audio(timestamp, audio) { 154 | warn!("Failed to put data into buffer: {:?}", why); 155 | } 156 | 157 | Ok(()) 158 | } 159 | 160 | fn handle_packet(&mut self, packet: Packet) -> Result<()> { 161 | match packet { 162 | Packet { 163 | content_type: packet::FLV_VIDEO_H264, 164 | timestamp: Some(ts), 165 | payload, 166 | } => self.handle_video(ts, &payload), 167 | Packet { 168 | content_type: packet::FLV_AUDIO_AAC, 169 | timestamp: Some(ts), 170 | payload, 171 | } => self.handle_audio(ts, &payload), 172 | _ => Ok(()), 173 | } 174 | } 175 | } 176 | 177 | impl Drop for Writer { 178 | fn drop(&mut self) { 179 | info!("Closing HLS writer for {}", self.stream_path.display()); 180 | } 181 | } 182 | 183 | 184 | fn prepare_stream_directory>(path: P) -> Result<()> { 185 | let stream_path = path.as_ref(); 186 | 187 | if stream_path.exists() && !stream_path.is_dir() { 188 | bail!( 189 | "Path '{}' exists, but is not a directory", 190 | stream_path.display() 191 | ); 192 | } 193 | 194 | debug!("Creating HLS directory at '{}'", stream_path.display()); 195 | fs::create_dir_all(stream_path)?; 196 | 197 | Ok(()) 198 | } 199 | -------------------------------------------------------------------------------- /crates/javelin-rtmp/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "javelin-rtmp" 3 | description = "Simple streaming server (RTMP)" 4 | version.workspace = true 5 | authors.workspace = true 6 | edition.workspace = true 7 | rust-version.workspace = true 8 | license-file.workspace = true 9 | readme.workspace = true 10 | repository.workspace = true 11 | categories.workspace = true 12 | keywords = ["rtmp"] 13 | publish = false 14 | 15 | 16 | [features] 17 | default = [] 18 | rtmps = ["native-tls", "tokio-native-tls"] 19 | 20 | 21 | [dependencies] 22 | anyhow.workspace = true 23 | bytes.workspace = true 24 | futures.workspace = true 25 | javelin-types.workspace = true 26 | javelin-core.workspace = true 27 | serde.workspace = true 28 | thiserror.workspace = true 29 | tracing.workspace = true 30 | 31 | [dependencies.native-tls] 32 | version = "0.2" 33 | optional = true 34 | 35 | [dependencies.tokio] 36 | workspace = true 37 | features = ["rt", "sync", "net"] 38 | 39 | [dependencies.tokio-native-tls] 40 | version = "0.3.0" 41 | optional = true 42 | 43 | [dependencies.tokio-util] 44 | version = "0.7.1" 45 | features = ["codec"] 46 | 47 | [dependencies.rml_rtmp] 48 | version = "0.8" 49 | git = "https://github.com/valeth/rust-media-libs" 50 | branch = "update-deps" 51 | -------------------------------------------------------------------------------- /crates/javelin-rtmp/src/config.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::net::SocketAddr; 3 | use std::time::Duration; 4 | 5 | use serde::Deserialize; 6 | 7 | 8 | #[derive(Debug, Clone, Deserialize)] 9 | pub struct Config { 10 | #[serde(default = "default_addr")] 11 | pub addr: SocketAddr, 12 | 13 | #[serde(default)] 14 | pub stream_keys: HashMap, // TODO: move to database 15 | 16 | #[serde(default = "default_conn_timeout")] 17 | pub connection_timeout: Duration, 18 | 19 | #[cfg(feature = "rtmps")] 20 | #[serde(default)] 21 | pub tls: tls::Config, 22 | } 23 | 24 | impl Default for Config { 25 | fn default() -> Self { 26 | Self { 27 | addr: default_addr(), 28 | stream_keys: HashMap::new(), 29 | connection_timeout: default_conn_timeout(), 30 | #[cfg(feature = "rtmps")] 31 | tls: Default::default(), 32 | } 33 | } 34 | } 35 | 36 | fn default_addr() -> SocketAddr { 37 | SocketAddr::from(([0, 0, 0, 0], 1935)) 38 | } 39 | 40 | fn default_conn_timeout() -> Duration { 41 | Duration::from_secs(5) 42 | } 43 | 44 | 45 | #[cfg(feature = "rtmps")] 46 | mod tls { 47 | use std::fs::File; 48 | use std::io::Read; 49 | use std::net::SocketAddr; 50 | use std::path::PathBuf; 51 | 52 | use anyhow::{Context, Result}; 53 | use serde::Deserialize; 54 | 55 | #[derive(Debug, Clone, Deserialize)] 56 | pub struct Config { 57 | #[serde(default)] 58 | pub enabled: bool, 59 | 60 | #[serde(default = "default_tls_addr")] 61 | pub addr: SocketAddr, 62 | 63 | #[serde(default)] 64 | pub cert_path: Option, 65 | 66 | #[serde(default)] 67 | pub cert_password: String, 68 | } 69 | 70 | impl Config { 71 | pub fn read_cert(&self) -> Result> { 72 | let path = &self.cert_path.as_ref().context("No cert path configured")?; 73 | let mut file = File::open(path)?; 74 | let mut buf = Vec::with_capacity(2500); 75 | file.read_to_end(&mut buf)?; 76 | Ok(buf) 77 | } 78 | } 79 | 80 | impl Default for Config { 81 | fn default() -> Self { 82 | Self { 83 | enabled: false, 84 | addr: default_tls_addr(), 85 | cert_path: None, 86 | cert_password: String::new(), 87 | } 88 | } 89 | } 90 | 91 | fn default_tls_addr() -> SocketAddr { 92 | SocketAddr::from(([0, 0, 0, 0], 1936)) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /crates/javelin-rtmp/src/convert.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use javelin_types::Metadata; 4 | use rml_rtmp::sessions::StreamMetadata; 5 | 6 | // Temporary conversion functions 7 | 8 | pub(crate) fn from_metadata(val: StreamMetadata) -> Metadata { 9 | let mut map = HashMap::with_capacity(11); 10 | 11 | if let Some(v) = val.audio_bitrate_kbps { 12 | map.insert("audio.bitrate", v.to_string()); 13 | } 14 | 15 | if let Some(v) = val.audio_channels { 16 | map.insert("audio.channels", v.to_string()); 17 | } 18 | 19 | if let Some(v) = val.audio_codec_id { 20 | map.insert("audio.codec_id", v.to_string()); 21 | } 22 | 23 | if let Some(v) = val.audio_is_stereo { 24 | map.insert("audio.stereo", v.to_string()); 25 | } 26 | 27 | if let Some(v) = val.audio_sample_rate { 28 | map.insert("audio.sampling_rate", v.to_string()); 29 | } 30 | 31 | if let Some(v) = val.video_bitrate_kbps { 32 | map.insert("video.bitrate", v.to_string()); 33 | } 34 | 35 | if let Some(v) = val.video_codec_id { 36 | map.insert("video.codec_id", v.to_string()); 37 | } 38 | 39 | if let Some(v) = val.video_frame_rate { 40 | map.insert("video.frame_rate", v.to_string()); 41 | } 42 | 43 | if let Some(v) = val.video_height { 44 | map.insert("video.height", v.to_string()); 45 | } 46 | 47 | if let Some(v) = val.video_width { 48 | map.insert("video.width", v.to_string()); 49 | } 50 | 51 | if let Some(v) = val.encoder { 52 | map.insert("encoder", v); 53 | } 54 | 55 | Metadata::from(map) 56 | } 57 | 58 | pub(crate) fn into_metadata(val: Metadata) -> StreamMetadata { 59 | StreamMetadata { 60 | video_width: val.get("video.width"), 61 | video_height: val.get("video.height"), 62 | video_codec_id: val.get("video.codec_id"), 63 | video_frame_rate: val.get("video.frame_rate"), 64 | video_bitrate_kbps: val.get("video.bitrate"), 65 | audio_codec_id: val.get("audio.codec_id"), 66 | audio_bitrate_kbps: val.get("audio.bitrate"), 67 | audio_sample_rate: val.get("audio.sampling_rate"), 68 | audio_channels: val.get("audio.channels"), 69 | audio_is_stereo: val.get("audio.stereo"), 70 | encoder: val.get("encoder"), 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /crates/javelin-rtmp/src/error.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | use thiserror::Error; 4 | use tokio::time; 5 | 6 | use crate::proto::Error as ProtocolError; 7 | 8 | 9 | #[derive(Error, Debug)] 10 | pub enum Error { 11 | #[error("No stream with name {0} found")] 12 | NoSuchStream(String), 13 | 14 | #[error("Client disconnected: {0}")] 15 | Disconnected(#[from] io::Error), 16 | 17 | #[error("Failed to create new session")] 18 | SessionCreationFailed, 19 | 20 | #[error("Failed to release session")] 21 | SessionReleaseFailed, 22 | 23 | #[error("Failed to join session")] 24 | SessionJoinFailed, 25 | 26 | #[error("Failed to send to session")] 27 | SessionSendFailed, 28 | 29 | #[error("Failed to return packet to peer {0}")] 30 | ReturnPacketFailed(u64), 31 | 32 | #[error(transparent)] 33 | ProtocolError(#[from] ProtocolError), 34 | 35 | #[error("Connection timeout")] 36 | ConnectionTimeout(#[from] time::error::Elapsed), 37 | } 38 | -------------------------------------------------------------------------------- /crates/javelin-rtmp/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod config; 2 | mod convert; 3 | pub mod error; 4 | mod peer; 5 | mod proto; 6 | pub mod service; 7 | 8 | 9 | pub use self::error::Error; 10 | pub use self::service::Service; 11 | -------------------------------------------------------------------------------- /crates/javelin-rtmp/src/peer.rs: -------------------------------------------------------------------------------- 1 | use bytes::Bytes; 2 | use futures::{SinkExt, TryStreamExt}; 3 | use javelin_core::session::{self, ManagerMessage, Message}; 4 | use javelin_types::{packet, Packet}; 5 | use tokio::io::{AsyncRead, AsyncWrite}; 6 | use tokio::sync::{self, mpsc, oneshot}; 7 | use tokio::time::timeout; 8 | use tokio_util::codec::{BytesCodec, Framed}; 9 | use tracing::{debug, info, trace}; 10 | 11 | use crate::config::Config; 12 | use crate::error::Error; 13 | use crate::proto::{Event, Protocol}; 14 | 15 | 16 | type ReturnQueue

= (mpsc::Sender

, mpsc::Receiver

); 17 | 18 | 19 | enum State { 20 | Initializing, 21 | Publishing(session::Handle), 22 | Playing(session::Handle, session::Watcher), 23 | Disconnecting, 24 | } 25 | 26 | 27 | /// Represents an incoming connection 28 | pub struct Peer 29 | where 30 | S: AsyncRead + AsyncWrite + Unpin, 31 | { 32 | id: u64, 33 | bytes_stream: Framed, 34 | session_manager: session::ManagerHandle, 35 | return_queue: ReturnQueue, 36 | proto: Protocol, 37 | config: Config, 38 | app_name: Option, 39 | state: State, 40 | } 41 | 42 | impl Peer 43 | where 44 | S: AsyncRead + AsyncWrite + Unpin, 45 | { 46 | pub fn new( 47 | id: u64, 48 | stream: S, 49 | session_manager: session::ManagerHandle, 50 | config: Config, 51 | ) -> Self { 52 | Self { 53 | id, 54 | bytes_stream: Framed::new(stream, BytesCodec::new()), 55 | session_manager, 56 | return_queue: mpsc::channel(64), 57 | proto: Protocol::new(), 58 | config, 59 | app_name: None, 60 | state: State::Initializing, 61 | } 62 | } 63 | 64 | pub async fn run(mut self) -> Result<(), Error> { 65 | loop { 66 | while let Ok(packet) = self.return_queue.1.try_recv() { 67 | if self.handle_return_packet(packet).await.is_err() { 68 | self.disconnect()? 69 | } 70 | } 71 | 72 | match &mut self.state { 73 | State::Initializing | State::Publishing(_) => { 74 | let val = self.bytes_stream.try_next(); 75 | match timeout(self.config.connection_timeout, val).await? { 76 | Ok(Some(data)) => { 77 | for event in self.proto.handle_bytes(&data).unwrap() { 78 | self.handle_event(event).await?; 79 | } 80 | } 81 | _ => self.disconnect()?, 82 | } 83 | } 84 | State::Playing(_, watcher) => { 85 | use sync::broadcast::error::RecvError; 86 | match watcher.recv().await { 87 | Ok(packet) => self.send_back(packet).await?, 88 | Err(RecvError::Closed) => self.disconnect()?, 89 | Err(_) => (), 90 | } 91 | } 92 | State::Disconnecting => { 93 | debug!("Disconnecting..."); 94 | return Ok(()); 95 | } 96 | } 97 | } 98 | } 99 | 100 | async fn handle_return_packet(&mut self, packet: Packet) -> Result<(), Error> { 101 | let bytes = match packet.content_type { 102 | packet::METADATA => self.proto.pack_metadata(packet)?, 103 | packet::FLV_VIDEO_H264 => self.proto.pack_video(packet)?, 104 | packet::FLV_AUDIO_AAC => self.proto.pack_audio(packet)?, 105 | _ => { 106 | trace!(content_type = ?packet.content_type, "Cannot handle content type"); 107 | return Err(Error::ReturnPacketFailed(self.id)); 108 | } 109 | }; 110 | let duration = self.config.connection_timeout; 111 | let bytes = Bytes::from(bytes); 112 | let res = timeout(duration, self.bytes_stream.send(bytes)).await?; 113 | Ok(res?) 114 | } 115 | 116 | async fn handle_event(&mut self, event: Event) -> Result<(), Error> { 117 | match event { 118 | Event::ReturnData(data) => { 119 | self.bytes_stream 120 | .send(data) 121 | .await 122 | .expect("Failed to return data"); 123 | } 124 | Event::SendPacket(packet) => { 125 | if let State::Publishing(session) = &mut self.state { 126 | session 127 | .send(Message::Packet(packet)) 128 | .map_err(|_| Error::SessionSendFailed)?; 129 | } 130 | } 131 | Event::AcquireSession { 132 | app_name, 133 | stream_key, 134 | } => { 135 | self.app_name = Some(app_name.clone()); 136 | let (request, response) = oneshot::channel(); 137 | self.session_manager 138 | .send(ManagerMessage::CreateSession(( 139 | app_name, stream_key, request, 140 | ))) 141 | .map_err(|_| Error::SessionCreationFailed)?; 142 | let session_sender = response.await.map_err(|_| Error::SessionCreationFailed)?; 143 | self.state = State::Publishing(session_sender); 144 | } 145 | Event::JoinSession { app_name, .. } => { 146 | let (request, response) = oneshot::channel(); 147 | self.session_manager 148 | .send(ManagerMessage::JoinSession((app_name, request))) 149 | .map_err(|_| Error::SessionJoinFailed)?; 150 | 151 | match response.await { 152 | Ok((session_sender, session_receiver)) => { 153 | self.state = State::Playing(session_sender, session_receiver); 154 | } 155 | Err(_) => self.disconnect()?, 156 | } 157 | } 158 | Event::SendInitData { .. } => { 159 | // TODO: better initialization handling 160 | if let State::Playing(session, _) = &mut self.state { 161 | let (request, response) = oneshot::channel(); 162 | session 163 | .send(Message::GetInitData(request)) 164 | .map_err(|_| Error::SessionSendFailed)?; 165 | 166 | if let Ok((Some(meta), Some(video), Some(audio))) = response.await { 167 | self.send_back(meta).await?; 168 | self.send_back(video).await?; 169 | self.send_back(audio).await?; 170 | } 171 | } 172 | } 173 | Event::ReleaseSession | Event::LeaveSession => self.disconnect()?, 174 | } 175 | 176 | Ok(()) 177 | } 178 | 179 | async fn send_back(&mut self, packet: Packet) -> Result<(), Error> { 180 | self.return_queue 181 | .0 182 | .send_timeout(packet, self.config.connection_timeout) 183 | .await 184 | .map_err(|_| Error::ReturnPacketFailed(self.id)) 185 | } 186 | 187 | fn disconnect(&mut self) -> Result<(), Error> { 188 | if let State::Publishing(session) = &mut self.state { 189 | let app_name = self.app_name.clone().unwrap(); 190 | session 191 | .send(Message::Disconnect) 192 | .map_err(|_| Error::SessionSendFailed)?; 193 | 194 | self.session_manager 195 | .send(ManagerMessage::ReleaseSession(app_name)) 196 | .map_err(|_| Error::SessionReleaseFailed)?; 197 | } 198 | 199 | self.state = State::Disconnecting; 200 | 201 | Ok(()) 202 | } 203 | } 204 | 205 | impl Drop for Peer 206 | where 207 | S: AsyncRead + AsyncWrite + Unpin, 208 | { 209 | fn drop(&mut self) { 210 | info!("Client {} disconnected", self.id); 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /crates/javelin-rtmp/src/proto.rs: -------------------------------------------------------------------------------- 1 | use std::convert::{TryFrom, TryInto}; 2 | 3 | use bytes::Bytes; 4 | use javelin_types::{packet, Packet}; 5 | use rml_rtmp::handshake::{Handshake, HandshakeProcessResult, PeerType}; 6 | use rml_rtmp::sessions::{ 7 | ServerSession, ServerSessionConfig, ServerSessionEvent, ServerSessionResult, 8 | }; 9 | use rml_rtmp::time::RtmpTimestamp; 10 | use thiserror::Error; 11 | use tracing::debug; 12 | 13 | use crate::convert; 14 | 15 | 16 | #[derive(Error, Debug)] 17 | pub enum Error { 18 | #[error("RTMP handshake failed")] 19 | HandshakeFailed, 20 | 21 | #[error("RTMP session initialization failed")] 22 | SessionInitializationFailed, 23 | 24 | #[error("Tried to use RTMP session while not initialized")] 25 | SessionNotInitialized, 26 | 27 | #[error("Received invalid input")] 28 | InvalidInput, 29 | 30 | #[error("RTMP request was not accepted")] 31 | RequestRejected, 32 | 33 | #[error("No stream ID")] 34 | NoStreamId, 35 | 36 | #[error("Application name cannot be empty")] 37 | EmptyAppName, 38 | } 39 | 40 | 41 | pub enum Event { 42 | ReturnData(Bytes), 43 | SendPacket(Packet), 44 | AcquireSession { 45 | app_name: String, 46 | stream_key: String, 47 | }, 48 | JoinSession { 49 | app_name: String, 50 | stream_key: String, 51 | }, 52 | SendInitData { 53 | app_name: String, 54 | }, 55 | ReleaseSession, 56 | LeaveSession, 57 | } 58 | 59 | 60 | enum State { 61 | HandshakePending, 62 | Ready, 63 | Publishing, 64 | Playing { stream_id: u32 }, 65 | Finished, 66 | } 67 | 68 | 69 | pub struct Protocol { 70 | state: State, 71 | return_queue: Vec, 72 | handshake: Handshake, 73 | session: Option, 74 | } 75 | 76 | impl Protocol { 77 | pub fn new() -> Self { 78 | Self::default() 79 | } 80 | 81 | pub fn handle_bytes(&mut self, input: &[u8]) -> Result, Error> { 82 | match &mut self.state { 83 | State::HandshakePending => { 84 | self.perform_handshake(input)?; 85 | } 86 | _ => { 87 | self.handle_input(input)?; 88 | } 89 | } 90 | 91 | Ok(self.return_queue.drain(..).collect()) 92 | } 93 | 94 | fn handle_input(&mut self, input: &[u8]) -> Result<(), Error> { 95 | let results = self 96 | .session()? 97 | .handle_input(input) 98 | .map_err(|_| Error::InvalidInput)?; 99 | self.handle_results(results)?; 100 | Ok(()) 101 | } 102 | 103 | fn perform_handshake(&mut self, input: &[u8]) -> Result<(), Error> { 104 | let result = self 105 | .handshake 106 | .process_bytes(input) 107 | .map_err(|_| Error::HandshakeFailed)?; 108 | 109 | match result { 110 | HandshakeProcessResult::InProgress { response_bytes } => { 111 | self.emit(Event::ReturnData(response_bytes.into())); 112 | } 113 | HandshakeProcessResult::Completed { 114 | response_bytes, 115 | remaining_bytes, 116 | } => { 117 | debug!("RTMP handshake successful"); 118 | if !response_bytes.is_empty() { 119 | self.emit(Event::ReturnData(response_bytes.into())); 120 | } 121 | 122 | self.initialize_session()?; 123 | 124 | if !remaining_bytes.is_empty() { 125 | self.handle_input(&remaining_bytes)?; 126 | } 127 | 128 | self.state = State::Ready; 129 | } 130 | } 131 | 132 | Ok(()) 133 | } 134 | 135 | fn initialize_session(&mut self) -> Result<(), Error> { 136 | let config = ServerSessionConfig::new(); 137 | let (session, results) = 138 | ServerSession::new(config).map_err(|_| Error::SessionInitializationFailed)?; 139 | self.session = Some(session); 140 | self.handle_results(results) 141 | } 142 | 143 | fn accept_request(&mut self, id: u32) -> Result<(), Error> { 144 | let results = { 145 | let session = self.session()?; 146 | session 147 | .accept_request(id) 148 | .map_err(|_| Error::RequestRejected)? 149 | }; 150 | self.handle_results(results) 151 | } 152 | 153 | pub fn pack_metadata(&mut self, packet: Packet) -> Result, Error> { 154 | let stream_id = self.stream_id()?; 155 | let metadata = convert::into_metadata(packet.try_into().unwrap()); 156 | self.session()? 157 | .send_metadata(stream_id, &metadata) 158 | .map_err(|_| Error::InvalidInput) 159 | .map(|v| v.bytes) 160 | } 161 | 162 | pub fn pack_video(&mut self, packet: Packet) -> Result, Error> { 163 | let stream_id = self.stream_id()?; 164 | let data = packet.payload; 165 | let timestamp = packet 166 | .timestamp 167 | .map(|v| RtmpTimestamp::new(v.into())) 168 | .unwrap(); 169 | 170 | self.session()? 171 | .send_video_data(stream_id, data, timestamp, false) 172 | .map_err(|_| Error::InvalidInput) 173 | .map(|v| v.bytes) 174 | } 175 | 176 | pub fn pack_audio(&mut self, packet: Packet) -> Result, Error> { 177 | let stream_id = self.stream_id()?; 178 | let data = packet.payload; 179 | let timestamp = packet 180 | .timestamp 181 | .map(|v| RtmpTimestamp::new(v.into())) 182 | .unwrap(); 183 | 184 | self.session()? 185 | .send_audio_data(stream_id, data, timestamp, false) 186 | .map_err(|_| Error::InvalidInput) 187 | .map(|v| v.bytes) 188 | } 189 | 190 | fn handle_results(&mut self, results: Vec) -> Result<(), Error> { 191 | for result in results { 192 | match result { 193 | ServerSessionResult::OutboundResponse(packet) => { 194 | self.emit(Event::ReturnData(packet.bytes.into())); 195 | } 196 | ServerSessionResult::RaisedEvent(event) => { 197 | self.handle_event(event)?; 198 | } 199 | ServerSessionResult::UnhandleableMessageReceived(_) => (), 200 | } 201 | } 202 | 203 | Ok(()) 204 | } 205 | 206 | fn handle_event(&mut self, event: ServerSessionEvent) -> Result<(), Error> { 207 | use ServerSessionEvent::*; 208 | 209 | match event { 210 | ConnectionRequested { 211 | request_id, 212 | app_name, 213 | .. 214 | } => { 215 | if app_name.is_empty() { 216 | return Err(Error::EmptyAppName); 217 | } 218 | 219 | self.accept_request(request_id)?; 220 | } 221 | PublishStreamRequested { 222 | request_id, 223 | app_name, 224 | stream_key, 225 | .. 226 | } => { 227 | self.emit(Event::AcquireSession { 228 | app_name, 229 | stream_key, 230 | }); 231 | self.accept_request(request_id)?; 232 | self.state = State::Publishing; 233 | } 234 | PublishStreamFinished { .. } => { 235 | self.emit(Event::ReleaseSession); 236 | self.state = State::Finished; 237 | } 238 | PlayStreamRequested { 239 | request_id, 240 | app_name, 241 | stream_key, 242 | stream_id, 243 | .. 244 | } => { 245 | self.emit(Event::JoinSession { 246 | app_name: app_name.clone(), 247 | stream_key, 248 | }); 249 | self.accept_request(request_id)?; 250 | self.emit(Event::SendInitData { app_name }); 251 | self.state = State::Playing { stream_id }; 252 | } 253 | PlayStreamFinished { .. } => { 254 | self.emit(Event::LeaveSession); 255 | self.state = State::Finished; 256 | } 257 | AudioDataReceived { 258 | data, timestamp, .. 259 | } => { 260 | let packet = Packet::new(packet::FLV_AUDIO_AAC, Some(timestamp.value), data); 261 | self.emit(Event::SendPacket(packet)); 262 | } 263 | VideoDataReceived { 264 | data, timestamp, .. 265 | } => { 266 | let packet = Packet::new(packet::FLV_VIDEO_H264, Some(timestamp.value), data); 267 | self.emit(Event::SendPacket(packet)); 268 | } 269 | StreamMetadataChanged { metadata, .. } => { 270 | let metadata = convert::from_metadata(metadata); 271 | let payload = Bytes::try_from(metadata).unwrap(); 272 | let packet = Packet::new::(packet::METADATA, None, payload); 273 | self.emit(Event::SendPacket(packet)); 274 | } 275 | _ => (), 276 | } 277 | 278 | Ok(()) 279 | } 280 | 281 | fn emit(&mut self, event: Event) { 282 | self.return_queue.push(event); 283 | } 284 | 285 | fn stream_id(&self) -> Result { 286 | match self.state { 287 | State::Playing { stream_id } => Ok(stream_id), 288 | _ => Err(Error::NoStreamId), 289 | } 290 | } 291 | 292 | fn session(&mut self) -> Result<&mut ServerSession, Error> { 293 | self.session.as_mut().ok_or(Error::SessionNotInitialized) 294 | } 295 | } 296 | 297 | impl Default for Protocol { 298 | fn default() -> Self { 299 | Self { 300 | state: State::HandshakePending, 301 | return_queue: Vec::with_capacity(8), 302 | handshake: Handshake::new(PeerType::Server), 303 | session: None, 304 | } 305 | } 306 | } 307 | -------------------------------------------------------------------------------- /crates/javelin-rtmp/src/service.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{self, Display}; 2 | use std::io::ErrorKind as IoErrorKind; 3 | use std::sync::atomic::{AtomicUsize, Ordering}; 4 | 5 | use anyhow::Result; 6 | use javelin_core::{session, Config}; 7 | use tokio::io::{AsyncRead, AsyncWrite}; 8 | use tokio::net::TcpListener; 9 | use tracing::{error, info}; 10 | #[cfg(feature = "rtmps")] 11 | use {native_tls, tokio_native_tls::TlsAcceptor}; 12 | 13 | use crate::config::Config as RtmpConfig; 14 | use crate::peer::Peer; 15 | use crate::Error; 16 | 17 | 18 | #[derive(Debug, Default)] 19 | pub(crate) struct ClientId { 20 | value: AtomicUsize, 21 | } 22 | 23 | impl ClientId { 24 | pub fn increment(&self) { 25 | self.value.fetch_add(1, Ordering::SeqCst); 26 | } 27 | } 28 | 29 | impl Display for ClientId { 30 | fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { 31 | write!(fmt, "{}", self.value.load(Ordering::SeqCst)) 32 | } 33 | } 34 | 35 | impl From<&ClientId> for u64 { 36 | fn from(id: &ClientId) -> Self { 37 | id.value.load(Ordering::SeqCst) as u64 38 | } 39 | } 40 | 41 | 42 | pub struct Service { 43 | config: RtmpConfig, 44 | session_manager: session::ManagerHandle, 45 | client_id: ClientId, 46 | } 47 | 48 | impl Service { 49 | pub fn new(session_manager: session::ManagerHandle, config: &Config) -> Self { 50 | Self { 51 | session_manager, 52 | config: config.get("rtmp").unwrap_or_default(), 53 | client_id: ClientId::default(), 54 | } 55 | } 56 | 57 | pub async fn run(self) { 58 | #[cfg(not(feature = "rtmps"))] 59 | let res = self.handle_rtmp().await; 60 | #[cfg(feature = "rtmps")] 61 | let res = tokio::try_join!(self.handle_rtmp(), self.handle_rtmps()); 62 | 63 | if let Err(err) = res { 64 | error!("{}", err); 65 | } 66 | } 67 | 68 | async fn handle_rtmp(&self) -> Result<()> { 69 | let addr = &self.config.addr; 70 | let listener = TcpListener::bind(addr).await?; 71 | info!("Listening for RTMP connections on {}", addr); 72 | 73 | loop { 74 | let (tcp_stream, _addr) = listener.accept().await?; 75 | self.process(tcp_stream); 76 | self.client_id.increment(); 77 | } 78 | } 79 | 80 | #[cfg(feature = "rtmps")] 81 | async fn handle_rtmps(&self) -> Result<()> { 82 | use tracing::info; 83 | 84 | if !self.config.tls.enabled { 85 | return Ok(()); 86 | } 87 | 88 | let addr = &self.config.tls.addr; 89 | let mut listener = TcpListener::bind(addr).await?; 90 | info!("Listening for RTMPS connections on {}", addr); 91 | 92 | let tls_acceptor = { 93 | let p12 = self.config.tls.read_cert()?; 94 | let password = &self.config.tls.cert_password; 95 | let cert = native_tls::Identity::from_pkcs12(&p12, password)?; 96 | TlsAcceptor::from(native_tls::TlsAcceptor::builder(cert).build()?) 97 | }; 98 | 99 | loop { 100 | let (tcp_stream, _addr) = listener.accept().await?; 101 | tcp_stream.set_keepalive(Some(Duration::from_secs(30)))?; 102 | let tls_stream = tls_acceptor.accept(tcp_stream).await?; 103 | self.process(tls_stream); 104 | } 105 | } 106 | 107 | fn process(&self, stream: S) 108 | where 109 | S: AsyncRead + AsyncWrite + Unpin + Send + Sync + 'static, 110 | { 111 | info!("New client connection: {}", &self.client_id); 112 | let id = (&self.client_id).into(); 113 | let peer = Peer::new( 114 | id, 115 | stream, 116 | self.session_manager.clone(), 117 | self.config.clone(), 118 | ); 119 | 120 | tokio::spawn(async move { 121 | if let Err(err) = peer.run().await { 122 | match err { 123 | Error::Disconnected(e) if e.kind() == IoErrorKind::ConnectionReset => (), 124 | e => error!("{}", e), 125 | } 126 | } 127 | }); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /crates/javelin-srt/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "javelin-srt" 3 | description = "Simple streaming server (SRT)" 4 | version.workspace = true 5 | authors.workspace = true 6 | edition.workspace = true 7 | rust-version.workspace = true 8 | license-file.workspace = true 9 | readme.workspace = true 10 | repository.workspace = true 11 | categories.workspace = true 12 | keywords = ["srt"] 13 | publish = false 14 | 15 | 16 | [lib] 17 | crate-type = ["rlib", "dylib"] 18 | 19 | 20 | [dependencies] 21 | serde.workspace = true 22 | chrono.workspace = true 23 | futures.workspace = true 24 | javelin-types.workspace = true 25 | javelin-core.workspace = true 26 | thiserror.workspace = true 27 | tracing.workspace = true 28 | tokio.workspace = true 29 | mpeg2ts = "0.3" 30 | base64 = "0.22" 31 | 32 | [dependencies.srt-protocol] 33 | version = "0.4" 34 | # git = "https://github.com/russelltg/srt-rs" 35 | # branch = "main" 36 | 37 | [dependencies.srt-tokio] 38 | version = "0.4" 39 | # git = "https://github.com/russelltg/srt-rs" 40 | # branch = "main" 41 | -------------------------------------------------------------------------------- /crates/javelin-srt/src/config.rs: -------------------------------------------------------------------------------- 1 | use std::net::SocketAddr; 2 | 3 | use serde::Deserialize; 4 | 5 | #[derive(Debug, Clone, Deserialize)] 6 | pub struct Config { 7 | #[serde(default = "default_addr")] 8 | pub addr: SocketAddr, 9 | } 10 | 11 | impl Default for Config { 12 | fn default() -> Self { 13 | Self { 14 | addr: default_addr(), 15 | } 16 | } 17 | } 18 | 19 | fn default_addr() -> SocketAddr { 20 | SocketAddr::from(([0, 0, 0, 0], 3001)) 21 | } 22 | -------------------------------------------------------------------------------- /crates/javelin-srt/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod config; 2 | mod peer; 3 | mod service; 4 | 5 | 6 | use std::io; 7 | 8 | pub use self::service::Service; 9 | 10 | 11 | #[derive(Debug, thiserror::Error)] 12 | pub enum Error { 13 | #[error("Client did not provide a stream id parameter")] 14 | StreamIdMissing, 15 | 16 | #[error("Client did not provide a well formed ACL through parameters")] 17 | InvalidAccessControlParams, 18 | 19 | #[error("Client did not provide required ACL parameters")] 20 | MissingAccessControlParams, 21 | 22 | #[error("Requested mode is not supported")] 23 | ModeNotSupported, 24 | 25 | #[error("Client is not authorized to access the resource")] 26 | Unauthorized, 27 | 28 | #[error("Failed to parse the given stream id")] 29 | StreamIdDecodeFailed { stream_id: String }, 30 | 31 | #[error(transparent)] 32 | Io(#[from] io::Error), 33 | } 34 | -------------------------------------------------------------------------------- /crates/javelin-srt/src/peer.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use std::time::Instant; 3 | 4 | use futures::{SinkExt, StreamExt}; 5 | use javelin_core::session::Message; 6 | use javelin_types::{packet, Packet}; 7 | use srt_tokio::SrtSocket; 8 | use tokio::sync::broadcast::error::RecvError; 9 | use tokio::sync::broadcast::Receiver; 10 | use tokio::sync::mpsc::UnboundedSender; 11 | use tracing::{error, trace, warn}; 12 | 13 | use crate::Error; 14 | 15 | 16 | type SessionSender = UnboundedSender; 17 | type SessionReceiver = Receiver; 18 | 19 | 20 | enum State { 21 | Publish, 22 | Receive(SessionReceiver), 23 | } 24 | 25 | 26 | pub struct Peer { 27 | session_sender: SessionSender, 28 | state: State, 29 | } 30 | 31 | impl Peer { 32 | pub fn new_publishing(session_sender: SessionSender) -> Self { 33 | Self { 34 | session_sender, 35 | state: State::Publish, 36 | } 37 | } 38 | 39 | pub fn new_receiving(session_sender: SessionSender, session_receiver: SessionReceiver) -> Self { 40 | Self { 41 | session_sender, 42 | state: State::Receive(session_receiver), 43 | } 44 | } 45 | } 46 | 47 | 48 | #[tracing::instrument(skip_all)] 49 | pub(crate) async fn handle_peer(peer: Peer, sock: SrtSocket) { 50 | let result = match peer.state { 51 | State::Publish => handle_publishing_peer(sock, peer.session_sender).await, 52 | State::Receive(session_receiver) => { 53 | handle_receiving_peer(sock, peer.session_sender, session_receiver).await 54 | } 55 | }; 56 | 57 | if let Err(err) = result { 58 | error!(%err); 59 | } 60 | 61 | trace!("Connection closed"); 62 | } 63 | 64 | 65 | #[tracing::instrument(skip_all)] 66 | async fn handle_receiving_peer( 67 | mut sock: SrtSocket, 68 | _session_sender: UnboundedSender, 69 | mut session_receiver: Receiver, 70 | ) -> Result<(), Error> { 71 | let socket_id = sock.settings().remote_sockid; 72 | trace!(?socket_id); 73 | 74 | loop { 75 | match session_receiver.recv().await { 76 | Ok(Packet { 77 | content_type: packet::CONTAINER_MPEGTS, 78 | payload, 79 | .. 80 | }) => { 81 | let timestamp = Instant::now(); 82 | match sock.send((timestamp, payload)).await { 83 | Ok(_) => (), 84 | Err(err) if err.kind() == io::ErrorKind::NotConnected => break, 85 | Err(err) => return Err(err.into()), 86 | } 87 | } 88 | Ok(Packet { content_type, .. }) => { 89 | trace!(?content_type, "Cannot handle packet at this point"); 90 | break; 91 | } 92 | Err(RecvError::Closed) => { 93 | break; 94 | } 95 | Err(RecvError::Lagged(skipped_amount)) => { 96 | warn!(%skipped_amount ,"Client receiver lagged behind"); 97 | } 98 | } 99 | } 100 | 101 | sock.close_and_finish().await?; 102 | 103 | Ok(()) 104 | } 105 | 106 | 107 | #[tracing::instrument(skip_all)] 108 | async fn handle_publishing_peer( 109 | mut sock: SrtSocket, 110 | session_sender: UnboundedSender, 111 | ) -> Result<(), Error> { 112 | let socket_id = sock.settings().remote_sockid; 113 | trace!(?socket_id); 114 | 115 | while let Some(data) = sock.next().await { 116 | let (_, bytes) = data?; 117 | let timestamp = chrono::Utc::now().timestamp(); 118 | let packet = Packet::new(packet::CONTAINER_MPEGTS, Some(timestamp), bytes); 119 | if session_sender.send(Message::Packet(packet)).is_err() { 120 | break; 121 | } 122 | } 123 | 124 | sock.close_and_finish().await?; 125 | 126 | Ok(()) 127 | } 128 | -------------------------------------------------------------------------------- /crates/javelin-srt/src/service.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use base64::engine::general_purpose::URL_SAFE as BASE64_URL_SAFE; 4 | use base64::Engine; 5 | use futures::StreamExt; 6 | use javelin_core::session::ManagerMessage; 7 | use javelin_core::{session, Config}; 8 | use srt_tokio::access::{ 9 | AccessControlList, ConnectionMode, ServerRejectReason, StandardAccessControlEntry, 10 | }; 11 | use srt_tokio::options::StreamId; 12 | use srt_tokio::{ConnectionRequest, SrtListener}; 13 | use tokio::sync::oneshot; 14 | use tracing::{error, info, trace}; 15 | 16 | use crate::config::Config as SrtConfig; 17 | use crate::peer::{handle_peer, Peer}; 18 | use crate::Error; 19 | 20 | 21 | #[derive(Debug)] 22 | pub struct Service { 23 | session_manager: session::ManagerHandle, 24 | config: SrtConfig, 25 | } 26 | 27 | impl Service { 28 | pub fn new(session_manager: session::ManagerHandle, config: &Config) -> Self { 29 | Service { 30 | session_manager, 31 | config: config.get("srt").unwrap_or_default(), 32 | } 33 | } 34 | 35 | pub async fn run(self) { 36 | let addr = self.config.addr; 37 | 38 | let (_listener, mut conn) = SrtListener::builder().bind(addr).await.unwrap(); 39 | 40 | info!("Listening for SRT connections on {}", &addr); 41 | 42 | while let Some(conn_req) = conn.incoming().next().await { 43 | let session_manager = self.session_manager.clone(); 44 | 45 | tokio::spawn(async move { 46 | if let Err(err) = handle_request(session_manager, conn_req).await { 47 | error!(%err); 48 | } 49 | }); 50 | } 51 | } 52 | } 53 | 54 | 55 | fn decode_base64_stream_id(stream_id: &StreamId) -> Result { 56 | trace!("attempting to decode streamid as base64"); 57 | 58 | let stream_id = stream_id.as_str(); 59 | 60 | let decoded_sid = 61 | BASE64_URL_SAFE 62 | .decode(stream_id) 63 | .map_err(|_| Error::StreamIdDecodeFailed { 64 | stream_id: stream_id.to_string(), 65 | })?; 66 | 67 | trace!(decoded_sid.len = %decoded_sid.len()); 68 | 69 | String::from_utf8(decoded_sid).map_err(|_| Error::StreamIdDecodeFailed { 70 | stream_id: stream_id.to_string(), 71 | }) 72 | } 73 | 74 | 75 | fn parse_stream_id(stream_id: &StreamId) -> Result { 76 | let stream_id = if stream_id.starts_with("#!") { 77 | trace!("got plain access control list"); 78 | Cow::from(stream_id.as_str()) 79 | } else { 80 | decode_base64_stream_id(stream_id)?.into() 81 | }; 82 | 83 | trace!(?stream_id); 84 | 85 | let acl = stream_id 86 | .parse::() 87 | .map_err(|_| Error::InvalidAccessControlParams)?; 88 | 89 | trace!(?acl); 90 | 91 | Ok(acl) 92 | } 93 | 94 | 95 | #[tracing::instrument(skip_all)] 96 | async fn authorize( 97 | session_handle: session::ManagerHandle, 98 | stream_id: Option<&StreamId>, 99 | ) -> Result { 100 | let stream_id = stream_id.ok_or(Error::StreamIdMissing)?; 101 | 102 | let acl = parse_stream_id(stream_id)?; 103 | let acl = acl.0.into_iter().map(StandardAccessControlEntry::try_from); 104 | 105 | let mut user_ident = None; 106 | let mut res_name = None; 107 | let mut conn_mode = None; 108 | 109 | for entry in acl { 110 | let entry = entry.map_err(|_| Error::InvalidAccessControlParams)?; 111 | 112 | match entry { 113 | StandardAccessControlEntry::UserName(uname) => { 114 | user_ident = Some(uname); 115 | } 116 | StandardAccessControlEntry::ResourceName(rname) => { 117 | res_name = Some(rname); 118 | } 119 | StandardAccessControlEntry::Mode(mode) => { 120 | conn_mode = Some(mode); 121 | } 122 | _ => (), 123 | } 124 | } 125 | 126 | let (user_ident, res_name, conn_mode) = match (user_ident, res_name, conn_mode) { 127 | (Some(u), Some(r), Some(m)) => (u, r, m), 128 | (Some(u), Some(r), None) => (u, r, ConnectionMode::Request), 129 | _ => return Err(Error::MissingAccessControlParams), 130 | }; 131 | 132 | let peer = match conn_mode { 133 | ConnectionMode::Publish => { 134 | let (tx, rx) = oneshot::channel(); 135 | 136 | let message = 137 | ManagerMessage::CreateSession((res_name.to_string(), user_ident.to_string(), tx)); 138 | 139 | session_handle 140 | .send(message) 141 | .map_err(|_| Error::Unauthorized)?; 142 | 143 | let session_tx = rx.await.map_err(|_| Error::Unauthorized)?; 144 | 145 | Peer::new_publishing(session_tx) 146 | } 147 | ConnectionMode::Request => { 148 | let (tx, rx) = oneshot::channel(); 149 | 150 | let message = ManagerMessage::JoinSession((res_name.to_string(), tx)); 151 | 152 | session_handle 153 | .send(message) 154 | .map_err(|_| Error::Unauthorized)?; 155 | 156 | let (session_tx, session_rx) = rx.await.map_err(|_| Error::Unauthorized)?; 157 | 158 | Peer::new_receiving(session_tx, session_rx) 159 | } 160 | _ => return Err(Error::ModeNotSupported), 161 | }; 162 | 163 | Ok(peer) 164 | } 165 | 166 | 167 | async fn handle_request( 168 | session_handle: session::ManagerHandle, 169 | conn_req: ConnectionRequest, 170 | ) -> Result<(), Error> { 171 | let stream_id = conn_req.stream_id(); 172 | 173 | match authorize(session_handle, stream_id).await { 174 | Ok(peer) => { 175 | trace!("Accepting request"); 176 | let sock = conn_req.accept(None).await?; 177 | tokio::spawn(async move { handle_peer(peer, sock).await }); 178 | } 179 | Err(err) => { 180 | reject_request(conn_req, err).await?; 181 | } 182 | }; 183 | 184 | Ok(()) 185 | } 186 | 187 | async fn reject_request(conn_req: ConnectionRequest, error: Error) -> Result<(), Error> { 188 | trace!(%error, "Rejecting request"); 189 | 190 | let reason = match error { 191 | Error::StreamIdDecodeFailed { stream_id: _ } 192 | | Error::StreamIdMissing 193 | | Error::InvalidAccessControlParams 194 | | Error::MissingAccessControlParams => ServerRejectReason::BadRequest, 195 | Error::ModeNotSupported => ServerRejectReason::BadMode, 196 | Error::Unauthorized => ServerRejectReason::Unauthorized, 197 | Error::Io(_) => ServerRejectReason::InternalServerError, 198 | }; 199 | 200 | conn_req.reject(reason.into()).await?; 201 | 202 | Ok(()) 203 | } 204 | -------------------------------------------------------------------------------- /crates/javelin-types/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "javelin-types" 3 | description = "Simple streaming server (shared types)" 4 | version.workspace = true 5 | authors.workspace = true 6 | edition.workspace = true 7 | rust-version.workspace = true 8 | license-file.workspace = true 9 | readme.workspace = true 10 | repository.workspace = true 11 | categories.workspace = true 12 | keywords.workspace = true 13 | publish = false 14 | 15 | 16 | [lib] 17 | crate-type = ["rlib", "dylib"] 18 | 19 | 20 | [dependencies] 21 | async-trait.workspace = true 22 | bincode.workspace = true 23 | bytes.workspace = true 24 | serde.workspace = true 25 | thiserror.workspace = true 26 | -------------------------------------------------------------------------------- /crates/javelin-types/src/data.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::convert::TryFrom; 3 | use std::str::FromStr; 4 | 5 | use bytes::Bytes; 6 | use serde::{Deserialize, Serialize}; 7 | 8 | use crate::{packet, Error, Packet}; 9 | 10 | 11 | type StringMap = HashMap; 12 | type StrMap<'a> = HashMap<&'a str, String>; 13 | 14 | 15 | #[derive(Clone, Copy, Serialize, Deserialize)] 16 | pub struct Timestamp { 17 | value: u64, 18 | } 19 | 20 | impl From for Timestamp { 21 | fn from(val: u32) -> Self { 22 | Self { value: val.into() } 23 | } 24 | } 25 | 26 | impl From for u32 { 27 | fn from(val: Timestamp) -> Self { 28 | val.value as u32 29 | } 30 | } 31 | 32 | impl From for Timestamp { 33 | fn from(val: u64) -> Self { 34 | Self { value: val } 35 | } 36 | } 37 | 38 | impl From for u64 { 39 | fn from(val: Timestamp) -> Self { 40 | val.value 41 | } 42 | } 43 | 44 | impl From for Timestamp { 45 | fn from(val: i64) -> Self { 46 | Self { value: val as u64 } 47 | } 48 | } 49 | 50 | impl From for i64 { 51 | fn from(val: Timestamp) -> Self { 52 | val.value as i64 53 | } 54 | } 55 | 56 | 57 | #[derive(Clone, Serialize, Deserialize)] 58 | pub struct Metadata(StringMap); 59 | 60 | impl Metadata { 61 | pub fn get(&self, key: K) -> Option 62 | where 63 | K: AsRef, 64 | V: FromStr, 65 | { 66 | self.0.get(key.as_ref()).and_then(|v| v.parse().ok()) 67 | } 68 | } 69 | 70 | impl From for Metadata { 71 | fn from(val: HashMap) -> Self { 72 | Self(val) 73 | } 74 | } 75 | 76 | impl<'a> From> for Metadata { 77 | fn from(val: StrMap<'a>) -> Self { 78 | let new_map = val 79 | .into_iter() 80 | .fold(StringMap::new(), |mut acc, (key, value)| { 81 | acc.insert(key.to_owned(), value); 82 | acc 83 | }); 84 | Self::from(new_map) 85 | } 86 | } 87 | 88 | impl TryFrom for Bytes { 89 | type Error = Box; 90 | 91 | fn try_from(val: Metadata) -> Result { 92 | let data = bincode::serialize(&val)?; 93 | Ok(Bytes::from(data)) 94 | } 95 | } 96 | 97 | impl TryFrom<&[u8]> for Metadata { 98 | type Error = Box; 99 | 100 | fn try_from(val: &[u8]) -> Result { 101 | Ok(bincode::deserialize(val)?) 102 | } 103 | } 104 | 105 | impl TryFrom for Packet { 106 | type Error = Error; 107 | 108 | fn try_from(val: Metadata) -> Result { 109 | Ok(Self { 110 | content_type: packet::METADATA, 111 | timestamp: None, 112 | payload: Bytes::try_from(val)?, 113 | }) 114 | } 115 | } 116 | 117 | impl TryFrom for Metadata { 118 | type Error = Error; 119 | 120 | fn try_from(val: Packet) -> Result { 121 | let payload = &*val.payload; 122 | Self::try_from(payload) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /crates/javelin-types/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod data; 2 | pub mod models; 3 | pub mod packet; 4 | 5 | 6 | pub type Error = Box; 7 | 8 | 9 | // foreign re-exports 10 | pub use async_trait::async_trait; 11 | 12 | pub use self::data::{Metadata, Timestamp}; 13 | pub use self::packet::Packet; 14 | -------------------------------------------------------------------------------- /crates/javelin-types/src/models.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | use crate::async_trait; 4 | 5 | 6 | #[derive(Debug, Error)] 7 | pub enum Error { 8 | #[error("Database lookup failed")] 9 | LookupFailed, 10 | 11 | #[error("Database update failed")] 12 | UpdateFailed, 13 | } 14 | 15 | 16 | #[derive(Debug)] 17 | pub struct User { 18 | pub name: String, 19 | pub key: String, 20 | } 21 | 22 | #[async_trait] 23 | pub trait UserRepository { 24 | async fn user_by_name(&self, name: &str) -> Result, Error>; 25 | 26 | async fn add_user_with_key(&mut self, name: &str, key: &str) -> Result<(), Error>; 27 | 28 | async fn user_has_key(&self, name: &str, key: &str) -> Result { 29 | if let Some(user) = self.user_by_name(name).await? { 30 | return Ok(user.key == key); 31 | } 32 | 33 | Ok(false) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /crates/javelin-types/src/packet.rs: -------------------------------------------------------------------------------- 1 | use bytes::Bytes; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use crate::Timestamp; 5 | 6 | 7 | // FIXME: remove temporary content id values 8 | pub const METADATA: ContentType = ContentType::new(0); 9 | pub const FLV_VIDEO_H264: ContentType = ContentType::new(1); 10 | pub const FLV_AUDIO_AAC: ContentType = ContentType::new(2); 11 | pub const CONTAINER_MPEGTS: ContentType = ContentType::new(3); 12 | 13 | 14 | #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 15 | pub struct ContentType(u32); 16 | 17 | impl ContentType { 18 | const fn new(content_id: u32) -> Self { 19 | Self(content_id) 20 | } 21 | } 22 | 23 | 24 | #[derive(Clone, Serialize, Deserialize)] 25 | pub struct Packet { 26 | pub content_type: ContentType, 27 | pub timestamp: Option, 28 | pub payload: Bytes, 29 | } 30 | 31 | impl Packet { 32 | pub fn new(content_type: ContentType, timestamp: Option, payload: B) -> Self 33 | where 34 | T: Into, 35 | B: Into, 36 | { 37 | Self { 38 | content_type, 39 | timestamp: timestamp.map(Into::into), 40 | payload: payload.into(), 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /crates/javelin/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "javelin" 3 | description = "Simple streaming server" 4 | version.workspace = true 5 | authors.workspace = true 6 | edition.workspace = true 7 | rust-version.workspace = true 8 | license-file.workspace = true 9 | readme.workspace = true 10 | repository.workspace = true 11 | categories.workspace = true 12 | keywords.workspace = true 13 | publish = false 14 | default-run = "server" 15 | 16 | 17 | [features] 18 | default = ["rtmp", "hls"] 19 | rtmp = ["javelin-rtmp"] 20 | rtmps = ["javelin-rtmp/rtmps"] 21 | hls = ["javelin-hls"] 22 | 23 | 24 | [dependencies] 25 | anyhow.workspace = true 26 | tracing.workspace = true 27 | tracing-subscriber.workspace = true 28 | chrono.workspace = true 29 | serde.workspace = true 30 | javelin-core.workspace = true 31 | javelin-types.workspace = true 32 | 33 | [dependencies.sqlx] 34 | workspace = true 35 | features = ["sqlite"] 36 | 37 | [dependencies.clap] 38 | version = "4.5" 39 | features = ["derive"] 40 | 41 | [dependencies.javelin-srt] 42 | workspace = true 43 | 44 | [dependencies.javelin-rtmp] 45 | workspace = true 46 | optional = true 47 | 48 | [dependencies.javelin-hls] 49 | workspace = true 50 | optional = true 51 | 52 | [dependencies.tokio] 53 | workspace = true 54 | features = ["rt-multi-thread"] 55 | -------------------------------------------------------------------------------- /crates/javelin/database/migrations/20240810235858_create_users.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS users ( 2 | id INTEGER PRIMARY KEY AUTOINCREMENT, 3 | name TEXT NOT NULL UNIQUE, 4 | key TEXT NOT NULL UNIQUE 5 | ); 6 | -------------------------------------------------------------------------------- /crates/javelin/src/bin/cli.rs: -------------------------------------------------------------------------------- 1 | #![warn(clippy::all)] 2 | 3 | use std::path::PathBuf; 4 | 5 | use anyhow::Result; 6 | use clap::{Parser, Subcommand}; 7 | use javelin::database::{Database, UserRepository}; 8 | use javelin_core::Config; 9 | 10 | 11 | #[derive(Parser)] 12 | #[command(version, about)] 13 | pub struct CliArgs { 14 | #[arg(short, long, default_value = "./config")] 15 | pub config_dir: PathBuf, 16 | 17 | #[command(subcommand)] 18 | pub cmd: Command, 19 | } 20 | 21 | #[derive(Subcommand)] 22 | pub enum Command { 23 | PermitStream { 24 | #[arg(long, required = true)] 25 | user: String, 26 | #[arg(long, required = true)] 27 | key: String, 28 | }, 29 | } 30 | 31 | 32 | #[tokio::main] 33 | async fn main() -> Result<()> { 34 | let args = CliArgs::parse(); 35 | 36 | let config = Config::try_from_path(&args.config_dir)?; 37 | 38 | match args.cmd { 39 | Command::PermitStream { user, key } => { 40 | permit_stream(&user, &key, &config).await?; 41 | } 42 | } 43 | 44 | Ok(()) 45 | } 46 | 47 | 48 | async fn permit_stream(user: &str, key: &str, config: &Config) -> Result<()> { 49 | let mut database_handle = Database::new(config).await; 50 | 51 | database_handle.add_user_with_key(user, key).await?; 52 | 53 | Ok(()) 54 | } 55 | -------------------------------------------------------------------------------- /crates/javelin/src/bin/server.rs: -------------------------------------------------------------------------------- 1 | #![warn(clippy::all)] 2 | 3 | use std::path::PathBuf; 4 | 5 | use anyhow::Result; 6 | use clap::Parser; 7 | use javelin::database::Database; 8 | use javelin_core::Config; 9 | use javelin_core::session; 10 | 11 | 12 | #[derive(Parser)] 13 | #[command(version, about)] 14 | pub struct ServerArgs { 15 | #[arg(short, long, default_value = "./config")] 16 | pub config_dir: PathBuf, 17 | } 18 | 19 | 20 | #[tokio::main] 21 | async fn main() -> Result<()> { 22 | if let Err(why) = init_tracing() { 23 | eprintln!("Failed to initialize logger: {}", why); 24 | }; 25 | 26 | let args = ServerArgs::parse(); 27 | 28 | let config = Config::try_from_path(&args.config_dir)?; 29 | 30 | let mut handles = Vec::new(); 31 | 32 | let database_handle = Database::new(&config).await; 33 | 34 | let session = session::Manager::new(database_handle.clone()); 35 | let session_handle = session.handle(); 36 | handles.push(tokio::spawn(session.run())); 37 | 38 | tokio::spawn(javelin_srt::Service::new(session_handle.clone(), &config).run()); 39 | 40 | #[cfg(feature = "hls")] 41 | handles.push(tokio::spawn({ 42 | javelin_hls::Service::new(session_handle.clone(), &config).run() 43 | })); 44 | 45 | #[cfg(feature = "rtmp")] 46 | handles.push(tokio::spawn({ 47 | javelin_rtmp::Service::new(session_handle, &config).run() 48 | })); 49 | 50 | // Wait for all spawned processes to complete 51 | for handle in handles { 52 | handle.await?; 53 | } 54 | 55 | Ok(()) 56 | } 57 | 58 | 59 | fn init_tracing() -> Result<()> { 60 | use tracing::Level; 61 | use tracing_subscriber::filter::Targets; 62 | use tracing_subscriber::layer::SubscriberExt; 63 | use tracing_subscriber::util::SubscriberInitExt; 64 | 65 | let max_level = if cfg!(debug_assertions) { 66 | Level::TRACE 67 | } else { 68 | Level::INFO 69 | }; 70 | 71 | let filter_layer = Targets::new() 72 | .with_target("javelin", max_level) 73 | .with_target("javelin_rtmp", max_level) 74 | .with_target("javelin_srt", max_level) 75 | .with_target("javelin_hls", max_level) 76 | .with_target("javelin_core", max_level) 77 | .with_target("javelin_codec", max_level) 78 | .with_default(Level::ERROR); 79 | 80 | tracing_subscriber::registry() 81 | .with(tracing_subscriber::fmt::Layer::default()) 82 | .with(filter_layer) 83 | .try_init()?; 84 | 85 | Ok(()) 86 | } 87 | -------------------------------------------------------------------------------- /crates/javelin/src/database.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use javelin_core::Config; 3 | use javelin_types::async_trait; 4 | pub use javelin_types::models::{Error, User, UserRepository}; 5 | use tracing::{debug, trace}; 6 | 7 | 8 | type Pool = sqlx::SqlitePool; 9 | 10 | 11 | #[derive(Clone)] 12 | pub struct Database { 13 | pool: Pool, 14 | } 15 | 16 | 17 | impl Database { 18 | pub async fn new(config: &Config) -> Self { 19 | let path: String = config.get("database.sqlite.path").unwrap(); 20 | let pool = sqlx::SqlitePool::connect(&format!("sqlite:{path}")) 21 | .await 22 | .expect("Failed to connect to database"); 23 | 24 | Self { pool } 25 | } 26 | } 27 | 28 | #[async_trait] 29 | impl UserRepository for Database { 30 | async fn user_by_name(&self, name: &str) -> Result, Error> { 31 | trace!(%name, "Querying user"); 32 | let user = sqlx::query_as!(User, "SELECT name, key FROM users WHERE name = $1", name) 33 | .fetch_optional(&self.pool) 34 | .await 35 | .map_err(|_| Error::LookupFailed)?; 36 | trace!(?user); 37 | Ok(user) 38 | } 39 | 40 | async fn add_user_with_key(&mut self, name: &str, key: &str) -> Result<(), Error> { 41 | let query = match self.user_by_name(name).await? { 42 | Some(user) if user.key == key => { 43 | debug!("User with key already exists, no update required"); 44 | return Ok(()); 45 | } 46 | Some(_) => { 47 | debug!("Updating existing user"); 48 | sqlx::query!("UPDATE users SET key = $1 WHERE name = $2", key, name) 49 | } 50 | None => { 51 | debug!("Creating new user"); 52 | sqlx::query!("INSERT INTO users (name, key) VALUES ($1, $2)", name, key) 53 | } 54 | }; 55 | 56 | query 57 | .execute(&self.pool) 58 | .await 59 | .map_err(|_| Error::UpdateFailed)?; 60 | 61 | Ok(()) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /crates/javelin/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![warn(clippy::all)] 2 | 3 | pub mod database; 4 | -------------------------------------------------------------------------------- /deny.toml: -------------------------------------------------------------------------------- 1 | [graph] 2 | targets = [ 3 | "x86_64-unknown-linux-gnu" 4 | ] 5 | 6 | 7 | [sources] 8 | unknown-registry = "deny" 9 | unknown-git = "deny" 10 | 11 | [sources.allow-org] 12 | github = [ 13 | "valeth", 14 | ] 15 | 16 | 17 | [advisories] 18 | ignore = [] 19 | 20 | 21 | [licenses] 22 | private.ignore = true 23 | allow = [ 24 | "MIT", 25 | "Apache-2.0", 26 | "BSD-3-Clause", 27 | ] 28 | 29 | [[licenses.exceptions]] 30 | crate = "unicode-ident" 31 | allow = ["Unicode-DFS-2016"] 32 | 33 | 34 | [bans] 35 | multiple-versions = "deny" 36 | wildcards = "deny" 37 | allow-wildcard-paths = true 38 | external-default-features = "allow" # very noisy when set to "warn" 39 | workspace-default-features = "allow" 40 | 41 | [[bans.deny]] 42 | name = "openssl" 43 | 44 | [[bans.deny]] 45 | name = "bytes" 46 | version = "<1" 47 | 48 | [[bans.deny]] 49 | name = "tokio" 50 | version = "<1" 51 | 52 | 53 | [[bans.deny]] 54 | name = "futures" 55 | version = "<0.3" 56 | 57 | [[bans.deny]] 58 | name = "uuid" 59 | version = "<1" 60 | 61 | [[bans.skip]] 62 | name = "syn" 63 | version = "<2.0" 64 | reason = "some core crates still depend on syn version 1 (mostly sqlx)" 65 | 66 | [[bans.skip]] 67 | name = "sync_wrapper" 68 | version = "=0.1" 69 | reason = "axum depends on two different versions through tower" 70 | 71 | [[bans.skip]] 72 | name = "hashbrown" 73 | version = "=0.14" 74 | reason = "sqlx depends on two different versions" 75 | -------------------------------------------------------------------------------- /dist/Dockerfile: -------------------------------------------------------------------------------- 1 | # |-------<[ Build ]>-------| 2 | 3 | FROM rust:1.31-slim AS build 4 | 5 | ENV DEBIAN_FRONTEND=noninteractive 6 | RUN apt-get update \ 7 | && apt-get install -y --no-install-recommends \ 8 | ca-certificates \ 9 | libssl-dev \ 10 | pkg-config 11 | 12 | RUN mkdir -p /build/out 13 | WORKDIR /build 14 | 15 | COPY ./ ./ 16 | RUN cargo build --release \ 17 | && cp target/release/javelin ./out 18 | 19 | 20 | # |-------<[ App ]>-------| 21 | 22 | FROM rust:1.31-slim 23 | 24 | LABEL maintainer="dev.patrick.auernig@gmail.com" 25 | 26 | ENV DEBIAN_FRONTEND=noninteractive 27 | RUN apt-get update \ 28 | && apt-get install -y --no-install-recommends \ 29 | ca-certificates 30 | 31 | RUN mkdir -p /var/data /app/config 32 | WORKDIR /app 33 | 34 | COPY --from=build /build/out/javelin ./javelin 35 | 36 | EXPOSE 1935 8080 37 | ENTRYPOINT ["/app/javelin", "--hls-root=/var/data"] 38 | -------------------------------------------------------------------------------- /dist/compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | 3 | volumes: 4 | hls_data: 5 | mongodb0: 6 | 7 | services: 8 | javelin: 9 | image: registry.gitlab.com/valeth/javelin:develop 10 | build: . 11 | command: 12 | - "--permit-stream-key=123456" 13 | ports: 14 | - "1935:1935" 15 | - "8080:8080" 16 | volumes: 17 | - "hls_data:/var/data" 18 | - "./config:/app/config" 19 | 20 | -------------------------------------------------------------------------------- /docs/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 | 9 | ## [Unreleased] 10 | 11 | ### Changed 12 | - Project is split into sub-crates. 13 | - Now using fern as the logging backend. 14 | - RTMP and RTMPS can now run simultaneously. 15 | 16 | ### Fixed 17 | - Prevent session deadlock by timing out idle RTMP connections. 18 | 19 | ### Removed 20 | - All module specific CLI flags. 21 | - Ability to set stream key via CLI arguments or configuration file. Now uses the database instead. 22 | 23 | 24 | --- 25 | 26 | ## [0.3.6] 27 | 28 | ### Added 29 | - Permitted stream keys can now be set via configuration file. 30 | 31 | --- 32 | 33 | ## [0.3.5] 34 | 35 | ### Fixed 36 | - Composition time is now included in PES presentation timestamp. 37 | - PES now also has a decoding timestamp set. 38 | 39 | --- 40 | 41 | ## [0.3.4] 42 | 43 | ### Added 44 | - Configuration option for integrated web server address. 45 | - Compile time and command line option to disable HLS support. 46 | - Multiple methods to handle re-publishing to same application. 47 | - Stream statistics API endpoint. 48 | 49 | ### Changed 50 | - Moved codec related code into sub-crate. 51 | 52 | --- 53 | 54 | ## [0.3.3] 55 | 56 | ### Fixed 57 | - HLS directory will now be completely cleared only on startup. 58 | - The "web" feature now includes the "hls" feature set. 59 | 60 | --- 61 | 62 | ## [0.3.2] 63 | 64 | ### Added 65 | - Internal HTTP file server with simple JSON API (currently just active streams). 66 | 67 | ### Changed 68 | - File cleanup is no longer done in batches. 69 | - TLS support is disabled by default. 70 | - HLS directory is now cleared every time on stream publish start. 71 | 72 | --- 73 | 74 | ## [0.3.1] 75 | 76 | ### Changed 77 | - Just some minor tweaks. 78 | 79 | --- 80 | 81 | ## [0.3.0] 82 | 83 | ### Added 84 | - Optional support for HLS streaming output. 85 | 86 | --- 87 | 88 | ## [0.2.3] 89 | 90 | ### Added 91 | - Allow limitation of permitted stream keys with `--permit-stream-key=` flag. 92 | 93 | ### Fixed 94 | - Help text should now be always up-to-date with `Cargo.toml`. 95 | 96 | --- 97 | 98 | ## [0.2.2] 99 | 100 | ### Fixed 101 | - Just minor manifest formatting fixes, nothing important. 102 | 103 | --- 104 | 105 | ## [0.2.1] 106 | 107 | ### Fixed 108 | - No longer requires a password if running with `--no-tls` flag. 109 | 110 | --- 111 | 112 | ## [0.2.0] 113 | 114 | ### Added 115 | - Optional TLS support is now available. 116 | 117 | --- 118 | 119 | ## [0.1.1] 120 | 121 | ### Fixed 122 | - Publishing clients should no longer linger forever. 123 | 124 | --- 125 | 126 | ## [0.1.0] 127 | 128 | ### Added 129 | - Required event handlers to make the protocol work. 130 | - Dockerfile and image for easier setup. 131 | 132 | 133 | 134 | 135 | [Unreleased]: https://gitlab.com/valeth/javelin/tree/develop 136 | [0.3.6]: https://gitlab.com/valeth/javelin/tree/0.3.6 137 | [0.3.5]: https://gitlab.com/valeth/javelin/tree/0.3.5 138 | [0.3.4]: https://gitlab.com/valeth/javelin/tree/0.3.4 139 | [0.3.3]: https://gitlab.com/valeth/javelin/tree/0.3.3 140 | [0.3.2]: https://gitlab.com/valeth/javelin/tree/0.3.2 141 | [0.3.1]: https://gitlab.com/valeth/javelin/tree/0.3.1 142 | [0.3.0]: https://gitlab.com/valeth/javelin/tree/0.3.0 143 | [0.2.3]: https://gitlab.com/valeth/javelin/tree/0.2.3 144 | [0.2.2]: https://gitlab.com/valeth/javelin/tree/0.2.2 145 | [0.2.1]: https://gitlab.com/valeth/javelin/tree/0.2.1 146 | [0.2.0]: https://gitlab.com/valeth/javelin/tree/0.2.0 147 | [0.1.1]: https://gitlab.com/valeth/javelin/tree/0.1.1 148 | [0.1.0]: https://gitlab.com/valeth/javelin/tree/0.1.0 149 | -------------------------------------------------------------------------------- /docs/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributions 2 | 3 | ## Basic Workflow 4 | 5 | 1. Fork the project 6 | 2. Create a new branch in your fork 7 | 3. Make some changes 8 | 4. Send a Merge Request to the upstream repository 9 | 1. Periodically rebase your branch on top of the main branch until it is ready to be merged 10 | 11 | 12 | ## Commit Messages 13 | 14 | This project follows the [Conventional Commits] spec to make commits easier to search 15 | through, organize, and generate changelogs. 16 | 17 | 18 | ## Code of Conduct 19 | 20 | We mostly follow the [Rust Project CoC]. 21 | But in general just use some common sense, and be decent to each other. 22 | 23 | 24 | 25 | 26 | [Conventional Commits]: https://www.conventionalcommits.org/en/v1.0.0 27 | [Rust Project CoC]: https://www.rust-lang.org/policies/code-of-conduct 28 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "nixpkgs": { 4 | "locked": { 5 | "lastModified": 1728979988, 6 | "narHash": "sha256-GBJRnbFLDg0y7ridWJHAP4Nn7oss50/VNgqoXaf/RVk=", 7 | "owner": "nixos", 8 | "repo": "nixpkgs", 9 | "rev": "7881fbfd2e3ed1dfa315fca889b2cfd94be39337", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "nixos", 14 | "ref": "nixpkgs-unstable", 15 | "repo": "nixpkgs", 16 | "type": "github" 17 | } 18 | }, 19 | "root": { 20 | "inputs": { 21 | "nixpkgs": "nixpkgs", 22 | "rust-overlay": "rust-overlay" 23 | } 24 | }, 25 | "rust-overlay": { 26 | "inputs": { 27 | "nixpkgs": [ 28 | "nixpkgs" 29 | ] 30 | }, 31 | "locked": { 32 | "lastModified": 1729184663, 33 | "narHash": "sha256-uNyi5vQrzaLkt4jj6ZEOs4+4UqOAwP6jFG2s7LIDwIk=", 34 | "owner": "oxalica", 35 | "repo": "rust-overlay", 36 | "rev": "16fb78d443c1970dda9a0bbb93070c9d8598a925", 37 | "type": "github" 38 | }, 39 | "original": { 40 | "owner": "oxalica", 41 | "repo": "rust-overlay", 42 | "type": "github" 43 | } 44 | } 45 | }, 46 | "root": "root", 47 | "version": 7 48 | } 49 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Javelin development environment"; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; 6 | rust-overlay = { 7 | url = "github:oxalica/rust-overlay"; 8 | inputs.nixpkgs.follows = "nixpkgs"; 9 | }; 10 | }; 11 | 12 | outputs = { nixpkgs, rust-overlay, ... }: 13 | let 14 | lib = nixpkgs.lib; 15 | systems = [ "x86_64-linux" ]; 16 | 17 | forEachSystem = fn: lib.genAttrs systems (system: 18 | let 19 | overlays = [ (import rust-overlay) ]; 20 | pkgs = import nixpkgs { inherit system overlays; }; 21 | in fn { inherit pkgs; } ); 22 | 23 | mkRustToolchain = pkgs: pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml; 24 | in { 25 | devShells = forEachSystem ({ pkgs }: 26 | let 27 | rustToolchain = mkRustToolchain pkgs; 28 | rustNightlyToolchain = pkgs.rust-bin.selectLatestNightlyWith (t: t.minimal.override { 29 | extensions = [ "rustfmt" ]; 30 | }); 31 | in { 32 | default = pkgs.mkShell { 33 | name = "javelin"; 34 | 35 | nativeBuildInputs = [ 36 | rustToolchain 37 | rustNightlyToolchain 38 | ]; 39 | 40 | packages = with pkgs; [ 41 | cargo-deny 42 | sqlx-cli 43 | ]; 44 | }; 45 | }); 46 | }; 47 | } 48 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.82.0" 3 | components = ["clippy", "rust-analyzer", "rust-src"] 4 | targets = ["x86_64-unknown-linux-gnu"] 5 | profile = "minimal" # includes rustc, cargo, and rust-std 6 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2021" 2 | 3 | # Just enforce it here as well instead of relying on editorconfig alone 4 | hard_tabs = false 5 | tab_spaces = 4 6 | newline_style = "Unix" 7 | 8 | imports_granularity = "Module" 9 | group_imports = "StdExternalCrate" 10 | 11 | force_multiline_blocks = false 12 | fn_single_line = false 13 | comment_width = 100 14 | wrap_comments = true 15 | hex_literal_case = "Upper" 16 | blank_lines_upper_bound = 2 17 | overflow_delimited_expr = true 18 | reorder_impl_items = true 19 | -------------------------------------------------------------------------------- /tools/docker: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "childprocess" 4 | require "toml" 5 | 6 | ARGV.first&.split(".") || [] 7 | 8 | class String 9 | def int? 10 | /^\d+$/.match?(self) 11 | end 12 | end 13 | 14 | DockerError = Class.new(StandardError) 15 | 16 | class InvalidVersion < StandardError 17 | def initialize 18 | super("Requires version of format x.y.z") 19 | end 20 | end 21 | 22 | def validate_version(version) 23 | semver = version&.split(".") 24 | raise InvalidVersion unless semver&.size == 3 25 | raise InvalidVersion unless semver&.all?(&:int?) 26 | return semver.map.with_index { |v, i| semver[0..i].join(".") } 27 | end 28 | 29 | def docker(*args) 30 | process = ChildProcess.build("docker", *args).tap do |p| 31 | p.io.inherit! 32 | p.start 33 | p.wait 34 | end 35 | 36 | raise DockerError unless process.exit_code.zero? 37 | end 38 | 39 | def docker_build(image_name, versions) 40 | tags = versions.flat_map { |v| ["-t", "#{image_name}:#{v}"] } 41 | puts "Building #{versions.size} images..." 42 | docker "build", *tags, "." 43 | end 44 | 45 | def docker_push(image_name, versions) 46 | puts "Pushing #{versions.size} images to #{image_name}" 47 | versions.each do |version| 48 | docker "push", "#{image_name}:#{version}" 49 | end 50 | end 51 | 52 | version = TOML.load_file("Cargo.toml").dig("package", "version") 53 | image_name = "registry.gitlab.com/valeth/javelin" 54 | 55 | begin 56 | versions = ["latest", *validate_version(version)] 57 | docker_build(image_name, versions) 58 | docker_push(image_name, versions) 59 | rescue InvalidVersion, DockerError => e 60 | warn e.message 61 | exit 1 62 | end 63 | 64 | --------------------------------------------------------------------------------