From 65ffc9e80940e3cc36e047b1ba658c1ced437bd4 Mon Sep 17 00:00:00 2001 From: Sarah Date: Tue, 10 Jun 2025 12:01:12 +0100 Subject: [PATCH] Merge branch `feat/tally` into main --- Cargo.lock | 422 +++++++++--- atem-connection-rs/Cargo.toml | 2 +- atem-connection-rs/src/atem.rs | 202 +++++- .../src/atem_lib/atem_packet.rs | 62 +- .../src/atem_lib/atem_socket.rs | 607 ++++++++++++++++-- .../src/atem_lib/atem_socket_inner.rs | 474 -------------- atem-connection-rs/src/atem_lib/mod.rs | 1 - .../src/commands/command_base.rs | 53 +- .../src/commands/device_profile.rs | 7 + .../device_profile/audio_mixer_config.rs | 47 ++ .../device_profile/media_pool_config.rs | 40 ++ .../device_profile/mix_effect_block_config.rs | 40 ++ .../device_profile/multiviewer_config.rs | 58 ++ .../device_profile/product_identifier.rs | 68 ++ .../src/commands/device_profile/topology.rs | 117 ++++ .../src/commands/device_profile/version.rs | 25 + .../src/commands/init_complete.rs | 31 + .../src/commands/mix_effects.rs | 28 +- .../src/commands/mix_effects/program_input.rs | 72 +++ atem-connection-rs/src/commands/mod.rs | 5 + .../src/commands/parse_commands.rs | 88 +++ .../src/commands/tally_by_source.rs | 62 ++ atem-connection-rs/src/commands/time.rs | 50 ++ atem-connection-rs/src/enums/mod.rs | 128 +++- atem-connection-rs/src/lib.rs | 4 +- atem-connection-rs/src/state/atem_macro.rs | 8 +- atem-connection-rs/src/state/audio.rs | 32 +- atem-connection-rs/src/state/color.rs | 4 +- atem-connection-rs/src/state/common.rs | 2 +- atem-connection-rs/src/state/fairlight.rs | 65 +- atem-connection-rs/src/state/info.rs | 62 +- atem-connection-rs/src/state/input.rs | 4 +- atem-connection-rs/src/state/media.rs | 10 +- atem-connection-rs/src/state/mod.rs | 24 +- atem-connection-rs/src/state/recording.rs | 16 +- atem-connection-rs/src/state/settings.rs | 8 +- atem-connection-rs/src/state/streaming.rs | 15 +- atem-connection-rs/src/state/util.rs | 34 +- .../src/state/video/downstream_keyers.rs | 8 +- atem-connection-rs/src/state/video/mod.rs | 34 +- .../src/state/video/super_source.rs | 8 +- .../src/state/video/upstream_keyers.rs | 409 +----------- atem-connection-rs/src/tally.rs | 13 + atem-test/Cargo.toml | 3 + atem-test/src/main.rs | 87 ++- flake.lock | 128 +--- flake.nix | 2 + 47 files changed, 2309 insertions(+), 1360 deletions(-) delete mode 100644 atem-connection-rs/src/atem_lib/atem_socket_inner.rs create mode 100644 atem-connection-rs/src/commands/device_profile.rs create mode 100644 atem-connection-rs/src/commands/device_profile/audio_mixer_config.rs create mode 100644 atem-connection-rs/src/commands/device_profile/media_pool_config.rs create mode 100644 atem-connection-rs/src/commands/device_profile/mix_effect_block_config.rs create mode 100644 atem-connection-rs/src/commands/device_profile/multiviewer_config.rs create mode 100644 atem-connection-rs/src/commands/device_profile/product_identifier.rs create mode 100644 atem-connection-rs/src/commands/device_profile/topology.rs create mode 100644 atem-connection-rs/src/commands/device_profile/version.rs create mode 100644 atem-connection-rs/src/commands/init_complete.rs create mode 100644 atem-connection-rs/src/commands/mix_effects/program_input.rs create mode 100644 atem-connection-rs/src/commands/parse_commands.rs create mode 100644 atem-connection-rs/src/commands/tally_by_source.rs create mode 100644 atem-connection-rs/src/commands/time.rs create mode 100644 atem-connection-rs/src/tally.rs diff --git a/Cargo.lock b/Cargo.lock index 2078298..4422846 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,6 +26,54 @@ dependencies = [ "memchr", ] +[[package]] +name = "anstream" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e1ebcb11de5c03c67de28a7df593d32191b44939c482e97702baaaa6ab6a5" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" + +[[package]] +name = "anstyle-parse" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] + [[package]] name = "atem-connection-rs" version = "0.1.0" @@ -33,8 +81,8 @@ dependencies = [ "derive-getters", "derive-new", "log", - "thiserror", "tokio", + "tokio-util", ] [[package]] @@ -42,9 +90,12 @@ name = "atem-test" version = "0.1.0" dependencies = [ "atem-connection-rs", + "clap", "color-eyre", "env_logger", + "log", "tokio", + "tokio-util", ] [[package]] @@ -53,7 +104,7 @@ version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ - "hermit-abi", + "hermit-abi 0.1.19", "libc", "winapi", ] @@ -87,9 +138,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bytes" -version = "1.1.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" [[package]] name = "cc" @@ -103,6 +154,46 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "clap" +version = "4.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e578d6ec4194633722ccf9544794b71b1385c3c027efe0c55db226fc880865c" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4df4df40ec50c46000231c914968278b1eb05098cf8f1b3a518a95030e71d1c7" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "clap_lex" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" + [[package]] name = "color-eyre" version = "0.5.11" @@ -130,6 +221,12 @@ dependencies = [ "tracing-error", ] +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + [[package]] name = "derive-getters" version = "0.2.0" @@ -175,12 +272,30 @@ dependencies = [ "once_cell", ] +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + [[package]] name = "gimli" version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0a01e0497841a3b2db4f8afa483cce65f7e96a3498bd6c541734792aeac8fe7" +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "hermit-abi" version = "0.1.19" @@ -190,6 +305,12 @@ dependencies = [ "libc", ] +[[package]] +name = "hermit-abi" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "379dada1584ad501b383485dd706b8afb7a70fcbc7f4da7d780638a5a6124a60" + [[package]] name = "humantime" version = "2.1.0" @@ -202,15 +323,6 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" -[[package]] -name = "instant" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" -dependencies = [ - "cfg-if", -] - [[package]] name = "lazy_static" version = "1.4.0" @@ -219,15 +331,15 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.107" +version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbe5e23404da5b4f555ef85ebed98fb4083e55a00c317800bc2a50ede9f3d219" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "lock_api" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109" +checksum = "88943dd7ef4a2e5a4bfa2753aaab3013e34ce2533d1996fb18ef591e315e2b3b" dependencies = [ "scopeguard", ] @@ -259,42 +371,22 @@ dependencies = [ [[package]] name = "mio" -version = "0.7.14" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc" +checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" dependencies = [ "libc", - "log", - "miow", - "ntapi", - "winapi", -] - -[[package]] -name = "miow" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" -dependencies = [ - "winapi", -] - -[[package]] -name = "ntapi" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44" -dependencies = [ - "winapi", + "wasi", + "windows-sys 0.48.0", ] [[package]] name = "num_cpus" -version = "1.13.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "hermit-abi", + "hermit-abi 0.3.8", "libc", ] @@ -321,34 +413,32 @@ checksum = "2386b4ebe91c2f7f51082d4cefa145d030e33a1842a96b12e4885cc3c01f7a55" [[package]] name = "parking_lot" -version = "0.11.2" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ - "instant", "lock_api", "parking_lot_core", ] [[package]] name = "parking_lot_core" -version = "0.8.5" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" dependencies = [ "cfg-if", - "instant", "libc", "redox_syscall", "smallvec", - "winapi", + "windows-targets 0.48.5", ] [[package]] name = "pin-project-lite" -version = "0.2.7" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" [[package]] name = "proc-macro2" @@ -370,9 +460,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.2.10" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" dependencies = [ "bitflags", ] @@ -402,9 +492,9 @@ checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" [[package]] name = "scopeguard" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "sharded-slab" @@ -417,18 +507,34 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" dependencies = [ "libc", ] [[package]] name = "smallvec" -version = "1.7.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309" +checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" + +[[package]] +name = "socket2" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "syn" @@ -461,26 +567,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "thiserror" -version = "1.0.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.74", -] - [[package]] name = "thread_local" version = "1.1.3" @@ -492,33 +578,45 @@ dependencies = [ [[package]] name = "tokio" -version = "1.14.0" +version = "1.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e992e41e0d2fb9f755b37446f20900f64446ef54874f40a60c78f021ac6144" +checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931" dependencies = [ - "autocfg", + "backtrace", "bytes", "libc", - "memchr", "mio", "num_cpus", - "once_cell", "parking_lot", "pin-project-lite", "signal-hook-registry", + "socket2", "tokio-macros", - "winapi", + "windows-sys 0.48.0", ] [[package]] name = "tokio-macros" -version = "1.6.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9efc1aba077437943f7515666aa2b882dfabfbfdf89c819ea75a8d6e9eaba5e" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 1.0.74", + "syn 2.0.48", +] + +[[package]] +name = "tokio-util" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", ] [[package]] @@ -586,6 +684,18 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + [[package]] name = "winapi" version = "0.3.9" @@ -616,3 +726,135 @@ name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" diff --git a/atem-connection-rs/Cargo.toml b/atem-connection-rs/Cargo.toml index 73ee78b..58dc8eb 100644 --- a/atem-connection-rs/Cargo.toml +++ b/atem-connection-rs/Cargo.toml @@ -7,5 +7,5 @@ edition = "2021" derive-getters = "0.2.0" derive-new = "0.6.0" log = "0.4.14" -thiserror = "1.0.30" tokio = { version = "1.13.0", features = ["full"] } +tokio-util = "0.7.10" diff --git a/atem-connection-rs/src/atem.rs b/atem-connection-rs/src/atem.rs index 07e00aa..bbfd9a0 100644 --- a/atem-connection-rs/src/atem.rs +++ b/atem-connection-rs/src/atem.rs @@ -1,19 +1,191 @@ -use crate::{commands::command_base::DeserializedCommand, state::AtemState}; +use std::{ + collections::{HashMap, VecDeque}, + net::SocketAddr, + ops::DerefMut, + sync::Arc, + time::Duration, +}; -pub struct AtemOptions { - address: Option, - port: Option, - debug_buffers: bool, - disable_multi_threaded: bool, - child_process_timeout: Option, -} +use tokio::{select, sync::Semaphore}; +use tokio_util::sync::CancellationToken; -pub enum AtemEvents { - Error(String), - Info(String), - Debug(String), +use crate::{ + atem_lib::atem_socket::{ + AtemSocket, AtemSocketCommand, AtemSocketEvent, AtemSocketMessage, TrackingId, + }, + commands::{ + command_base::{BasicWritableCommand, DeserializedCommand}, + device_profile::version::DESERIALIZE_VERSION_RAW_NAME, + init_complete::DESERIALIZE_INIT_COMPLETE_RAW_NAME, + parse_commands::deserialize_commands, + time::DESERIALIZE_TIME_RAW_NAME, + }, + enums::ProtocolVersion, + state::AtemState, +}; + +#[derive(Default)] +pub enum AtemConnectionStatus { + #[default] + Closed, + Connecting, Connected, - Disconnected, - StateChanged(Box<(AtemState, Vec)>), - ReceivedCommands(Vec>), +} + +pub struct Atem { + protocol_version: tokio::sync::RwLock, + + socket: tokio::sync::RwLock, + + waiting_semaphores: tokio::sync::RwLock>>, + socket_message_tx: tokio::sync::mpsc::Sender, +} + +impl Atem { + pub fn new( + socket: AtemSocket, + socket_message_tx: tokio::sync::mpsc::Sender, + ) -> Self { + Self { + protocol_version: tokio::sync::RwLock::new(ProtocolVersion::V7_2), + + socket: tokio::sync::RwLock::new(socket), + + waiting_semaphores: tokio::sync::RwLock::new(HashMap::new()), + socket_message_tx, + } + } + + pub async fn connect(&self, address: SocketAddr) -> bool { + let (callback_tx, callback_rx) = tokio::sync::oneshot::channel(); + self.socket_message_tx + .send(AtemSocketMessage::Connect { + address, + result_callback: callback_tx, + }) + .await + .unwrap(); + + callback_rx.await.unwrap() + } + + pub async fn run( + &self, + mut atem_event_rx: tokio::sync::mpsc::UnboundedReceiver, + cancel: CancellationToken, + ) { + let mut status = AtemConnectionStatus::default(); + let mut state = AtemState::default(); + + let mut poll_interval = tokio::time::interval(Duration::from_millis(5)); + + while !cancel.is_cancelled() { + let tick = poll_interval.tick(); + select! { + _ = cancel.cancelled() => {}, + _ = tick => {}, + message = atem_event_rx.recv() => match message { + Some(event) => match event { + AtemSocketEvent::Connected => { + log::info!("Atem connected"); + } + AtemSocketEvent::Disconnected => todo!("Disconnected"), + AtemSocketEvent::ReceivedCommands(payload) => { + let commands = deserialize_commands(&payload, self.protocol_version.write().await.deref_mut()); + self.mutate_state(&mut state, &mut status, commands).await + } + AtemSocketEvent::AckedCommand(tracking_id) => { + log::debug!("Received tracking Id {tracking_id}"); + if let Some(semaphore) = + self.waiting_semaphores.read().await.get(&tracking_id) + { + semaphore.add_permits(1); + } else { + log::warn!("Received tracking Id {tracking_id} with no-one waiting for it to be resolved.") + } + } + }, + None => { + log::info!("ATEM event channel has closed, exiting event loop."); + cancel.cancel(); + } + } + } + + self.socket.write().await.poll().await; + } + } + + pub async fn send_commands(&self, commands: Vec>) { + let protocol_version = { self.protocol_version.read().await.clone() }; + let (callback_tx, callback_rx) = tokio::sync::oneshot::channel(); + self.socket_message_tx + .send(AtemSocketMessage::SendCommands { + commands: commands + .iter() + .map(|command| AtemSocketCommand::new(command, &protocol_version)) + .collect(), + tracking_ids_callback: callback_tx, + }) + .await + .unwrap(); + let callback = callback_rx.await.unwrap(); + + let semaphore = Arc::new(Semaphore::new(0)); + + for tracking_id in callback.tracking_ids.iter() { + self.waiting_semaphores + .write() + .await + .insert(tracking_id.clone(), semaphore.clone()); + } + + callback.barrier.wait().await; + + // If this fails then the semaphore has been closed which is a darn shame but at that point + // the best we can do it continue on our merry way in life and remain oblivious to + // the fire raging in other realms. + semaphore + .acquire_many(callback.tracking_ids.len() as u32) + .await + .ok(); + + for tracking_id in callback.tracking_ids.iter() { + self.waiting_semaphores.write().await.remove(tracking_id); + } + } + + async fn mutate_state( + &self, + state: &mut AtemState, + status: &mut AtemConnectionStatus, + commands: VecDeque>, + ) { + let new_state = state.clone(); + for command in commands { + match command.raw_name() { + DESERIALIZE_VERSION_RAW_NAME => { + log::debug!("Received version response"); + *state = AtemState::default(); + *status = AtemConnectionStatus::Connecting + } + DESERIALIZE_INIT_COMPLETE_RAW_NAME => { + log::debug!("Received init complete from ATEM"); + *status = AtemConnectionStatus::Connected + } + DESERIALIZE_TIME_RAW_NAME => { + todo!("Time command") + } + _ => { + log::debug!("Applying {} to state", command.raw_name()); + command.apply_to_state(state); + } + } + } + + if new_state != *state { + *state = new_state; + todo!("Emit change"); + } + } } diff --git a/atem-connection-rs/src/atem_lib/atem_packet.rs b/atem-connection-rs/src/atem_lib/atem_packet.rs index 18a9b02..deeb5b9 100755 --- a/atem-connection-rs/src/atem_lib/atem_packet.rs +++ b/atem-connection-rs/src/atem_lib/atem_packet.rs @@ -1,9 +1,11 @@ -pub struct AtemPacket { - length: u16, +#[derive(Debug)] +pub struct AtemPacket<'packet_buffer> { flags: u8, session_id: u16, remote_packet_id: u16, - body: Vec, + retransmit_requested_from_packet_id: Option, + ack_reply: Option, + body: Option<&'packet_buffer [u8]>, } pub enum AtemPacketErr { @@ -32,15 +34,7 @@ impl From for u8 { } } -impl AtemPacket { - pub fn length(&self) -> u16 { - self.length - } - - pub fn flags(&self) -> u8 { - self.flags - } - +impl<'packet_buffer> AtemPacket<'packet_buffer> { pub fn session_id(&self) -> u16 { self.session_id } @@ -49,8 +43,16 @@ impl AtemPacket { self.remote_packet_id } - pub fn body(&self) -> Vec { - self.body.clone() + pub fn body(&self) -> Option<&[u8]> { + self.body + } + + pub fn retransmit_request(&self) -> Option { + self.retransmit_requested_from_packet_id + } + + pub fn ack_reply(&self) -> Option { + self.ack_reply } pub fn has_flag(&self, flag: PacketFlag) -> bool { @@ -58,10 +60,10 @@ impl AtemPacket { } } -impl TryFrom<&[u8]> for AtemPacket { +impl<'packet_buffer> TryFrom<&'packet_buffer [u8]> for AtemPacket<'packet_buffer> { type Error = AtemPacketErr; - fn try_from(buffer: &[u8]) -> Result { + fn try_from(buffer: &'packet_buffer [u8]) -> Result { if buffer.len() < 12 { return Err(AtemPacketErr::TooShort(format!( "Invalid packet from ATEM {:x?}", @@ -69,7 +71,7 @@ impl TryFrom<&[u8]> for AtemPacket { ))); } - let length = u16::from_be_bytes(buffer[0..2].try_into().unwrap()) & 0x07ff; + let length = u16::from_be_bytes([buffer[0], buffer[1]]) & 0x07ff; if length as usize != buffer.len() { return Err(AtemPacketErr::LengthDiffers(format!( "Length of message differs, expected {} got {}", @@ -79,17 +81,35 @@ impl TryFrom<&[u8]> for AtemPacket { } let flags = buffer[0] >> 3; - let session_id = u16::from_be_bytes(buffer[2..4].try_into().unwrap()); - let remote_packet_id = u16::from_be_bytes(buffer[10..12].try_into().unwrap()); + let session_id = u16::from_be_bytes([buffer[2], buffer[3]]); + let remote_packet_id = u16::from_be_bytes([buffer[10], buffer[11]]); - let body = buffer[12..].to_vec(); + let body = if buffer.len() > 12 { + Some(&buffer[12..]) + } else { + None + }; + + let retransmit_requested_from_packet_id = + if flags & u8::from(PacketFlag::RetransmitRequest) > 0 { + Some(u16::from_be_bytes([buffer[6], buffer[7]])) + } else { + None + }; + + let ack_reply = if flags & u8::from(PacketFlag::AckReply) > 0 { + Some(u16::from_be_bytes([buffer[4], buffer[5]])) + } else { + None + }; Ok(AtemPacket { - length, flags, session_id, remote_packet_id, body, + retransmit_requested_from_packet_id, + ack_reply, }) } } diff --git a/atem-connection-rs/src/atem_lib/atem_socket.rs b/atem-connection-rs/src/atem_lib/atem_socket.rs index e1b5d4e..4945b54 100644 --- a/atem-connection-rs/src/atem_lib/atem_socket.rs +++ b/atem-connection-rs/src/atem_lib/atem_socket.rs @@ -1,68 +1,591 @@ -use std::{io, sync::Arc, thread::yield_now}; +use std::{ + collections::VecDeque, + fmt::Display, + io, + net::SocketAddr, + sync::Arc, + time::{Duration, SystemTime}, +}; -use tokio::{sync::RwLock, task::JoinHandle}; +use tokio::{ + net::UdpSocket, + select, + sync::{Barrier, Mutex}, +}; -use super::atem_socket_inner::AtemSocketInner; +use crate::{ + atem_lib::{atem_packet::AtemPacket, atem_util}, + commands::{ + command_base::{BasicWritableCommand, DeserializedCommand}, + parse_commands::deserialize_commands, + }, + enums::ProtocolVersion, +}; -pub struct AtemSocket { - socket: Arc>, +use super::atem_packet::PacketFlag; - inner_socket_handle: JoinHandle<()>, +const IN_FLIGHT_TIMEOUT: u64 = 60; +const CONNECTION_TIMEOUT: u64 = 5000; +const CONNECTION_RETRY_INTERVAL: u64 = 1000; +const RETRANSMIT_CHECK_INTERVAL: u64 = 1000; +const MAX_PACKET_RETRIES: u16 = 10; +const MAX_PACKET_ID: u16 = 1 << 15; +const MAX_PACKET_PER_ACK: u16 = 16; + +// Set to max UDP packet size, for now +const MAX_PACKET_RECEIVE_SIZE: usize = 65535; +const ACK_PACKET_LENGTH: u16 = 12; + +pub enum AtemSocketMessage { + Connect { + address: SocketAddr, + result_callback: tokio::sync::oneshot::Sender, + }, + Disconnect, + SendCommands { + commands: Vec, + tracking_ids_callback: tokio::sync::oneshot::Sender, + }, } -#[derive(Debug, Error)] -pub enum AtemSocketConnectionError { - #[error("Socket connection error")] - IoError(#[from] io::Error), +pub struct TrackingIdsCallback { + pub tracking_ids: Vec, + pub barrier: Arc, +} + +#[derive(Clone)] +pub enum AtemSocketEvent { + Connected, + Disconnected, + ReceivedCommands(Vec), + AckedCommand(TrackingId), +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct TrackingId(u64); + +impl Display for TrackingId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +#[derive(Clone)] +pub struct AckedPacket { + pub packet_id: u16, + pub tracking_id: u64, +} + +pub struct AtemSocketCommand { + payload: Vec, + raw_name: String, +} + +impl AtemSocketCommand { + pub fn new(command: &C, version: &ProtocolVersion) -> Self { + Self { + payload: command.payload(version), + raw_name: command.get_raw_name().to_string(), + } + } +} + +pub struct AtemSocket { + connection_state: ConnectionState, + reconnect_timer: Option, + retransmit_timer: Option, + + next_tracking_id: u64, + + next_send_packet_id: u16, + session_id: u16, + + socket: Option, + address: SocketAddr, + + protocol_version: ProtocolVersion, + + last_received_at: SystemTime, + last_received_packed_id: u16, + in_flight: Vec, + ack_timer: Option, + received_without_ack: u16, + + atem_message_rx: tokio::sync::mpsc::Receiver, + atem_event_tx: tokio::sync::mpsc::UnboundedSender, + connected_callbacks: Mutex>>, + + tick_interval: tokio::time::Interval, +} + +#[derive(PartialEq, Clone)] +enum ConnectionState { + Closed, + SynSent, + Established, +} + +#[allow(clippy::from_over_into)] +impl Into for ConnectionState { + fn into(self) -> u8 { + match self { + ConnectionState::Closed => 0x00, + ConnectionState::SynSent => 0x01, + ConnectionState::Established => 0x02, + } + } +} + +#[derive(Clone)] +struct InFlightPacket { + packet_id: u16, + tracking_id: u64, + payload: Vec, + pub last_sent: SystemTime, + pub resent: u16, +} + +enum AtemSocketReceiveError { + Closed, } impl AtemSocket { - pub fn new() -> Self { - let socket = AtemSocketInner::new(); - let socket = Arc::new(RwLock::new(socket)); + pub fn new( + atem_message_rx: tokio::sync::mpsc::Receiver, + atem_event_tx: tokio::sync::mpsc::UnboundedSender, + ) -> Self { + let tick_interval = tokio::time::interval(Duration::from_millis(5)); + Self { + connection_state: ConnectionState::Closed, + reconnect_timer: None, + retransmit_timer: None, - let socket_clone = Arc::clone(&socket); - let handle = tokio::spawn(async move { - loop { - socket_clone.write().await.tick().await; + next_tracking_id: 0, - yield_now(); - } - }); + next_send_packet_id: 1, + session_id: 0, - AtemSocket { - socket, + socket: None, + address: "0.0.0.0:0".parse().unwrap(), - inner_socket_handle: handle, + protocol_version: ProtocolVersion::V7_2, + + last_received_at: SystemTime::now(), + last_received_packed_id: 0, + in_flight: vec![], + ack_timer: None, + received_without_ack: 0, + + atem_message_rx, + atem_event_tx, + connected_callbacks: Mutex::default(), + + tick_interval, } } - pub async fn connect( - &mut self, - address: String, - port: u16, - ) -> Result<(), AtemSocketConnectionError> { - self.socket.write().await.connect(address, port).await?; + pub async fn poll(&mut self) { + let tick = self.tick_interval.tick(); + select! { + _ = tick => {}, + message = self.atem_message_rx.recv() => { + match message { + Some(AtemSocketMessage::Connect { + address, + result_callback, + }) => { + { + let mut connected_callbacks = self.connected_callbacks.lock().await; + connected_callbacks.push(result_callback); + } + if self.connect(address).await.is_err() { + log::debug!("Connect failed"); + let mut connected_callbacks = self.connected_callbacks.lock().await; + for callback in connected_callbacks.drain(0..) { + let _ = callback.send(false); + } + } + } + Some(AtemSocketMessage::Disconnect) => self.disconnect(), + Some(AtemSocketMessage::SendCommands { + commands, + tracking_ids_callback, + }) => { + let barrier = Arc::new(Barrier::new(2)); + tracking_ids_callback + .send(TrackingIdsCallback { + tracking_ids: self.send_commands(commands).await, + barrier: barrier.clone(), + }) + .ok(); + + // Let's play the game "Synchronisation Shenanigans"! + // So, we are sending tracking Ids to the sender of this message, the sender will then wait + // for each of these tracking Ids to be ACK'd by the ATEM. However, the sender will need to + // do ✨ some form of shenanigans ✨ in order to be ready to receive tracking Ids. So we send + // them a barrier as part of the callback so that they can tell us that they are ready for + // us to continue with ATEM communication, at which point we may immediately inform them of a + // received tracking Id matching one included in this callback. + // + // Now, if we were being 🚩 Real Proper Software Developers 🚩 we'd probably expect the receiver + // of the callback to do clever things so that if a tracking Id is received immediately, they + // then wait for something that wants that tracking Id on their side, rather than blocking this + // task so that the caller can do ✨ shenanigans ✨. However, that sounds far too clever and too + // much like 🚩 Real Actual Work 🚩 so instead we've chosen to do this and hope that whichever + // actor we're waiting on doesn't take _too_ long to do ✨ shenanigans ✨ before signalling that + // they are ready. If they do, I suggest finding whoever wrote that code and bonking them 🔨. + barrier.wait().await; + }, + None => { + log::info!("ATEM message channel has closed."); + } + } + } + }; + + self.tick().await; + } + + pub async fn connect(&mut self, address: SocketAddr) -> Result<(), io::Error> { + let socket = UdpSocket::bind("0.0.0.0:0").await?; + socket.connect(address).await?; + self.socket = Some(socket); + + self.start_timers(); + + self.next_send_packet_id = 1; + self.session_id = 0; + self.in_flight = vec![]; + log::debug!("Reconnect"); + + self.send_packet(&atem_util::COMMAND_CONNECT_HELLO).await; + self.connection_state = ConnectionState::SynSent; Ok(()) } - pub async fn disconnect(self) { - self.inner_socket_handle.abort(); - self.socket.write().await.disconnect() + pub fn disconnect(&mut self) { + self.stop_timers(); + + self.retransmit_timer = None; + self.reconnect_timer = None; + self.ack_timer = None; + self.socket = None; + + let prev_connection_state = self.connection_state.clone(); + self.connection_state = ConnectionState::Closed; + + if prev_connection_state == ConnectionState::Established { + self.on_disconnect(); + } + } + + pub async fn send_commands(&mut self, commands: Vec) -> Vec { + let mut tracking_ids: Vec = Vec::with_capacity(commands.len()); + for command in commands.into_iter() { + let tracking_id = self.next_packet_tracking_id(); + self.send_command(&command.payload, &command.raw_name, tracking_id) + .await; + tracking_ids.push(TrackingId(tracking_id)); + } + + tracking_ids } pub async fn send_command(&mut self, payload: &[u8], raw_name: &str, tracking_id: u64) { - self.socket - .write() - .await - .send_command(payload, raw_name, tracking_id) - .await; - } -} + let packet_id = self.next_send_packet_id; + self.next_send_packet_id += 1; + if self.next_send_packet_id >= MAX_PACKET_ID { + self.next_send_packet_id = 0; + } -impl Default for AtemSocket { - fn default() -> Self { - Self::new() + let opcode = u16::from(u8::from(PacketFlag::AckRequest)) << 11; + + let mut buffer = vec![0; 20 + payload.len()]; + + // Headers + buffer[0..2].copy_from_slice(&u16::to_be_bytes(opcode | (payload.len() as u16 + 20))); + buffer[2..4].copy_from_slice(&u16::to_be_bytes(self.session_id)); + buffer[10..12].copy_from_slice(&u16::to_be_bytes(packet_id)); + + // Command + buffer[12..14].copy_from_slice(&u16::to_be_bytes(payload.len() as u16 + 8)); + buffer[16..20].copy_from_slice(raw_name.as_bytes()); + + // Body + buffer[20..20 + payload.len()].copy_from_slice(payload); + self.send_packet(&buffer).await; + + self.in_flight.push(InFlightPacket { + packet_id, + tracking_id, + payload: buffer, + last_sent: SystemTime::now(), + resent: 0, + }) + } + + async fn restart_connection(&mut self) { + self.disconnect(); + self.connect(self.address).await.ok(); + } + + async fn tick(&mut self) { + let messages = self.receive().await.ok(); + if let Some(messages) = messages { + for message in messages.iter() { + self.recieved_packet(message).await; + } + } + if let Some(ack_time) = self.ack_timer { + if ack_time <= SystemTime::now() { + self.ack_timer = None; + self.received_without_ack = 0; + self.send_ack(self.last_received_packed_id).await; + } + } + if let Some(reconnect_time) = self.reconnect_timer { + if reconnect_time <= SystemTime::now() { + if self.last_received_at + Duration::from_millis(CONNECTION_TIMEOUT) + <= SystemTime::now() + { + log::debug!("{:?}", self.last_received_at); + log::debug!("Connection timed out, restarting"); + self.restart_connection().await; + } + self.start_reconnect_timer(); + } + } + if let Some(retransmit_time) = self.retransmit_timer { + if retransmit_time <= SystemTime::now() { + self.check_for_retransmit().await; + self.start_retransmit_timer(); + } + } + } + + async fn receive(&mut self) -> Result>, AtemSocketReceiveError> { + let mut messages: Vec> = vec![]; + let socket = self.socket.as_mut().ok_or(AtemSocketReceiveError::Closed)?; + + let mut buf = [0; MAX_PACKET_RECEIVE_SIZE]; + if let Ok((message_size, _)) = socket.try_recv_from(&mut buf) { + messages.push(buf[0..message_size].to_owned()); + } + + Ok(messages) + } + + fn is_packet_covered_by_ack(&self, ack_id: u16, packet_id: u16) -> bool { + let tolerance: u16 = MAX_PACKET_ID / 2; + let pkt_is_shortly_before = packet_id < ack_id && packet_id + tolerance > ack_id; + let pkt_is_shortly_after = packet_id > ack_id && packet_id < ack_id + tolerance; + let pkt_is_before_wrap = packet_id > ack_id + tolerance; + packet_id == ack_id + || ((pkt_is_shortly_before || pkt_is_before_wrap) && !pkt_is_shortly_after) + } + + async fn recieved_packet(&mut self, packet: &[u8]) { + let Ok(atem_packet): Result = packet.try_into() else { + return; + }; + + log::debug!("Received {:x?}", atem_packet); + + self.last_received_at = SystemTime::now(); + + self.session_id = atem_packet.session_id(); + let remote_packet_id = atem_packet.remote_packet_id(); + + if atem_packet.has_flag(PacketFlag::NewSessionId) { + log::debug!("New session"); + self.connection_state = ConnectionState::Established; + self.last_received_packed_id = remote_packet_id; + self.send_ack(remote_packet_id).await; + self.on_connect().await; + return; + } + + if self.connection_state == ConnectionState::Established { + if let Some(from_packet_id) = atem_packet.retransmit_request() { + log::debug!("Retransmit request: {:x?}", from_packet_id); + + self.retransmit_from(from_packet_id).await; + } + + if atem_packet.has_flag(PacketFlag::AckRequest) { + if remote_packet_id == (self.last_received_packed_id + 1) % MAX_PACKET_ID { + self.last_received_packed_id = remote_packet_id; + self.send_or_queue_ack().await; + + if let Some(body) = atem_packet.body() { + self.on_commands_received(body); + } + } else if self + .is_packet_covered_by_ack(self.last_received_packed_id, remote_packet_id) + { + self.send_or_queue_ack().await; + } + } + + if atem_packet.has_flag(PacketFlag::IsRetransmit) { + log::debug!("ATEM retransmitted packet {:x?}", remote_packet_id); + } + + if let Some(ack_packet_id) = atem_packet.ack_reply() { + let mut acked_commands: Vec = vec![]; + + self.in_flight = self + .in_flight + .clone() + .into_iter() + .filter(|pkt| { + if self.is_packet_covered_by_ack(ack_packet_id, pkt.packet_id) { + acked_commands.push(AckedPacket { + packet_id: pkt.packet_id, + tracking_id: pkt.tracking_id, + }); + false + } else { + true + } + }) + .collect(); + self.on_command_acknowledged(acked_commands); + } + } + } + + async fn send_packet(&self, packet: &[u8]) { + log::debug!("Send {:x?}", packet); + if let Some(socket) = &self.socket { + socket.send(packet).await.ok(); + } else { + log::debug!("Socket is not open") + } + } + + async fn send_or_queue_ack(&mut self) { + self.received_without_ack += 1; + if self.received_without_ack >= MAX_PACKET_PER_ACK { + self.received_without_ack = 0; + self.ack_timer = None; + self.send_ack(self.last_received_packed_id).await; + } else if self.ack_timer.is_none() { + self.ack_timer = Some(SystemTime::now() + Duration::from_millis(5)); + } + } + + async fn send_ack(&mut self, packet_id: u16) { + log::debug!("Sending ack for packet {:x?}", packet_id); + let flag: u8 = PacketFlag::AckReply.into(); + let opcode = u16::from(flag) << 11; + let mut buffer: [u8; ACK_PACKET_LENGTH as _] = [0; 12]; + buffer[0..2].copy_from_slice(&u16::to_be_bytes(opcode | ACK_PACKET_LENGTH)); + buffer[2..4].copy_from_slice(&u16::to_be_bytes(self.session_id)); + buffer[4..6].copy_from_slice(&u16::to_be_bytes(packet_id)); + self.send_packet(&buffer).await; + } + + async fn retransmit_from(&mut self, from_id: u16) { + let from_id = from_id % MAX_PACKET_ID; + + if let Some(index) = self + .in_flight + .iter() + .position(|pkt| pkt.packet_id == from_id) + { + log::debug!( + "Resending from {} to {}", + from_id, + self.in_flight[self.in_flight.len() - 1].packet_id + ); + for i in index..self.in_flight.len() { + let mut sent_packet = self.in_flight[i].clone(); + if sent_packet.packet_id == from_id + || !self.is_packet_covered_by_ack(from_id, sent_packet.packet_id) + { + sent_packet.last_sent = SystemTime::now(); + sent_packet.resent += 1; + + self.send_packet(&sent_packet.payload).await; + } + } + } else { + log::debug!("Unable to resend: {}", from_id); + self.restart_connection().await; + } + } + + async fn check_for_retransmit(&mut self) { + for sent_packet in self.in_flight.clone() { + if sent_packet.last_sent + Duration::from_millis(IN_FLIGHT_TIMEOUT) < SystemTime::now() + { + if sent_packet.resent <= MAX_PACKET_RETRIES + && self + .is_packet_covered_by_ack(self.next_send_packet_id, sent_packet.packet_id) + { + log::debug!("Retransmit from timeout: {}", sent_packet.packet_id); + + self.retransmit_from(sent_packet.packet_id).await; + } else { + log::debug!("Packet timed out: {}", sent_packet.packet_id); + self.restart_connection().await; + } + } + } + } + + fn on_commands_received(&mut self, payload: &[u8]) { + let _ = self + .atem_event_tx + .send(AtemSocketEvent::ReceivedCommands(payload.to_vec())); + } + + fn on_command_acknowledged(&mut self, packets: Vec) { + for ack in packets { + let _ = self + .atem_event_tx + .send(AtemSocketEvent::AckedCommand(TrackingId(ack.tracking_id))); + } + } + + async fn on_connect(&mut self) { + let _ = self.atem_event_tx.send(AtemSocketEvent::Connected); + let mut connected_callbacks = self.connected_callbacks.lock().await; + for callback in connected_callbacks.drain(0..) { + let _ = callback.send(false); + } + } + + fn on_disconnect(&mut self) { + let _ = self.atem_event_tx.send(AtemSocketEvent::Disconnected); + } + + fn start_timers(&mut self) { + log::debug!("Starting timers"); + self.start_reconnect_timer(); + self.start_retransmit_timer(); + } + + fn stop_timers(&mut self) { + self.reconnect_timer = None; + self.retransmit_timer = None; + } + + fn start_reconnect_timer(&mut self) { + self.reconnect_timer = + Some(SystemTime::now() + Duration::from_millis(CONNECTION_RETRY_INTERVAL)); + } + + fn start_retransmit_timer(&mut self) { + self.retransmit_timer = + Some(SystemTime::now() + Duration::from_millis(RETRANSMIT_CHECK_INTERVAL)); + } + + fn next_packet_tracking_id(&mut self) -> u64 { + self.next_tracking_id = self.next_tracking_id.checked_add(1).unwrap_or(1); + + self.next_tracking_id } } diff --git a/atem-connection-rs/src/atem_lib/atem_socket_inner.rs b/atem-connection-rs/src/atem_lib/atem_socket_inner.rs deleted file mode 100644 index c9c2594..0000000 --- a/atem-connection-rs/src/atem_lib/atem_socket_inner.rs +++ /dev/null @@ -1,474 +0,0 @@ -use std::{ - io, - net::SocketAddr, - time::{Duration, SystemTime}, -}; - -use log::debug; -use tokio::net::UdpSocket; - -use crate::atem_lib::atem_util; - -const IN_FLIGHT_TIMEOUT: u64 = 60; -const CONNECTION_TIMEOUT: u64 = 5000; -const CONNECTION_RETRY_INTERVAL: u64 = 1000; -const RETRANSMIT_CHECK_INTERVAL: u64 = 1000; -const MAX_PACKET_RETRIES: u16 = 10; -const MAX_PACKET_ID: u16 = 1 << 15; -const MAX_PACKET_PER_ACK: u16 = 16; - -// Set to max UDP packet size, for now -const MAX_PACKET_RECEIVE_SIZE: usize = 65535; -const ACK_PACKET_LENGTH: u16 = 12; - -#[derive(PartialEq, Clone)] -enum ConnectionState { - Closed, - SynSent, - Established, -} - -#[allow(clippy::from_over_into)] -impl Into for ConnectionState { - fn into(self) -> u8 { - match self { - ConnectionState::Closed => 0x00, - ConnectionState::SynSent => 0x01, - ConnectionState::Established => 0x02, - } - } -} - -#[derive(PartialEq)] -enum PacketFlag { - AckRequest, - NewSessionId, - IsRetransmit, - RetransmitRequest, - AckReply, -} - -impl From for u8 { - fn from(flag: PacketFlag) -> Self { - match flag { - PacketFlag::AckRequest => 0x01, - PacketFlag::NewSessionId => 0x02, - PacketFlag::IsRetransmit => 0x04, - PacketFlag::RetransmitRequest => 0x08, - PacketFlag::AckReply => 0x10, - } - } -} - -#[derive(Clone)] -struct InFlightPacket { - packet_id: u16, - tracking_id: u64, - payload: Vec, - pub last_sent: SystemTime, - pub resent: u16, -} - -struct AckedPacket { - packet_id: u16, - tracking_id: u64, -} - -pub struct AtemSocketCommand { - payload: Vec, - raw_name: String, - tracking_id: u64, -} - -pub struct AtemSocketInner { - connection_state: ConnectionState, - reconnect_timer: Option, - retransmit_timer: Option, - - next_send_packet_id: u16, - session_id: u16, - - socket: Option, - address: String, - port: u16, - - last_received_at: SystemTime, - last_received_packed_id: u16, - in_flight: Vec, - ack_timer: Option, - received_without_ack: u16, -} - -enum AtemSocketReceiveError { - Closed, -} - -#[derive(Debug, Error)] -enum AtemSocketWriteError { - #[error("Socket closed")] - Closed, - - #[error("Socket disconnected")] - Disconnected(#[from] io::Error), -} - -impl AtemSocketInner { - pub fn new() -> Self { - AtemSocketInner { - connection_state: ConnectionState::Closed, - reconnect_timer: None, - retransmit_timer: None, - - next_send_packet_id: 1, - session_id: 0, - - socket: None, - address: "0.0.0.0".to_string(), - port: 0, - - last_received_at: SystemTime::now(), - last_received_packed_id: 0, - in_flight: vec![], - ack_timer: None, - received_without_ack: 0, - } - } - - pub async fn connect(&mut self, address: String, port: u16) -> Result<(), io::Error> { - self.address = address.clone(); - self.port = port; - - let socket = UdpSocket::bind("0.0.0.0:0").await?; - let remote_addr = format!("{}:{}", address, port) - .parse::() - .unwrap(); - socket.connect(remote_addr).await?; - self.socket = Some(socket); - - self.start_timers(); - - self.next_send_packet_id = 1; - self.session_id = 0; - self.in_flight = vec![]; - debug!("Reconnect"); - - self.send_packet(&atem_util::COMMAND_CONNECT_HELLO).await; - self.connection_state = ConnectionState::SynSent; - - Ok(()) - } - - pub fn disconnect(&mut self) { - self.stop_timers(); - - self.retransmit_timer = None; - self.reconnect_timer = None; - self.ack_timer = None; - self.socket = None; - - let prev_connection_state = self.connection_state.clone(); - self.connection_state = ConnectionState::Closed; - - if prev_connection_state == ConnectionState::Established { - self.on_disconnect(); - } - } - - pub async fn send_commands(&mut self, commands: Vec) { - for command in commands.into_iter() { - self.send_command(&command.payload, &command.raw_name, command.tracking_id) - .await; - } - } - - pub async fn send_command(&mut self, payload: &[u8], raw_name: &str, tracking_id: u64) { - let packet_id = self.next_send_packet_id; - self.next_send_packet_id += 1; - if self.next_send_packet_id >= MAX_PACKET_ID { - self.next_send_packet_id = 0; - } - - let opcode = u16::from(u8::from(PacketFlag::AckRequest)) << 11; - - let mut buffer = vec![0; 20 + payload.len()]; - - // Headers - buffer[0..2].copy_from_slice(&u16::to_be_bytes(opcode | (payload.len() as u16 + 20))); - buffer[2..4].copy_from_slice(&u16::to_be_bytes(self.session_id)); - buffer[10..12].copy_from_slice(&u16::to_be_bytes(packet_id)); - - // Command - buffer[12..14].copy_from_slice(&u16::to_be_bytes(payload.len() as u16 + 8)); - buffer[16..20].copy_from_slice(raw_name.as_bytes()); - - // Body - buffer[20..20 + payload.len()].copy_from_slice(payload); - self.send_packet(&buffer).await; - - debug!("{:x?}", buffer); - - self.in_flight.push(InFlightPacket { - packet_id, - tracking_id, - payload: buffer, - last_sent: SystemTime::now(), - resent: 0, - }) - } - - async fn restart_connection(&mut self) { - self.disconnect(); - self.connect(self.address.clone(), self.port).await.ok(); - } - - pub async fn tick(&mut self) { - let messages = self.receive().await.ok(); - if let Some(messages) = messages { - for message in messages.iter() { - self.recieved_packet(message).await; - } - } - if let Some(ack_time) = self.ack_timer { - if ack_time <= SystemTime::now() { - self.ack_timer = None; - self.received_without_ack = 0; - self.send_ack(self.last_received_packed_id).await; - } - } - if let Some(reconnect_time) = self.reconnect_timer { - if reconnect_time <= SystemTime::now() { - if self.last_received_at + Duration::from_millis(CONNECTION_TIMEOUT) - <= SystemTime::now() - { - debug!("{:?}", self.last_received_at); - debug!("Connection timed out, restarting"); - self.restart_connection().await; - } - self.start_reconnect_timer(); - } - } - if let Some(retransmit_time) = self.retransmit_timer { - if retransmit_time <= SystemTime::now() { - self.check_for_retransmit().await; - self.start_retransmit_timer(); - } - } - } - - async fn receive(&mut self) -> Result>, AtemSocketReceiveError> { - let mut messages: Vec> = vec![]; - let socket = self.socket.as_mut().ok_or(AtemSocketReceiveError::Closed)?; - - let mut buf = [0; MAX_PACKET_RECEIVE_SIZE]; - if let Ok((message_size, _)) = socket.try_recv_from(&mut buf) { - messages.push(buf[0..message_size].to_owned()); - } - - Ok(messages) - } - - fn is_packet_covered_by_ack(&self, ack_id: u16, packet_id: u16) -> bool { - let tolerance: u16 = MAX_PACKET_ID / 2; - let pkt_is_shortly_before = packet_id < ack_id && packet_id + tolerance > ack_id; - let pkt_is_shortly_after = packet_id > ack_id && packet_id < ack_id + tolerance; - let pkt_is_before_wrap = packet_id > ack_id + tolerance; - packet_id == ack_id - || ((pkt_is_shortly_before || pkt_is_before_wrap) && !pkt_is_shortly_after) - } - - async fn recieved_packet(&mut self, packet: &[u8]) { - debug!("RECV {:x?}", packet); - - if packet.len() < 12 { - debug!("Invalid packet from ATEM {:x?}", packet); - return; - } - - self.last_received_at = SystemTime::now(); - let length = u16::from_be_bytes(packet[0..2].try_into().unwrap()) & 0x07ff; - - if length as usize != packet.len() { - debug!( - "Length of message differs, expected {} got {}", - length, - packet.len() - ); - return; - } - - let flags = packet[0] >> 3; - self.session_id = u16::from_be_bytes(packet[2..4].try_into().unwrap()); - let remote_packet_id = u16::from_be_bytes(packet[10..12].try_into().unwrap()); - - if flags & u8::from(PacketFlag::NewSessionId) > 0 { - debug!("New session"); - self.connection_state = ConnectionState::Established; - self.last_received_packed_id = remote_packet_id; - self.send_ack(remote_packet_id).await; - return; - } - - if self.connection_state == ConnectionState::Established { - if flags & u8::from(PacketFlag::RetransmitRequest) > 0 { - let from_packet_id = u16::from_be_bytes(packet[6..8].try_into().unwrap()); - debug!("Retransmit request: {:x?}", from_packet_id); - - self.retransmit_from(from_packet_id).await; - } - - if flags & u8::from(PacketFlag::AckRequest) > 0 { - if remote_packet_id == (self.last_received_packed_id + 1) % MAX_PACKET_ID { - self.last_received_packed_id = remote_packet_id; - self.send_or_queue_ack().await; - - if length > 12 { - self.on_command_received(&packet[12..], remote_packet_id); - } - } else if self - .is_packet_covered_by_ack(self.last_received_packed_id, remote_packet_id) - { - self.send_or_queue_ack().await; - } - } - - if flags & u8::from(PacketFlag::IsRetransmit) > 0 { - debug!("ATEM retransmitted packet {:x?}", remote_packet_id); - } - - if flags & u8::from(PacketFlag::AckReply) > 0 { - let ack_packet_id = u16::from_be_bytes(packet[4..6].try_into().unwrap()); - let mut acked_commands: Vec = vec![]; - - self.in_flight = self - .in_flight - .clone() - .into_iter() - .filter(|pkt| { - if self.is_packet_covered_by_ack(ack_packet_id, pkt.packet_id) { - acked_commands.push(AckedPacket { - packet_id: pkt.packet_id, - tracking_id: pkt.tracking_id, - }); - false - } else { - true - } - }) - .collect(); - self.on_command_acknowledged(acked_commands); - } - } - } - - async fn send_packet(&self, packet: &[u8]) { - debug!("Send {:x?}", packet); - if let Some(socket) = &self.socket { - socket.send(packet).await.ok(); - } else { - debug!("Socket is not open") - } - } - - async fn send_or_queue_ack(&mut self) { - self.received_without_ack += 1; - if self.received_without_ack >= MAX_PACKET_PER_ACK { - self.received_without_ack = 0; - self.ack_timer = None; - self.send_ack(self.last_received_packed_id).await; - } else if self.ack_timer.is_none() { - self.ack_timer = Some(SystemTime::now() + Duration::from_millis(5)); - } - } - - async fn send_ack(&mut self, packet_id: u16) { - debug!("Sending ack for packet {:x?}", packet_id); - let flag: u8 = PacketFlag::AckReply.into(); - let opcode = u16::from(flag) << 11; - let mut buffer: [u8; ACK_PACKET_LENGTH as _] = [0; 12]; - buffer[0..2].copy_from_slice(&u16::to_be_bytes(opcode as u16 | ACK_PACKET_LENGTH)); - buffer[2..4].copy_from_slice(&u16::to_be_bytes(self.session_id)); - buffer[4..6].copy_from_slice(&u16::to_be_bytes(packet_id)); - self.send_packet(&buffer).await; - } - - async fn retransmit_from(&mut self, from_id: u16) { - let from_id = from_id % MAX_PACKET_ID; - - if let Some(index) = self - .in_flight - .iter() - .position(|pkt| pkt.packet_id == from_id) - { - debug!( - "Resending from {} to {}", - from_id, - self.in_flight[self.in_flight.len() - 1].packet_id - ); - for i in index..self.in_flight.len() { - let mut sent_packet = self.in_flight[i].clone(); - if sent_packet.packet_id == from_id - || !self.is_packet_covered_by_ack(from_id, sent_packet.packet_id) - { - sent_packet.last_sent = SystemTime::now(); - sent_packet.resent += 1; - - self.send_packet(&sent_packet.payload).await; - } - } - } else { - debug!("Unable to resend: {}", from_id); - self.restart_connection().await; - } - } - - async fn check_for_retransmit(&mut self) { - for sent_packet in self.in_flight.clone() { - if sent_packet.last_sent + Duration::from_millis(IN_FLIGHT_TIMEOUT) < SystemTime::now() - { - if sent_packet.resent <= MAX_PACKET_RETRIES - && self - .is_packet_covered_by_ack(self.next_send_packet_id, sent_packet.packet_id) - { - debug!("Retransmit from timeout: {}", sent_packet.packet_id); - - self.retransmit_from(sent_packet.packet_id).await; - } else { - debug!("Packet timed out: {}", sent_packet.packet_id); - self.restart_connection().await; - } - } - } - } - - fn on_command_received(&mut self, payload: &[u8], packet_id: u16) { - // TODO: Emit some event - } - - fn on_command_acknowledged(&mut self, ids: Vec) { - // TODO: Emit some event - } - - fn on_disconnect(&mut self) { - // TODO: Emit some event - } - - fn start_timers(&mut self) { - self.start_reconnect_timer(); - self.start_retransmit_timer(); - } - - fn stop_timers(&mut self) { - self.reconnect_timer = None; - self.retransmit_timer = None; - } - - fn start_reconnect_timer(&mut self) { - self.reconnect_timer = - Some(SystemTime::now() + Duration::from_millis(CONNECTION_RETRY_INTERVAL)); - } - - fn start_retransmit_timer(&mut self) { - self.retransmit_timer = - Some(SystemTime::now() + Duration::from_millis(RETRANSMIT_CHECK_INTERVAL)); - } -} diff --git a/atem-connection-rs/src/atem_lib/mod.rs b/atem-connection-rs/src/atem_lib/mod.rs index 6a8af6a..9b3090b 100644 --- a/atem-connection-rs/src/atem_lib/mod.rs +++ b/atem-connection-rs/src/atem_lib/mod.rs @@ -1,4 +1,3 @@ mod atem_packet; pub mod atem_socket; -mod atem_socket_inner; pub mod atem_util; diff --git a/atem-connection-rs/src/commands/command_base.rs b/atem-connection-rs/src/commands/command_base.rs index 896e73a..b708af6 100644 --- a/atem-connection-rs/src/commands/command_base.rs +++ b/atem-connection-rs/src/commands/command_base.rs @@ -1,23 +1,56 @@ -use std::collections::HashMap; +use std::{collections::HashMap, fmt::Debug, process::Command, sync::Arc}; use crate::{enums::ProtocolVersion, state::AtemState}; -pub trait DeserializedCommand { - fn apply_to_state(&self, state: &mut AtemState) -> Vec; +pub trait DeserializedCommand: Send + Sync + Debug { + fn raw_name(&self) -> &'static str; + fn apply_to_state(&self, state: &mut AtemState); } -pub trait DeserializableCommand: DeserializedCommand { +pub trait CommandDeserializer: Send + Sync { + fn deserialize(&self, buffer: &[u8], version: &ProtocolVersion) + -> Arc; +} + +pub trait SerializableCommand: Send + Sync { + fn payload(&self, version: &ProtocolVersion) -> Vec; +} + +impl SerializableCommand for Box { + fn payload(&self, version: &ProtocolVersion) -> Vec { + (**self).payload(version) + } +} + +impl SerializableCommand for &'_ Box { + fn payload(&self, version: &ProtocolVersion) -> Vec { + (**self).payload(version) + } +} + +pub trait BasicWritableCommand: SerializableCommand + Send + Sync { fn get_raw_name(&self) -> &'static str; fn get_minimum_version(&self) -> ProtocolVersion; } -pub trait SerializableCommand { - fn payload(&self, version: ProtocolVersion) -> Vec; +impl BasicWritableCommand for Box { + fn get_raw_name(&self) -> &'static str { + (**self).get_raw_name() + } + + fn get_minimum_version(&self) -> ProtocolVersion { + (**self).get_minimum_version() + } } -pub trait BasicWritableCommand: SerializableCommand { - fn get_raw_name(&self) -> &'static str; - fn get_minimum_version(&self) -> ProtocolVersion; +impl BasicWritableCommand for &'_ Box { + fn get_raw_name(&self) -> &'static str { + (**self).get_raw_name() + } + + fn get_minimum_version(&self) -> ProtocolVersion { + (**self).get_minimum_version() + } } pub trait WritableCommand: BasicWritableCommand { @@ -25,5 +58,3 @@ pub trait WritableCommand: BasicWritableCommand { fn get_flag(&self) -> f64; fn set_flag(&mut self, flag: f64); } - -pub trait SymmetricalCommand: DeserializableCommand + SerializableCommand {} diff --git a/atem-connection-rs/src/commands/device_profile.rs b/atem-connection-rs/src/commands/device_profile.rs new file mode 100644 index 0000000..5226f58 --- /dev/null +++ b/atem-connection-rs/src/commands/device_profile.rs @@ -0,0 +1,7 @@ +pub mod audio_mixer_config; +pub mod media_pool_config; +pub mod mix_effect_block_config; +pub mod multiviewer_config; +pub mod product_identifier; +pub mod topology; +pub mod version; diff --git a/atem-connection-rs/src/commands/device_profile/audio_mixer_config.rs b/atem-connection-rs/src/commands/device_profile/audio_mixer_config.rs new file mode 100644 index 0000000..9cec08f --- /dev/null +++ b/atem-connection-rs/src/commands/device_profile/audio_mixer_config.rs @@ -0,0 +1,47 @@ +use std::sync::Arc; + +use crate::{ + commands::command_base::{CommandDeserializer, DeserializedCommand}, + state::{audio::AtemClassicAudioState, info::AudioMixerInfo}, +}; + +pub const DESERIALIZE_AUDIO_MIXER_CONFIG_NAME: &str = "_AMC"; + +#[derive(Debug)] +pub struct AudioMixerConfig { + inputs: u8, + monitors: u8, + headphones: u8, +} + +impl DeserializedCommand for AudioMixerConfig { + fn raw_name(&self) -> &'static str { + DESERIALIZE_AUDIO_MIXER_CONFIG_NAME + } + + fn apply_to_state(&self, state: &mut crate::state::AtemState) { + state.info.audio_mixer = Some(AudioMixerInfo::new( + self.inputs, + self.monitors, + self.headphones, + )); + state.audio = Some(AtemClassicAudioState::new(self.inputs, self.monitors != 0)) + } +} + +#[derive(Default)] +pub struct AudioMixerConfigDeserializer {} + +impl CommandDeserializer for AudioMixerConfigDeserializer { + fn deserialize( + &self, + buffer: &[u8], + version: &crate::enums::ProtocolVersion, + ) -> std::sync::Arc { + Arc::new(AudioMixerConfig { + inputs: buffer[0], + monitors: buffer[1], + headphones: buffer[2], + }) + } +} diff --git a/atem-connection-rs/src/commands/device_profile/media_pool_config.rs b/atem-connection-rs/src/commands/device_profile/media_pool_config.rs new file mode 100644 index 0000000..285cb55 --- /dev/null +++ b/atem-connection-rs/src/commands/device_profile/media_pool_config.rs @@ -0,0 +1,40 @@ +use std::sync::Arc; + +use crate::{ + commands::command_base::{CommandDeserializer, DeserializedCommand}, + state::info::MediaPoolInfo, +}; + +pub const DESERIALIZE_MEDIA_POOL_CONFIG_NAME: &str = "_mpl"; + +#[derive(Debug)] +pub struct MediaPoolConfig { + still_count: u8, + clip_count: u8, +} + +impl DeserializedCommand for MediaPoolConfig { + fn raw_name(&self) -> &'static str { + DESERIALIZE_MEDIA_POOL_CONFIG_NAME + } + + fn apply_to_state(&self, state: &mut crate::state::AtemState) { + state.info.media_pool = Some(MediaPoolInfo::new(self.still_count, self.clip_count)) + } +} + +#[derive(Default)] +pub struct MediaPoolConfigDeserializer {} + +impl CommandDeserializer for MediaPoolConfigDeserializer { + fn deserialize( + &self, + buffer: &[u8], + _version: &crate::enums::ProtocolVersion, + ) -> std::sync::Arc { + Arc::new(MediaPoolConfig { + still_count: buffer[0], + clip_count: buffer[1], + }) + } +} diff --git a/atem-connection-rs/src/commands/device_profile/mix_effect_block_config.rs b/atem-connection-rs/src/commands/device_profile/mix_effect_block_config.rs new file mode 100644 index 0000000..0b7c9cc --- /dev/null +++ b/atem-connection-rs/src/commands/device_profile/mix_effect_block_config.rs @@ -0,0 +1,40 @@ +use std::sync::Arc; + +use crate::{ + commands::command_base::{CommandDeserializer, DeserializedCommand}, + state::info::MixEffectInfo, +}; + +pub const DESERIALIZE_MIX_EFFECT_BLOCK_CONFIG_NAME: &str = "_MeC"; + +#[derive(Debug)] +pub struct MixEffectBlockConfig { + mix_effect: u8, + key_count: u8, +} + +impl DeserializedCommand for MixEffectBlockConfig { + fn raw_name(&self) -> &'static str { + DESERIALIZE_MIX_EFFECT_BLOCK_CONFIG_NAME + } + + fn apply_to_state(&self, state: &mut crate::state::AtemState) { + state.info.mix_effects[self.mix_effect as usize] = Some(MixEffectInfo::new(self.key_count)); + } +} + +#[derive(Default)] +pub struct MixEffectBlockConfigDeserializer {} + +impl CommandDeserializer for MixEffectBlockConfigDeserializer { + fn deserialize( + &self, + buffer: &[u8], + _version: &crate::enums::ProtocolVersion, + ) -> std::sync::Arc { + Arc::new(MixEffectBlockConfig { + mix_effect: buffer[0], + key_count: buffer[1], + }) + } +} diff --git a/atem-connection-rs/src/commands/device_profile/multiviewer_config.rs b/atem-connection-rs/src/commands/device_profile/multiviewer_config.rs new file mode 100644 index 0000000..00099fb --- /dev/null +++ b/atem-connection-rs/src/commands/device_profile/multiviewer_config.rs @@ -0,0 +1,58 @@ +use std::sync::Arc; + +use crate::{ + commands::command_base::{CommandDeserializer, DeserializedCommand}, + enums::ProtocolVersion, + state::info::MultiviewerInfo, +}; + +pub const DESERIALIZE_MULTIVIEWER_NAME: &str = "_MvC"; + +#[derive(Debug)] +pub struct MultiviewerConfig { + count: Option, + window_count: u8, +} + +impl DeserializedCommand for MultiviewerConfig { + fn raw_name(&self) -> &'static str { + DESERIALIZE_MULTIVIEWER_NAME + } + + fn apply_to_state(&self, state: &mut crate::state::AtemState) { + // TODO: This can't be right... + + let existing_count = match &state.info.multiviewer { + Some(multiviewer) => multiviewer.count().as_ref().copied(), + None => None, + }; + let count = match self.count { + Some(count) => Some(count), + None => existing_count, + }; + state.info.multiviewer = Some(MultiviewerInfo::new(count, self.window_count)); + } +} + +#[derive(Default)] +pub struct MultiviewerConfigDeserializer {} + +impl CommandDeserializer for MultiviewerConfigDeserializer { + fn deserialize( + &self, + buffer: &[u8], + version: &crate::enums::ProtocolVersion, + ) -> std::sync::Arc { + if *version >= ProtocolVersion::V8_1_1 { + Arc::new(MultiviewerConfig { + count: None, + window_count: buffer[1], + }) + } else { + Arc::new(MultiviewerConfig { + count: Some(buffer[0]), + window_count: buffer[1], + }) + } + } +} diff --git a/atem-connection-rs/src/commands/device_profile/product_identifier.rs b/atem-connection-rs/src/commands/device_profile/product_identifier.rs new file mode 100644 index 0000000..6d79764 --- /dev/null +++ b/atem-connection-rs/src/commands/device_profile/product_identifier.rs @@ -0,0 +1,68 @@ +use std::{ffi::CString, sync::Arc}; + +use crate::{ + commands::command_base::{CommandDeserializer, DeserializedCommand}, + enums::{Model, ProtocolVersion}, +}; + +pub const DESERIALIZE_PRODUCT_IDENTIFIER_RAW_NAME: &str = "_pin"; + +#[derive(Debug)] +pub struct ProductIdentifier { + pub product_identifier: String, + pub model: Model, +} + +impl DeserializedCommand for ProductIdentifier { + fn raw_name(&self) -> &'static str { + DESERIALIZE_PRODUCT_IDENTIFIER_RAW_NAME + } + + fn apply_to_state(&self, state: &mut crate::state::AtemState) { + state.info.product_identifier = Some(self.product_identifier.clone()); + state.info.model = self.model.clone(); + + match state.info.model { + Model::TwoME + | Model::TwoME4K + | Model::TwoMEBS4K + | Model::Constellation + | Model::Constellation8K + | Model::ConstellationHD4ME + | Model::Constellation4K4ME => { + state.info.power = vec![false, false]; + } + _ => { + state.info.power = vec![false]; + } + } + } +} + +#[derive(Default)] +pub struct ProductIdentifierDeserializer {} + +impl CommandDeserializer for ProductIdentifierDeserializer { + fn deserialize( + &self, + buffer: &[u8], + version: &ProtocolVersion, + ) -> Arc { + let null_byte_index = buffer + .iter() + .position(|byte| *byte == b'\0') + .expect("No null byte"); + let product_identifier = + CString::from_vec_with_nul(buffer[..(null_byte_index + 1)].to_vec()) + .expect("Malformed string"); + let model = buffer[40]; + + Arc::new(ProductIdentifier { + product_identifier: product_identifier + .to_str() + .expect("Invalid rust string") + .to_string(), + model: model.into(), + }) + } +} diff --git a/atem-connection-rs/src/commands/device_profile/topology.rs b/atem-connection-rs/src/commands/device_profile/topology.rs new file mode 100644 index 0000000..0f69cd1 --- /dev/null +++ b/atem-connection-rs/src/commands/device_profile/topology.rs @@ -0,0 +1,117 @@ +use std::sync::Arc; + +use crate::{ + commands::command_base::{CommandDeserializer, DeserializedCommand}, + enums::ProtocolVersion, + state::info::{AtemCapabilites, MultiviewerInfo}, +}; + +pub const DESERIALIZE_TOPOLOGY_RAW_NAME: &str = "_top"; + +#[derive(Debug)] +pub struct Topology { + mix_effects: u8, + sources: u8, + auxilliaries: u8, + mix_minus_outputs: u8, + media_players: u8, + multiviewers: Option, + serial_ports: u8, + max_hyperdecks: u8, + dves: u8, + stingers: u8, + super_sources: u8, + talkback_channels: u8, + downstream_keyers: u8, + camera_control: bool, + advanced_chroma_keyers: bool, + only_configurable_outputs: bool, +} + +impl DeserializedCommand for Topology { + fn raw_name(&self) -> &'static str { + DESERIALIZE_TOPOLOGY_RAW_NAME + } + + fn apply_to_state(&self, state: &mut crate::state::AtemState) { + state.info.capabilities = Some(AtemCapabilites::new( + self.mix_effects, + self.sources, + self.auxilliaries, + self.mix_minus_outputs, + self.media_players, + self.serial_ports, + self.max_hyperdecks, + self.dves, + self.stingers, + self.super_sources, + self.talkback_channels, + self.downstream_keyers, + self.camera_control, + self.advanced_chroma_keyers, + self.only_configurable_outputs, + )); + + let window_count = if let Some(mv) = &state.info.multiviewer { + *mv.window_count() + } else { + 10 + }; + + state.info.multiviewer = Some(MultiviewerInfo::new(self.multiviewers, window_count)); + } +} + +#[derive(Default)] +pub struct TopologyDeserializer {} + +impl CommandDeserializer for TopologyDeserializer { + fn deserialize( + &self, + buffer: &[u8], + version: &ProtocolVersion, + ) -> Arc { + let v230offset = if *version > ProtocolVersion::V8_0_1 { + 1 + } else { + 0 + }; + + let multiviewers = if v230offset > 0 { + Some(buffer[6]) + } else { + None + }; + + let advanced_chroma_keyers = if buffer.len() > 20 { + buffer[21 + v230offset] == 1 + } else { + false + }; + + let only_configurable_outputs = if buffer.len() > 20 { + buffer[22 + v230offset] == 1 + } else { + false + }; + + Arc::new(Topology { + mix_effects: buffer[0], + sources: buffer[1], + downstream_keyers: buffer[2], + auxilliaries: buffer[3], + mix_minus_outputs: buffer[4], + media_players: buffer[5], + multiviewers, + serial_ports: buffer[6 + v230offset], + max_hyperdecks: buffer[7 + v230offset], + dves: buffer[8 + v230offset], + stingers: buffer[9 + v230offset], + super_sources: buffer[10 + v230offset], + talkback_channels: buffer[12 + v230offset], + camera_control: buffer[17 + v230offset] == 1, + advanced_chroma_keyers, + only_configurable_outputs, + }) + } +} diff --git a/atem-connection-rs/src/commands/device_profile/version.rs b/atem-connection-rs/src/commands/device_profile/version.rs new file mode 100644 index 0000000..ee2338e --- /dev/null +++ b/atem-connection-rs/src/commands/device_profile/version.rs @@ -0,0 +1,25 @@ +use crate::{commands::command_base::DeserializedCommand, enums::ProtocolVersion}; + +pub const DESERIALIZE_VERSION_RAW_NAME: &str = "_ver"; + +#[derive(Debug)] +pub struct Version { + pub version: ProtocolVersion, +} + +impl DeserializedCommand for Version { + fn raw_name(&self) -> &'static str { + DESERIALIZE_VERSION_RAW_NAME + } + + fn apply_to_state(&self, state: &mut crate::state::AtemState) { + state.info.api_version = self.version.clone(); + } +} + +pub fn deserialize_version(buffer: &[u8]) -> Version { + let version = u32::from_be_bytes([buffer[0], buffer[1], buffer[2], buffer[3]]); + let version: ProtocolVersion = version.try_into().expect("Invalid protocol version"); + + Version { version } +} diff --git a/atem-connection-rs/src/commands/init_complete.rs b/atem-connection-rs/src/commands/init_complete.rs new file mode 100644 index 0000000..e23fc7e --- /dev/null +++ b/atem-connection-rs/src/commands/init_complete.rs @@ -0,0 +1,31 @@ +use std::sync::Arc; + +use crate::enums::ProtocolVersion; + +use super::command_base::{CommandDeserializer, DeserializedCommand}; + +pub const DESERIALIZE_INIT_COMPLETE_RAW_NAME: &str = "InCm"; + +#[derive(Debug)] +pub struct InitComplete {} + +impl DeserializedCommand for InitComplete { + fn raw_name(&self) -> &'static str { + DESERIALIZE_INIT_COMPLETE_RAW_NAME + } + + fn apply_to_state(&self, _state: &mut crate::state::AtemState) {} +} + +#[derive(Default)] +pub struct InitCompleteDeserializer {} + +impl CommandDeserializer for InitCompleteDeserializer { + fn deserialize( + &self, + _buffer: &[u8], + version: &ProtocolVersion, + ) -> Arc { + Arc::new(InitComplete {}) + } +} diff --git a/atem-connection-rs/src/commands/mix_effects.rs b/atem-connection-rs/src/commands/mix_effects.rs index b0a6e40..776e8d0 100644 --- a/atem-connection-rs/src/commands/mix_effects.rs +++ b/atem-connection-rs/src/commands/mix_effects.rs @@ -1,27 +1 @@ -use super::command_base::{BasicWritableCommand, SerializableCommand}; - -#[derive(new)] -pub struct ProgramInput { - mix_effect: u8, - source: u16, -} - -impl SerializableCommand for ProgramInput { - fn payload(&self, _version: crate::enums::ProtocolVersion) -> Vec { - let mut buf = vec![0; 4]; - buf[..1].copy_from_slice(&self.mix_effect.to_be_bytes()); - buf[2..].copy_from_slice(&self.source.to_be_bytes()); - - buf - } -} - -impl BasicWritableCommand for ProgramInput { - fn get_raw_name(&self) -> &'static str { - "CPgI" - } - - fn get_minimum_version(&self) -> crate::enums::ProtocolVersion { - crate::enums::ProtocolVersion::Unknown - } -} +pub mod program_input; diff --git a/atem-connection-rs/src/commands/mix_effects/program_input.rs b/atem-connection-rs/src/commands/mix_effects/program_input.rs new file mode 100644 index 0000000..47a6738 --- /dev/null +++ b/atem-connection-rs/src/commands/mix_effects/program_input.rs @@ -0,0 +1,72 @@ +use std::sync::Arc; + +use crate::{ + commands::command_base::{ + BasicWritableCommand, CommandDeserializer, DeserializedCommand, SerializableCommand, + }, + enums::ProtocolVersion, + state::util::get_mix_effect, +}; + +pub const DESERIALIZE_PROGRAM_INPUT_RAW_NAME: &str = "PrgI"; + +#[derive(Debug, new)] +pub struct ProgramInput { + pub mix_effect: u8, + pub source: u16, +} + +impl SerializableCommand for ProgramInput { + fn payload(&self, _version: &crate::enums::ProtocolVersion) -> Vec { + let mut buf = vec![0; 4]; + buf[..1].copy_from_slice(&self.mix_effect.to_be_bytes()); + buf[2..].copy_from_slice(&self.source.to_be_bytes()); + + buf + } +} + +impl BasicWritableCommand for ProgramInput { + fn get_raw_name(&self) -> &'static str { + "CPgI" + } + + fn get_minimum_version(&self) -> crate::enums::ProtocolVersion { + crate::enums::ProtocolVersion::Unknown + } +} + +impl DeserializedCommand for ProgramInput { + fn raw_name(&self) -> &'static str { + DESERIALIZE_PROGRAM_INPUT_RAW_NAME + } + + fn apply_to_state(&self, state: &mut crate::state::AtemState) { + let Some(capabilities) = state.info.capabilities() else { + todo!("Return error"); + }; + + if self.mix_effect > *capabilities.mix_effects() { + todo!("Return error"); + } + + let mix_effect = get_mix_effect(state, self.mix_effect as usize); + mix_effect.program_input = self.source; + } +} + +#[derive(Default)] +pub struct ProgramInputDeserializer {} + +impl CommandDeserializer for ProgramInputDeserializer { + fn deserialize( + &self, + buffer: &[u8], + version: &ProtocolVersion, + ) -> Arc { + let mix_effect = buffer[0]; + let source = u16::from_be_bytes([buffer[2], buffer[3]]); + + Arc::new(ProgramInput { mix_effect, source }) + } +} diff --git a/atem-connection-rs/src/commands/mod.rs b/atem-connection-rs/src/commands/mod.rs index a95dff4..8de1246 100644 --- a/atem-connection-rs/src/commands/mod.rs +++ b/atem-connection-rs/src/commands/mod.rs @@ -1,2 +1,7 @@ pub mod command_base; +pub mod device_profile; +pub mod init_complete; pub mod mix_effects; +pub mod parse_commands; +pub mod tally_by_source; +pub mod time; diff --git a/atem-connection-rs/src/commands/parse_commands.rs b/atem-connection-rs/src/commands/parse_commands.rs new file mode 100644 index 0000000..1c51d1d --- /dev/null +++ b/atem-connection-rs/src/commands/parse_commands.rs @@ -0,0 +1,88 @@ +use std::{collections::VecDeque, sync::Arc}; + +use crate::{ + commands::device_profile::version::{deserialize_version, DESERIALIZE_VERSION_RAW_NAME}, + enums::ProtocolVersion, +}; + +use super::{ + command_base::{CommandDeserializer, DeserializedCommand}, + device_profile::{ + audio_mixer_config::{AudioMixerConfigDeserializer, DESERIALIZE_AUDIO_MIXER_CONFIG_NAME}, + media_pool_config::{MediaPoolConfigDeserializer, DESERIALIZE_MEDIA_POOL_CONFIG_NAME}, + mix_effect_block_config::{ + MixEffectBlockConfigDeserializer, DESERIALIZE_MIX_EFFECT_BLOCK_CONFIG_NAME, + }, + multiviewer_config::{MultiviewerConfigDeserializer, DESERIALIZE_MULTIVIEWER_NAME}, + product_identifier::{ + ProductIdentifierDeserializer, DESERIALIZE_PRODUCT_IDENTIFIER_RAW_NAME, + }, + topology::{TopologyDeserializer, DESERIALIZE_TOPOLOGY_RAW_NAME}, + }, + init_complete::{InitCompleteDeserializer, DESERIALIZE_INIT_COMPLETE_RAW_NAME}, + mix_effects::program_input::{ProgramInputDeserializer, DESERIALIZE_PROGRAM_INPUT_RAW_NAME}, + tally_by_source::{TallyBySourceDeserializer, DESERIALIZE_TALLY_BY_SOURCE_RAW_NAME}, + time::{TimeDeserializer, DESERIALIZE_TIME_RAW_NAME}, +}; + +pub fn deserialize_commands( + payload: &[u8], + version: &mut ProtocolVersion, +) -> VecDeque> { + let mut parsed_commands: VecDeque> = VecDeque::new(); + let mut head = 0; + + while payload.len() > head + 8 { + let length = u16::from_be_bytes([payload[head], payload[head + 1]]) as usize; + let Ok(name) = String::from_utf8(payload[(head + 4)..(head + 8)].to_vec()) else { + break; + }; + + if length < 8 { + break; + } + + log::debug!("Received command {} with length {}", name, length); + + let command_buffer = &payload[head + 8..head + length]; + + if name == DESERIALIZE_VERSION_RAW_NAME { + let version_command = deserialize_version(command_buffer); + *version = version_command.version.clone(); + log::info!("Switched to protocol version {}", version); + parsed_commands.push_back(Arc::new(version_command)); + } else if let Some(deserializer) = command_deserializer_from_string(name.as_str()) { + let deserialized_command = deserializer.deserialize(command_buffer, version); + log::debug!("Received {:?}", deserialized_command); + parsed_commands.push_back(deserialized_command); + } else { + log::warn!("Received command {name} for which there is no deserializer."); + // TODO: Remove! + todo!("Write deserializer for {name}."); + } + + head += length; + } + + parsed_commands +} + +fn command_deserializer_from_string(command_str: &str) -> Option> { + match command_str { + DESERIALIZE_INIT_COMPLETE_RAW_NAME => Some(Box::::default()), + DESERIALIZE_PROGRAM_INPUT_RAW_NAME => Some(Box::::default()), + DESERIALIZE_TALLY_BY_SOURCE_RAW_NAME => Some(Box::::default()), + DESERIALIZE_TIME_RAW_NAME => Some(Box::::default()), + DESERIALIZE_TOPOLOGY_RAW_NAME => Some(Box::::default()), + DESERIALIZE_MIX_EFFECT_BLOCK_CONFIG_NAME => { + Some(Box::::default()) + } + DESERIALIZE_PRODUCT_IDENTIFIER_RAW_NAME => { + Some(Box::::default()) + } + DESERIALIZE_MEDIA_POOL_CONFIG_NAME => Some(Box::::default()), + DESERIALIZE_MULTIVIEWER_NAME => Some(Box::::default()), + DESERIALIZE_AUDIO_MIXER_CONFIG_NAME => Some(Box::::default()), + _ => None, + } +} diff --git a/atem-connection-rs/src/commands/tally_by_source.rs b/atem-connection-rs/src/commands/tally_by_source.rs new file mode 100644 index 0000000..dd6e341 --- /dev/null +++ b/atem-connection-rs/src/commands/tally_by_source.rs @@ -0,0 +1,62 @@ +use std::{collections::HashMap, sync::Arc}; + +use crate::enums::ProtocolVersion; + +use super::command_base::{CommandDeserializer, DeserializedCommand}; + +pub const DESERIALIZE_TALLY_BY_SOURCE_RAW_NAME: &str = "TlSr"; + +#[derive(Debug)] +pub struct TallySource { + pub program: bool, + pub preview: bool, +} + +#[derive(Debug, new)] +pub struct TallyBySource { + pub sources: HashMap, +} + +impl DeserializedCommand for TallyBySource { + fn raw_name(&self) -> &'static str { + DESERIALIZE_TALLY_BY_SOURCE_RAW_NAME + } + + fn apply_to_state(&self, state: &mut crate::state::AtemState) { + todo!("Apply to state: Tally By Source") + } +} + +#[derive(Default)] +pub struct TallyBySourceDeserializer {} + +impl CommandDeserializer for TallyBySourceDeserializer { + fn deserialize( + &self, + buffer: &[u8], + version: &ProtocolVersion, + ) -> Arc { + let source_count = u16::from_be_bytes([buffer[0], buffer[1]]) as usize; + + log::debug!("{:?}", buffer); + log::debug!("Source count: {}", source_count); + + let mut sources = HashMap::new(); + for i in 0..source_count { + let source_byte_offset = 2 + (i * 3); + let source = + u16::from_be_bytes([buffer[source_byte_offset], buffer[source_byte_offset + 1]]); + let value_byte_offset = 4 + (i * 3); + let value = u8::from_be_bytes([buffer[value_byte_offset]]); + sources.insert( + source, + TallySource { + program: (value & 0x01) > 0, + preview: (value & 0x02) > 0, + }, + ); + } + + Arc::new(TallyBySource { sources }) + } +} diff --git a/atem-connection-rs/src/commands/time.rs b/atem-connection-rs/src/commands/time.rs new file mode 100644 index 0000000..fc0b8f6 --- /dev/null +++ b/atem-connection-rs/src/commands/time.rs @@ -0,0 +1,50 @@ +use std::sync::Arc; + +use crate::enums::ProtocolVersion; + +use super::command_base::{CommandDeserializer, DeserializedCommand}; + +pub const DESERIALIZE_TIME_RAW_NAME: &str = "Time"; + +#[derive(Debug)] +pub struct TimeInfo { + pub hour: u8, + pub minute: u8, + pub second: u8, + pub frame: u8, + pub drop_frame: bool, +} + +#[derive(Debug)] +pub struct Time { + info: TimeInfo, +} + +impl DeserializedCommand for Time { + fn raw_name(&self) -> &'static str { + DESERIALIZE_TIME_RAW_NAME + } + + fn apply_to_state(&self, state: &mut crate::state::AtemState) {} +} + +#[derive(Default)] +pub struct TimeDeserializer {} + +impl CommandDeserializer for TimeDeserializer { + fn deserialize( + &self, + buffer: &[u8], + version: &ProtocolVersion, + ) -> Arc { + let info = TimeInfo { + hour: buffer[0], + minute: buffer[1], + second: buffer[2], + frame: buffer[3], + drop_frame: buffer[5] == 1, + }; + + Arc::new(Time { info }) + } +} diff --git a/atem-connection-rs/src/enums/mod.rs b/atem-connection-rs/src/enums/mod.rs index 8a2be2f..390321f 100644 --- a/atem-connection-rs/src/enums/mod.rs +++ b/atem-connection-rs/src/enums/mod.rs @@ -1,4 +1,8 @@ +use std::fmt::Display; + +#[derive(Debug, Clone, Default, PartialEq)] pub enum Model { + #[default] Unknown = 0x00, TVS = 0x01, OneME = 0x02, @@ -17,15 +21,61 @@ pub enum Model { MiniProISO = 0x0f, MiniExtreme = 0x10, MiniExtremeISO = 0x11, + ConstellationHD1ME = 0x12, + ConstellationHD2ME = 0x13, + ConstellationHD4ME = 0x14, + SDI = 0x15, + SDIProISO = 0x16, + SDIExtremeISO = 0x17, + // 0x18 ?? + // 0x19 ?? + TelevisionStudioHD8 = 0x1a, + TelevisionStudioHD8ISO = 0x1b, + // 0x1c ?? + // 0x1d ?? + Constellation4K4ME = 0x1e, + // 0x1f ?? + TelevisionStudio4K8 = 0x20, } -impl Default for Model { - fn default() -> Self { - Model::Unknown +impl From for Model { + fn from(value: u8) -> Self { + match value { + 0x01 => Model::TVS, + 0x02 => Model::OneME, + 0x03 => Model::TwoME, + 0x04 => Model::PS4K, + 0x05 => Model::OneME4K, + 0x06 => Model::TwoME4K, + 0x07 => Model::TwoMEBS4K, + 0x08 => Model::TVSHD, + 0x09 => Model::TVSProHD, + 0x0a => Model::TVSPro4K, + 0x0b => Model::Constellation, + 0x0c => Model::Constellation8K, + 0x0d => Model::Mini, + 0x0e => Model::MiniPro, + 0x0f => Model::MiniProISO, + 0x10 => Model::MiniExtreme, + 0x11 => Model::MiniExtremeISO, + 0x12 => Model::ConstellationHD1ME, + 0x13 => Model::ConstellationHD2ME, + 0x14 => Model::ConstellationHD4ME, + 0x15 => Model::SDI, + 0x16 => Model::SDIProISO, + 0x17 => Model::SDIExtremeISO, + 0x1a => Model::TelevisionStudioHD8, + 0x1b => Model::TelevisionStudioHD8ISO, + 0x1e => Model::Constellation4K4ME, + 0x20 => Model::TelevisionStudio4K8, + _ => Model::Unknown, + } } } +#[derive(Debug, Default, Clone, PartialEq, PartialOrd)] pub enum ProtocolVersion { + #[default] Unknown = 0, V7_2 = 0x00020016, // 2.22 // TODO - verify this is correct V7_5_2 = 0x0002001b, // 2.27 // The naming of this may be off @@ -34,12 +84,36 @@ pub enum ProtocolVersion { V8_1_1 = 0x0002001e, // 2.30 } -impl Default for ProtocolVersion { - fn default() -> Self { - ProtocolVersion::Unknown +impl TryFrom for ProtocolVersion { + type Error = (); + + fn try_from(value: u32) -> Result { + match value { + 0 => Ok(ProtocolVersion::Unknown), + 0x00020016 => Ok(ProtocolVersion::V7_2), + 0x0002001b => Ok(ProtocolVersion::V7_5_2), + 0x0002001c => Ok(ProtocolVersion::V8_0), + 0x0002001d => Ok(ProtocolVersion::V8_0_1), + 0x0002001e => Ok(ProtocolVersion::V8_1_1), + _ => Ok(ProtocolVersion::Unknown), + } } } +impl Display for ProtocolVersion { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ProtocolVersion::Unknown => write!(f, "Unknown"), + ProtocolVersion::V7_2 => write!(f, "v7.2"), + ProtocolVersion::V7_5_2 => write!(f, "v7.5.2"), + ProtocolVersion::V8_0 => write!(f, "v8.0"), + ProtocolVersion::V8_0_1 => write!(f, "v8.0.1"), + ProtocolVersion::V8_1_1 => write!(f, "v8.1.1"), + } + } +} + +#[derive(Clone, PartialEq)] pub enum TransitionStyle { MIX = 0x00, DIP = 0x01, @@ -48,6 +122,7 @@ pub enum TransitionStyle { STING = 0x04, } +#[derive(Clone, PartialEq)] pub enum TransitionSelection { Background = 1 << 0, Key1 = 1 << 1, @@ -56,6 +131,7 @@ pub enum TransitionSelection { Key4 = 1 << 4, } +#[derive(Clone, PartialEq)] pub enum DVEEffect { SwooshTopLeft = 0, SwooshTop = 1, @@ -98,6 +174,7 @@ pub enum DVEEffect { GraphicLogoWipe = 34, } +#[derive(Clone, PartialEq)] pub enum MacroAction { Run = 0, Stop = 1, @@ -107,6 +184,7 @@ pub enum MacroAction { Delete = 5, } +#[derive(Clone, PartialEq)] pub enum ExternalPortType { Unknown = 0, SDI = 1, @@ -123,6 +201,7 @@ pub enum ExternalPortType { TRSJack = 2048, } +#[derive(Clone, PartialEq)] pub enum InternalPortType { External = 0, Black = 1, @@ -149,6 +228,8 @@ const SOURCE_AVAILABILITY_SUPERSOURCE_BOX: isize = 1 << 3; const SOURCE_AVAILABILITY_KEY_SOURCE: isize = 1 << 4; const SOURCE_AVAILABILITY_AUXILIARY_1: isize = 1 << 5; const SOURCE_AVAILABILITY_AUXILIARY_2: isize = 1 << 6; + +#[derive(Clone, PartialEq)] pub enum SourceAvailability { None = SOURCE_AVAILABILITY_NONE, Auxiliary = SOURCE_AVAILABILITY_AUXILIARY, @@ -172,6 +253,8 @@ const ME_AVAILABILITY_ME_1: isize = 1 << 0; const ME_AVAILABILITY_ME_2: isize = 1 << 1; const ME_AVAILABILITY_ME_3: isize = 1 << 2; const ME_AVAILABILITY_ME_4: isize = 1 << 3; + +#[derive(Clone, PartialEq)] pub enum MeAvailability { None = ME_AVAILABILITY_NONE, Me1 = ME_AVAILABILITY_ME_1, @@ -181,6 +264,7 @@ pub enum MeAvailability { All = ME_AVAILABILITY_ME_1 | ME_AVAILABILITY_ME_2 | ME_AVAILABILITY_ME_3 | ME_AVAILABILITY_ME_4, } +#[derive(Clone, PartialEq)] pub enum BorderBevel { None = 0, InOut = 1, @@ -188,6 +272,7 @@ pub enum BorderBevel { Out = 3, } +#[derive(Clone, PartialEq)] pub enum IsAtKeyFrame { None = 0, A = 1 << 0, @@ -195,6 +280,7 @@ pub enum IsAtKeyFrame { RunToInfinite = 1 << 2, } +#[derive(Clone, PartialEq)] pub enum Pattern { LeftToRightBar = 0, TopToBottomBar = 1, @@ -216,7 +302,7 @@ pub enum Pattern { TopRightDiagonal = 17, } -#[derive(Clone, Copy)] +#[derive(Clone, Copy, PartialEq)] pub enum MixEffectKeyType { Luma = 0, Chroma = 1, @@ -224,6 +310,7 @@ pub enum MixEffectKeyType { DVE = 3, } +#[derive(Clone, PartialEq)] pub enum FlyKeyKeyFrame { None = 0, A = 1, @@ -232,6 +319,7 @@ pub enum FlyKeyKeyFrame { RunToInfinite = 4, } +#[derive(Clone, PartialEq)] pub enum FlyKeyDirection { CentreOfKey = 0, TopLeft = 1, @@ -245,11 +333,13 @@ pub enum FlyKeyDirection { BottomRight = 9, } +#[derive(Clone, PartialEq)] pub enum SuperSourceArtOption { Background, Foreground, } +#[derive(Clone, PartialEq)] pub enum TransferMode { NoOp, Write, @@ -257,7 +347,9 @@ pub enum TransferMode { WriteAudio = 256, } +#[derive(Clone, Default, PartialEq)] pub enum VideoMode { + #[default] N525i5994NTSC = 0, P625i50PAL = 1, N525i5994169 = 2, @@ -293,12 +385,7 @@ pub enum VideoMode { N1080p60 = 27, } -impl Default for VideoMode { - fn default() -> Self { - VideoMode::N525i5994NTSC - } -} - +#[derive(Clone, PartialEq)] pub enum TransferState { Queued, Locked, @@ -306,29 +393,34 @@ pub enum TransferState { Finished, } +#[derive(Clone, PartialEq)] pub enum MediaSourceType { Still = 1, Clip, } +#[derive(Clone, PartialEq)] pub enum AudioMixOption { Off = 0, On = 1, AudioFollowVideo = 2, } +#[derive(Clone, PartialEq)] pub enum AudioSourceType { ExternalVideo, MediaPlayer, ExternalAudio, } +#[derive(Clone, PartialEq)] pub enum StreamingError { None, InvalidState = 1 << 4, Unknown = 1 << 15, } +#[derive(Clone, PartialEq)] pub enum StreamingStatus { Idle = 1 << 0, Connecting = 1 << 1, @@ -336,6 +428,7 @@ pub enum StreamingStatus { Stopping = 1 << 5, // + Streaming } +#[derive(Clone, PartialEq)] pub enum RecordingError { None = 1 << 1, NoMedia = 0, @@ -346,12 +439,14 @@ pub enum RecordingError { Unknown = 1 << 15, } +#[derive(Clone, PartialEq)] pub enum RecordingStatus { Idle = 0, Recording = 1 << 0, Stopping = 1 << 7, } +#[derive(Clone, PartialEq)] pub enum RecordingDiskStatus { Idle = 1 << 0, Unformatted = 1 << 1, @@ -361,18 +456,21 @@ pub enum RecordingDiskStatus { Removed = 1 << 5, } +#[derive(Clone, PartialEq)] pub enum FairlightAudioMixOption { Off = 1, On = 2, AudioFollowVideo = 4, } +#[derive(Clone, PartialEq)] pub enum FairlightInputConfiguration { Mono = 1, Stereo = 2, DualMono = 4, } +#[derive(Clone, PartialEq)] pub enum FairlightAnalogInputLevel { Microphone = 1, ConsumerLine = 2, @@ -380,11 +478,13 @@ pub enum FairlightAnalogInputLevel { ProLine = 4, } +#[derive(Clone, PartialEq)] pub enum FairlightAudioSourceType { Mono = 0, Stereo = 1, } +#[derive(Clone, PartialEq)] pub enum FairlightInputType { EmbeddedWithVideo = 0, MediaPlayer = 1, @@ -397,6 +497,8 @@ const MULTI_VIEWER_LAYOUT_TOP_LEFT_SMALL: isize = 1; const MULTI_VIEWER_LAYOUT_TOP_RIGHT_SMALL: isize = 2; const MULTI_VIEWER_LAYOUT_BOTTOM_LEFT_SMALL: isize = 4; const MULTI_VIEWER_LAYOUT_BOTTOM_RIGHT_SMALL: isize = 8; + +#[derive(Clone, PartialEq)] pub enum MultiViewerLayout { Default = MULTI_VIEWER_LAYOUT_DEFAULT, TopLeftSmall = MULTI_VIEWER_LAYOUT_TOP_LEFT_SMALL, diff --git a/atem-connection-rs/src/lib.rs b/atem-connection-rs/src/lib.rs index f0a8e48..b82f233 100644 --- a/atem-connection-rs/src/lib.rs +++ b/atem-connection-rs/src/lib.rs @@ -2,12 +2,10 @@ extern crate derive_new; #[macro_use] extern crate derive_getters; -extern crate tokio; -#[macro_use] -extern crate thiserror; pub mod atem; pub mod atem_lib; pub mod commands; pub mod enums; pub mod state; +pub mod tally; diff --git a/atem-connection-rs/src/state/atem_macro.rs b/atem-connection-rs/src/state/atem_macro.rs index 5bbc8f9..4236470 100644 --- a/atem-connection-rs/src/state/atem_macro.rs +++ b/atem-connection-rs/src/state/atem_macro.rs @@ -1,4 +1,4 @@ -#[derive(Getters, new, Default)] +#[derive(Clone, PartialEq, Getters, new, Default)] pub struct MacroPlayerState { pub is_running: bool, pub is_waiting: bool, @@ -6,13 +6,13 @@ pub struct MacroPlayerState { pub macro_index: u64, } -#[derive(Getters, new, Default)] +#[derive(Clone, PartialEq, Getters, new, Default)] pub struct MacroRecorderState { pub is_recording: bool, pub macro_index: u64, } -#[derive(Getters, new)] +#[derive(Clone, PartialEq, Getters, new)] pub struct MacroPropertiesState { is_used: bool, has_unsupported_ops: bool, @@ -20,7 +20,7 @@ pub struct MacroPropertiesState { pub description: String, } -#[derive(Getters, new, Default)] +#[derive(Clone, PartialEq, Getters, new, Default)] pub struct MacroState { pub macro_player: MacroPlayerState, pub macro_recorder: MacroRecorderState, diff --git a/atem-connection-rs/src/state/audio.rs b/atem-connection-rs/src/state/audio.rs index 0e478a7..0255afd 100644 --- a/atem-connection-rs/src/state/audio.rs +++ b/atem-connection-rs/src/state/audio.rs @@ -6,7 +6,7 @@ pub type AudioChannel = ClassicAudioChannel; pub type AudioMasterChannel = ClassicAudioMasterChannel; pub type AtemAudioState = AtemClassicAudioState; -#[derive(Getters, new)] +#[derive(Clone, PartialEq, Getters, new)] pub struct ClassicAudioChannel { source_type: AudioSourceType, pub port_type: ExternalPortType, @@ -18,14 +18,14 @@ pub struct ClassicAudioChannel { pub rca_to_xlr_enabled: bool, } -#[derive(Getters, new)] +#[derive(Clone, PartialEq, Getters, new)] pub struct ClassicAudioMasterChannel { pub gain: f64, pub balance: f64, pub follow_fade_to_black: bool, } -#[derive(Getters, new)] +#[derive(Clone, PartialEq, Getters, new)] pub struct ClassicAudioMonitorChannel { pub enabled: bool, pub gain: f64, @@ -36,7 +36,7 @@ pub struct ClassicAudioMonitorChannel { pub dim_level: f64, } -#[derive(Getters, new)] +#[derive(Clone, PartialEq, Getters, new)] pub struct ClassicAudioHeadphoneOutputChannel { pub gain: f64, pub program_out_gain: f64, @@ -44,14 +44,28 @@ pub struct ClassicAudioHeadphoneOutputChannel { pub talkback_gain: f64, } -#[derive(Getters, new)] +#[derive(Clone, PartialEq, Getters)] pub struct AtemClassicAudioState { - number_of_channels: Option, - has_monitor: Option, - pub channels: HashMap, + number_of_channels: u8, + has_monitor: bool, + pub channels: HashMap, pub monitor: Option, pub headphones: Option, pub master: Option, - pub audio_follow_video_crossfade_transition_enabled: Option, + pub audio_follow_video_crossfade_transition_enabled: bool, +} + +impl AtemClassicAudioState { + pub fn new(number_of_channels: u8, has_monitor: bool) -> Self { + Self { + number_of_channels, + has_monitor, + channels: Default::default(), + monitor: Default::default(), + headphones: Default::default(), + master: Default::default(), + audio_follow_video_crossfade_transition_enabled: false, + } + } } diff --git a/atem-connection-rs/src/state/color.rs b/atem-connection-rs/src/state/color.rs index 4f8acb1..9fd9290 100644 --- a/atem-connection-rs/src/state/color.rs +++ b/atem-connection-rs/src/state/color.rs @@ -1,6 +1,6 @@ -#[derive(Getters, new)] +#[derive(Clone, PartialEq, Getters, new)] pub struct ColorGeneratorState { pub hue: u64, pub saturation: u64, - pub luma: u64 + pub luma: u64, } diff --git a/atem-connection-rs/src/state/common.rs b/atem-connection-rs/src/state/common.rs index 11b2d98..aaf2f02 100644 --- a/atem-connection-rs/src/state/common.rs +++ b/atem-connection-rs/src/state/common.rs @@ -1,4 +1,4 @@ -#[derive(Getters, new)] +#[derive(Clone, PartialEq, Getters, new)] pub struct Timecode { pub hours: u64, pub minutes: u64, diff --git a/atem-connection-rs/src/state/fairlight.rs b/atem-connection-rs/src/state/fairlight.rs index 3bbedfc..ba97e30 100644 --- a/atem-connection-rs/src/state/fairlight.rs +++ b/atem-connection-rs/src/state/fairlight.rs @@ -1,36 +1,39 @@ use std::collections::HashMap; -use crate::enums::{ExternalPortType, FairlightAnalogInputLevel, FairlightAudioMixOption, FairlightAudioSourceType, FairlightInputConfiguration, FairlightInputType}; +use crate::enums::{ + ExternalPortType, FairlightAnalogInputLevel, FairlightAudioMixOption, FairlightAudioSourceType, + FairlightInputConfiguration, FairlightInputType, +}; -#[derive(Getters, new)] +#[derive(Clone, PartialEq, Getters, new)] pub struct FairlightAudioDynamicsState { pub make_up_gain: Option, - + pub limiter: Option, pub compressor: Option, - pub expander: Option + pub expander: Option, } -#[derive(Getters, new)] +#[derive(Clone, PartialEq, Getters, new)] pub struct FairlightAudioLimiterState { pub limiter_enabled: bool, pub threshold: u64, pub attack: u64, pub hold: u64, - pub release: u64 + pub release: u64, } -#[derive(Getters, new)] +#[derive(Clone, PartialEq, Getters, new)] pub struct FairlightAudioCompressorState { pub compressor_enabled: bool, pub threshold: u64, pub ratio: u64, pub attack: u64, pub hold: u64, - pub release: u64 + pub release: u64, } -#[derive(Getters, new)] +#[derive(Clone, PartialEq, Getters, new)] pub struct FairlightAudioExpanderState { pub expander_enabled: bool, pub gate_enabled: bool, @@ -39,33 +42,33 @@ pub struct FairlightAudioExpanderState { pub ratio: u64, pub attack: u64, pub hold: u64, - pub release: u64 + pub release: u64, } -#[derive(Getters, new)] +#[derive(Clone, PartialEq, Getters, new)] pub struct FairlightAudioEqualizerBandState { pub band_enabled: bool, supported_shapes: Vec, // TODO - pub shape: u64, // TODO + pub shape: u64, // TODO supported_frequency_ranges: Vec, // TODO - pub frequency_ranges: u64, // TODO + pub frequency_ranges: u64, // TODO pub frequency: u64, pub gain: u64, - pub q_factor: u64 + pub q_factor: u64, } -#[derive(Getters, new)] +#[derive(Clone, PartialEq, Getters, new)] pub struct FairlightAudioMasterChannelPropertiesState { // Gain in decibel, -Infinity to +6dB pub fader_gain: u64, - pub follow_fade_to_black: bool + pub follow_fade_to_black: bool, } -#[derive(Getters, new)] +#[derive(Clone, PartialEq, Getters, new)] pub struct FairlightAudioMasterChannel { pub properties: Option, @@ -73,29 +76,29 @@ pub struct FairlightAudioMasterChannel { pub dynamicss: Option, } -#[derive(Getters, new)] +#[derive(Clone, PartialEq, Getters, new)] pub struct FairlightAudioMonitorChannel { pub gain: u64, pub input_master_gain: u64, pub input_talkback_gain: u64, - pub input_sidetone_gain: u64 + pub input_sidetone_gain: u64, } -#[derive(Getters, new)] +#[derive(Clone, PartialEq, Getters, new)] pub struct FairlightAudioSource { pub properties: Option, pub equalizer: Option, - pub dynamics: Option + pub dynamics: Option, } -#[derive(Getters, new)] +#[derive(Clone, PartialEq, Getters, new)] pub struct FairlightAudioEqualizerState { pub enabled: bool, pub gain: u64, - bands: Vec + bands: Vec, } -#[derive(Getters, new)] +#[derive(Clone, PartialEq, Getters, new)] pub struct FairlightAudioSourcePropertiesState { source_type: FairlightAudioSourceType, @@ -110,16 +113,16 @@ pub struct FairlightAudioSourcePropertiesState { pub fader_gain: u64, supported_mix_options: Vec, - pub mix_option: FairlightAudioMixOption + pub mix_option: FairlightAudioMixOption, } -#[derive(Getters, new)] +#[derive(Clone, PartialEq, Getters, new)] pub struct FairlightAudioInput { pub properties: Option, - pub sources: HashMap + pub sources: HashMap, } -#[derive(Getters, new)] +#[derive(Clone, PartialEq, Getters, new)] pub struct FairlightAudioInputProperties { input_type: FairlightInputType, external_port_type: ExternalPortType, @@ -128,14 +131,14 @@ pub struct FairlightAudioInputProperties { pub active_configuration: FairlightInputConfiguration, supported_input_levels: Vec, - pub activeInputLevel: FairlightAnalogInputLevel + pub active_input_level: FairlightAnalogInputLevel, } -#[derive(Getters, new)] +#[derive(Clone, PartialEq, Getters, new)] pub struct AtemFairlightAudioState { pub inputs: HashMap, pub master: Option, pub monitor: Option, - pub audio_follow_video_crossfade_transition_enabled: Option + pub audio_follow_video_crossfade_transition_enabled: Option, } diff --git a/atem-connection-rs/src/state/info.rs b/atem-connection-rs/src/state/info.rs index 4620c73..c97f2be 100644 --- a/atem-connection-rs/src/state/info.rs +++ b/atem-connection-rs/src/state/info.rs @@ -1,65 +1,65 @@ use crate::enums::{Model, ProtocolVersion}; -#[derive(Getters, new)] +#[derive(Clone, PartialEq, Getters, new)] pub struct AtemCapabilites { - mix_effects: u64, - sources: u64, - auxilliaries: u64, - mix_minus_outputs: u64, - media_players: u64, - serial_ports: u64, - max_hyperdecks: u64, - dves: u64, - stingers: u64, - super_sources: u64, - talkback_channels: u64, - downstream_keyers: u64, + mix_effects: u8, + sources: u8, + auxilliaries: u8, + mix_minus_outputs: u8, + media_players: u8, + serial_ports: u8, + max_hyperdecks: u8, + dves: u8, + stingers: u8, + super_sources: u8, + talkback_channels: u8, + downstream_keyers: u8, camera_control: bool, advanced_chroma_keyers: bool, only_configurable_outputs: bool, } -#[derive(Getters, new)] +#[derive(Clone, PartialEq, Getters, new)] pub struct MixEffectInfo { - key_count: u64, + key_count: u8, } -#[derive(Getters, new)] +#[derive(Clone, PartialEq, Getters, new)] pub struct SuperSourceInfo { box_count: u64, } -#[derive(Getters, new)] +#[derive(Clone, PartialEq, Getters, new)] pub struct AudioMixerInfo { - inputs: u64, - monitors: u64, - headphones: u64, + inputs: u8, + monitors: u8, + headphones: u8, } -#[derive(Getters, new)] +#[derive(Clone, PartialEq, Getters, new)] pub struct FairlightAudioMixerInfo { inputs: u64, monitors: u64, } -#[derive(Getters, new)] +#[derive(Clone, PartialEq, Getters, new)] pub struct MacroPoolInfo { macro_count: u64, } -#[derive(Getters, new)] +#[derive(Clone, PartialEq, Getters, new)] pub struct MediaPoolInfo { - still_count: u64, - clip_count: u64, + still_count: u8, + clip_count: u8, } -#[derive(Getters, new)] +#[derive(Clone, PartialEq, Getters, new)] pub struct MultiviewerInfo { - count: u64, - window_count: u64, + count: Option, + window_count: u8, } -#[derive(new)] +#[derive(Clone, PartialEq, new)] pub struct TimeInfo { pub hour: u64, pub minute: u64, @@ -68,7 +68,7 @@ pub struct TimeInfo { pub drop_frame: bool, } -#[derive(new, Default)] +#[derive(Clone, PartialEq, Getters, new, Default)] pub struct DeviceInfo { pub api_version: ProtocolVersion, pub capabilities: Option, @@ -82,5 +82,5 @@ pub struct DeviceInfo { pub macro_pool: Option, pub media_pool: Option, pub multiviewer: Option, - pub lastTime: Option, + pub last_time: Option, } diff --git a/atem-connection-rs/src/state/input.rs b/atem-connection-rs/src/state/input.rs index 5fd2f0b..0c3c654 100644 --- a/atem-connection-rs/src/state/input.rs +++ b/atem-connection-rs/src/state/input.rs @@ -1,6 +1,6 @@ use crate::enums::{ExternalPortType, InternalPortType, MeAvailability, SourceAvailability}; -#[derive(Getters, new)] +#[derive(Clone, PartialEq, Getters, new)] pub struct InputChannel { input_id: u64, pub long_name: String, @@ -10,5 +10,5 @@ pub struct InputChannel { pub external_port_type: ExternalPortType, internal_port_type: InternalPortType, source_availability: SourceAvailability, - me_availability: MeAvailability + me_availability: MeAvailability, } diff --git a/atem-connection-rs/src/state/media.rs b/atem-connection-rs/src/state/media.rs index f71788f..f32824e 100644 --- a/atem-connection-rs/src/state/media.rs +++ b/atem-connection-rs/src/state/media.rs @@ -1,6 +1,6 @@ use crate::enums; -#[derive(Getters, new)] +#[derive(Clone, PartialEq, Getters, new)] pub struct MediaPlayer { pub playing: bool, pub is_loop: bool, @@ -8,7 +8,7 @@ pub struct MediaPlayer { pub clip_frame: u64, } -#[derive(Getters, new)] +#[derive(Clone, PartialEq, Getters, new)] pub struct MediaPlayerSource { pub source_type: enums::MediaSourceType, pub clip_index: u64, @@ -17,21 +17,21 @@ pub struct MediaPlayerSource { pub type MediaPlayerState = (MediaPlayer, MediaPlayerSource); -#[derive(Getters, new, Default)] +#[derive(Clone, PartialEq, Getters, new, Default)] pub struct MediaState { still_pool: Vec, clip_pool: Vec, players: Vec, } -#[derive(Getters, new)] +#[derive(Clone, PartialEq, Getters, new)] pub struct StillFrame { pub is_used: bool, pub hash: String, pub file_name: String, } -#[derive(Getters, new)] +#[derive(Clone, PartialEq, Getters, new)] pub struct ClipBank { pub is_used: bool, pub name: String, diff --git a/atem-connection-rs/src/state/mod.rs b/atem-connection-rs/src/state/mod.rs index ea46c34..407a300 100644 --- a/atem-connection-rs/src/state/mod.rs +++ b/atem-connection-rs/src/state/mod.rs @@ -14,18 +14,18 @@ pub mod streaming; pub mod util; pub mod video; -#[derive(Default)] +#[derive(Default, Clone, PartialEq)] pub struct AtemState { - info: info::DeviceInfo, - video: video::AtemVideoState, - audio: Option, - fairlight: Option, - media: media::MediaState, - inputs: HashMap, + pub info: info::DeviceInfo, + pub video: video::AtemVideoState, + pub audio: Option, + pub fairlight: Option, + pub media: media::MediaState, + pub inputs: HashMap, // macro is a rust keyword - atem_macro: atem_macro::MacroState, - settings: settings::SettingsState, - recording: Option, - streaming: Option, - color_generators: HashMap, + pub atem_macro: atem_macro::MacroState, + pub settings: settings::SettingsState, + pub recording: Option, + pub streaming: Option, + pub color_generators: HashMap, } diff --git a/atem-connection-rs/src/state/recording.rs b/atem-connection-rs/src/state/recording.rs index 3cafa4b..4aa8220 100644 --- a/atem-connection-rs/src/state/recording.rs +++ b/atem-connection-rs/src/state/recording.rs @@ -4,38 +4,38 @@ use crate::enums::{RecordingDiskStatus, RecordingError, RecordingStatus}; use super::common::Timecode; -#[derive(Getters, new)] +#[derive(Clone, PartialEq, Getters, new)] pub struct RecordingState { pub status: Option, pub properties: RecordingStateProperties, pub duration: Option, - pub disks: HashMap + pub disks: HashMap, } -#[derive(Getters, new)] +#[derive(Clone, PartialEq, Getters, new)] pub struct RecordingDiskProperties { pub disk_id: u64, pub volume_name: String, pub recording_time_available: u64, - pub status: RecordingDiskStatus + pub status: RecordingDiskStatus, } -#[derive(Getters, new)] +#[derive(Clone, PartialEq, Getters, new)] pub struct RecordingStateStatus { pub state: RecordingStatus, pub error: RecordingError, - pub recording_time_available: u64 + pub recording_time_available: u64, } -#[derive(Getters, new)] +#[derive(Clone, PartialEq, Getters, new)] pub struct RecordingStateProperties { pub filename: String, pub working_set_1_disk_id: u64, pub working_set_2_disk_id: u64, - pub record_in_all_cameras: bool + pub record_in_all_cameras: bool, } diff --git a/atem-connection-rs/src/state/settings.rs b/atem-connection-rs/src/state/settings.rs index ae4450c..2f601cc 100644 --- a/atem-connection-rs/src/state/settings.rs +++ b/atem-connection-rs/src/state/settings.rs @@ -9,7 +9,7 @@ pub trait MultiViewerSourceState { fn get_supports_safe_area(&self) -> bool; } -#[derive(Getters, new)] +#[derive(Clone, PartialEq, Getters, new)] pub struct MultiViewerWindowState { pub safe_title: Option, pub audio_meter: Option, @@ -40,13 +40,13 @@ impl MultiViewerSourceState for MultiViewerWindowState { } } -#[derive(Getters, new)] +#[derive(Clone, PartialEq, Getters, new)] pub struct MultiViewerPropertiesState { pub layout: MultiViewerLayout, pub program_preview_swapped: bool, } -#[derive(Getters, new)] +#[derive(Clone, PartialEq, Getters, new)] pub struct MultiViewer { index: u64, windows: Vec, @@ -54,7 +54,7 @@ pub struct MultiViewer { pub vu_opacity: Option, } -#[derive(Getters, new, Default)] +#[derive(Clone, PartialEq, Getters, new, Default)] pub struct SettingsState { multi_viewers: Vec, pub video_mode: VideoMode, diff --git a/atem-connection-rs/src/state/streaming.rs b/atem-connection-rs/src/state/streaming.rs index 5608ec1..c94644b 100644 --- a/atem-connection-rs/src/state/streaming.rs +++ b/atem-connection-rs/src/state/streaming.rs @@ -2,31 +2,32 @@ use crate::enums::{StreamingError, StreamingStatus}; use super::common::Timecode; -#[derive(Getters)] +#[derive(Clone, PartialEq, Getters)] pub struct StreamingState { pub status: Option, pub stats: Option, pub service: StreamingServiceProperties, - pub duration: Option + pub duration: Option, } -#[derive(Getters, new)] +#[derive(Clone, PartialEq, Getters, new)] pub struct StreamingStateStatus { state: StreamingStatus, - error: StreamingError + error: StreamingError, } -#[derive(Getters, new)] +#[derive(Clone, PartialEq, Getters, new)] pub struct StreamingStateStats { cache_used: u64, - encoding_bitrate: u64 + encoding_bitrate: u64, } +#[derive(Clone, PartialEq)] pub struct StreamingServiceProperties { pub service_name: String, pub url: String, pub key: String, - bitrates: (u64, u64) + bitrates: (u64, u64), } diff --git a/atem-connection-rs/src/state/util.rs b/atem-connection-rs/src/state/util.rs index 914821d..776e7b5 100644 --- a/atem-connection-rs/src/state/util.rs +++ b/atem-connection-rs/src/state/util.rs @@ -1,9 +1,41 @@ -use super::{settings::MultiViewer, AtemState}; +use crate::enums::{TransitionSelection, TransitionStyle}; + +use super::{ + settings::MultiViewer, + video::{MixEffect, TransitionPosition, TransitionProperties, TransitionSettings}, + AtemState, +}; pub fn create() -> AtemState { AtemState::default() } +pub fn get_mix_effect(state: &mut AtemState, index: usize) -> &mut MixEffect { + // TODO: Use of index here is terrible and dangerous + + if state.video.mix_effects().get(index).is_none() { + let mix_effect = MixEffect::new( + index, + 0, + 0, + false, + None, + TransitionPosition::new(false, 0.0, 0.0), + TransitionProperties::new( + TransitionStyle::MIX, + vec![TransitionSelection::Background], + TransitionStyle::MIX, + vec![TransitionSelection::Background], + ), + TransitionSettings::new(None, None, None, None, None), + vec![], + ); + state.video.mix_effects_mut()[index] = mix_effect.clone(); + }; + + &mut state.video.mix_effects_mut()[index] +} + pub fn get_multi_viewer(state: &mut AtemState, index: usize) -> Option<&MultiViewer> { state.settings.multi_viewers().get(index) } diff --git a/atem-connection-rs/src/state/video/downstream_keyers.rs b/atem-connection-rs/src/state/video/downstream_keyers.rs index 0eb9433..06d244d 100644 --- a/atem-connection-rs/src/state/video/downstream_keyers.rs +++ b/atem-connection-rs/src/state/video/downstream_keyers.rs @@ -9,7 +9,7 @@ pub trait DownstreamKeyerBase { fn set_is_towards_on_air(&mut self, on_air: Option); } -#[derive(Getters, new)] +#[derive(Clone, PartialEq, Getters, new)] pub struct DownstreamKeyer { pub sources: Option, pub properties: Option, @@ -57,7 +57,7 @@ pub trait DownstreamKeyerGeneral { fn set_invert(&mut self, invert: bool); } -#[derive(Getters, new)] +#[derive(Clone, PartialEq, Getters, new)] pub struct DownstreamKeyerMask { pub enabled: bool, pub top: f64, @@ -66,7 +66,7 @@ pub struct DownstreamKeyerMask { pub right: f64, } -#[derive(Getters, new)] +#[derive(Clone, PartialEq, Getters, new)] pub struct DownstreamKeyerProperties { pub tie: bool, pub rate: f64, @@ -104,7 +104,7 @@ impl DownstreamKeyerGeneral for DownstreamKeyerProperties { } } -#[derive(Getters, new)] +#[derive(Clone, PartialEq, Getters, new)] pub struct DownstreamKeyerSources { pub fill_source: f64, pub cut_source: f64, diff --git a/atem-connection-rs/src/state/video/mod.rs b/atem-connection-rs/src/state/video/mod.rs index 57fb684..fba70b5 100644 --- a/atem-connection-rs/src/state/video/mod.rs +++ b/atem-connection-rs/src/state/video/mod.rs @@ -4,13 +4,13 @@ mod downstream_keyers; mod super_source; mod upstream_keyers; -#[derive(Getters, new)] +#[derive(Clone, PartialEq, Getters, new)] pub struct DipTransitionSettings { pub rate: f64, pub input: f64, } -#[derive(Getters, new)] +#[derive(Clone, PartialEq, Getters, new)] pub struct DVETransitionSettings { pub rate: f64, pub logo_rate: f64, @@ -27,12 +27,12 @@ pub struct DVETransitionSettings { pub flip_flop: bool, } -#[derive(Getters, new)] +#[derive(Clone, PartialEq, Getters, new)] pub struct MixTransitionSettings { pub rate: f64, } -#[derive(Getters, new)] +#[derive(Clone, PartialEq, Getters, new)] pub struct StingerTransitionSettings { pub source: f64, pub pre_multiplied_key: bool, @@ -47,7 +47,7 @@ pub struct StingerTransitionSettings { pub mix_rate: f64, } -#[derive(Getters, new)] +#[derive(Clone, PartialEq, Getters, new)] pub struct WipeTransitionSettings { pub rate: f64, pub pattern: f64, @@ -61,7 +61,7 @@ pub struct WipeTransitionSettings { pub flip_flop: bool, } -#[derive(Getters, new)] +#[derive(Clone, PartialEq, Getters, new)] pub struct TransitionProperties { style: enums::TransitionStyle, selection: Vec, @@ -69,7 +69,7 @@ pub struct TransitionProperties { pub next_selection: Vec, } -#[derive(Getters, new)] +#[derive(Clone, PartialEq, Getters, new)] pub struct TransitionSettings { pub dip: Option, pub dve: Option, @@ -78,18 +78,18 @@ pub struct TransitionSettings { pub wipe: Option, } -#[derive(Getters, new)] +#[derive(Clone, PartialEq, Getters, new)] pub struct TransitionPosition { in_transition: bool, remaining_frames: f64, pub handle_position: f64, } -#[derive(Getters, new)] +#[derive(Clone, PartialEq, Getters, new)] pub struct MixEffect { - index: f64, - pub program_input: f64, - pub preview_input: f64, + index: usize, + pub program_input: u16, + pub preview_input: u16, pub transition_preview: bool, pub fade_to_black: Option, pub transition_position: TransitionPosition, @@ -98,7 +98,7 @@ pub struct MixEffect { upstream_keyers: Vec, } -#[derive(Getters, new)] +#[derive(Clone, PartialEq, Getters, new)] pub struct FadeToBlackProperties { is_fully_black: bool, in_transition: bool, @@ -106,10 +106,16 @@ pub struct FadeToBlackProperties { pub rate: f64, } -#[derive(Getters, new, Default)] +#[derive(Clone, PartialEq, Getters, new, Default)] pub struct AtemVideoState { mix_effects: Vec, downstream_keyers: Vec, auxiliaries: Vec, super_sources: Vec, } + +impl AtemVideoState { + pub fn mix_effects_mut(&mut self) -> &mut Vec { + &mut self.mix_effects + } +} diff --git a/atem-connection-rs/src/state/video/super_source.rs b/atem-connection-rs/src/state/video/super_source.rs index 400cb2b..92d85cd 100644 --- a/atem-connection-rs/src/state/video/super_source.rs +++ b/atem-connection-rs/src/state/video/super_source.rs @@ -1,6 +1,6 @@ use crate::enums; -#[derive(Getters, new)] +#[derive(Clone, PartialEq, Getters, new)] pub struct SuperSourceBox { pub enabled: bool, pub source: f64, @@ -14,7 +14,7 @@ pub struct SuperSourceBox { pub crop_right: f64, } -#[derive(Getters, new)] +#[derive(Clone, PartialEq, Getters, new)] pub struct SuperSourceProperties { pub art_fill_source: f64, pub art_cut_source: f64, @@ -25,7 +25,7 @@ pub struct SuperSourceProperties { pub art_invert_key: bool, } -#[derive(Getters, new)] +#[derive(Clone, PartialEq, Getters, new)] pub struct SuperSourceBorder { pub border_enabled: bool, pub border_bevel: enums::BorderBevel, @@ -42,7 +42,7 @@ pub struct SuperSourceBorder { pub border_light_source_altitude: f64, } -#[derive(Getters, new)] +#[derive(Clone, PartialEq, Getters, new)] pub struct SuperSource { index: f64, boxes: [Option; 4], diff --git a/atem-connection-rs/src/state/video/upstream_keyers.rs b/atem-connection-rs/src/state/video/upstream_keyers.rs index f8c39aa..43ab537 100644 --- a/atem-connection-rs/src/state/video/upstream_keyers.rs +++ b/atem-connection-rs/src/state/video/upstream_keyers.rs @@ -16,57 +16,7 @@ pub trait UpstreamKeyerTypeSettings { fn set_fly_enabled(&mut self, enabled: bool); } -pub trait UpstreamKeyerMaskSettings { - fn get_mask_enabled(&self) -> bool; - fn set_mask_enabled(&mut self, enabled: bool); - fn get_mask_top(&self) -> f64; - fn set_mask_top(&mut self, mask: f64); - fn get_mask_bottom(&self) -> f64; - fn set_mask_bottom(&mut self, mask: f64); - fn get_mask_left(&self) -> f64; - fn set_mask_right(&mut self, mask: f64); -} - -pub trait UpstreamKeyerDVEBase: UpstreamKeyerMaskSettings { - fn get_size_x(&self) -> f64; - fn set_size_x(&mut self, size_x: f64); - fn get_size_y(&self) -> f64; - fn set_size_y(&mut self, size_y: f64); - fn get_position_x(&self) -> f64; - fn set_position_x(&mut self, position_x: f64); - fn get_position_y(&self) -> f64; - fn set_position_y(&mut self, position_y: f64); - fn get_rotation(&self) -> f64; - fn set_rotation(&mut self, rotation: f64); - - fn get_border_outer_width(&self) -> f64; - fn set_border_outer_width(&mut self, width: f64); - fn get_border_inner_width(&self) -> f64; - fn set_border_inner_width(&mut self, width: f64); - fn get_border_outer_softness(&self) -> f64; - fn set_border_outer_softness(&mut self, softness: f64); - fn get_border_inner_softness(&self) -> f64; - fn set_border_inner_softness(&mut self, softness: f64); - fn get_border_bevel_softness(&self) -> f64; - fn set_border_bevel_softness(&mut self, softness: f64); - fn get_border_bevel_position(&self) -> f64; - fn set_border_bevel_position(&mut self, position: f64); - - fn get_border_opacity(&self) -> f64; - fn set_border_opacity(&mut self, opacity: f64); - fn get_border_hue(&self) -> f64; - fn set_border_hue(&mut self, hue: f64); - fn get_border_saturation(&self) -> f64; - fn set_border_saturation(&mut self, saturation: f64); - fn get_border_luma(&self) -> f64; - fn set_border_luma(&mut self, luma: f64); - - fn get_light_source_direction(&self) -> f64; - fn set_light_source_direction(&mut self, direction: f64); - fn get_light_source_altitude(&self) -> f64; - fn set_light_source_altitude(&mut self, altitude: f64); -} - +#[derive(Clone, PartialEq)] pub struct UpstreamKeyerDVESettings { pub border_enabled: bool, pub shadow_enabled: bool, @@ -101,178 +51,7 @@ pub struct UpstreamKeyerDVESettings { pub light_source_altitude: f64, } -impl UpstreamKeyerMaskSettings for UpstreamKeyerDVESettings { - fn get_mask_enabled(&self) -> bool { - self.mask_enabled - } - - fn set_mask_enabled(&mut self, enabled: bool) { - self.mask_enabled = enabled - } - - fn get_mask_top(&self) -> f64 { - self.mask_top - } - - fn set_mask_top(&mut self, mask: f64) { - self.mask_top = mask - } - - fn get_mask_bottom(&self) -> f64 { - self.mask_bottom - } - - fn set_mask_bottom(&mut self, mask: f64) { - self.mask_bottom = mask - } - - fn get_mask_left(&self) -> f64 { - self.mask_left - } - - fn set_mask_right(&mut self, mask: f64) { - self.mask_left = mask - } -} - -impl UpstreamKeyerDVEBase for UpstreamKeyerDVESettings { - fn get_size_x(&self) -> f64 { - self.size_x - } - - fn set_size_x(&mut self, size_x: f64) { - self.size_x = size_x - } - - fn get_size_y(&self) -> f64 { - self.size_y - } - - fn set_size_y(&mut self, size_y: f64) { - self.size_y = size_y - } - - fn get_position_x(&self) -> f64 { - self.position_x - } - - fn set_position_x(&mut self, position_x: f64) { - self.position_x = position_x - } - - fn get_position_y(&self) -> f64 { - self.position_y - } - - fn set_position_y(&mut self, position_y: f64) { - self.position_y = position_y - } - - fn get_rotation(&self) -> f64 { - self.rotation - } - - fn set_rotation(&mut self, rotation: f64) { - self.rotation = rotation - } - - fn get_border_outer_width(&self) -> f64 { - self.border_outer_width - } - - fn set_border_outer_width(&mut self, width: f64) { - self.border_outer_width = width - } - - fn get_border_inner_width(&self) -> f64 { - self.border_inner_width - } - - fn set_border_inner_width(&mut self, width: f64) { - self.border_inner_width = width - } - - fn get_border_outer_softness(&self) -> f64 { - self.border_outer_softness - } - - fn set_border_outer_softness(&mut self, softness: f64) { - self.border_outer_softness = softness - } - - fn get_border_inner_softness(&self) -> f64 { - self.border_inner_softness - } - - fn set_border_inner_softness(&mut self, softness: f64) { - self.border_inner_softness = softness - } - - fn get_border_bevel_softness(&self) -> f64 { - self.border_bevel_softness - } - - fn set_border_bevel_softness(&mut self, softness: f64) { - self.border_bevel_softness = softness - } - - fn get_border_bevel_position(&self) -> f64 { - self.border_bevel_position - } - - fn set_border_bevel_position(&mut self, position: f64) { - self.border_bevel_position = position - } - - fn get_border_opacity(&self) -> f64 { - self.border_opacity - } - - fn set_border_opacity(&mut self, opacity: f64) { - self.border_opacity = opacity - } - - fn get_border_hue(&self) -> f64 { - self.border_hue - } - - fn set_border_hue(&mut self, hue: f64) { - self.border_hue = hue - } - - fn get_border_saturation(&self) -> f64 { - self.border_saturation - } - - fn set_border_saturation(&mut self, saturation: f64) { - self.border_saturation = saturation - } - - fn get_border_luma(&self) -> f64 { - self.border_luma - } - - fn set_border_luma(&mut self, luma: f64) { - self.border_luma = luma - } - - fn get_light_source_direction(&self) -> f64 { - self.light_source_direction - } - - fn set_light_source_direction(&mut self, direction: f64) { - self.light_source_direction = direction - } - - fn get_light_source_altitude(&self) -> f64 { - self.light_source_altitude - } - - fn set_light_source_altitude(&mut self, altitude: f64) { - self.light_source_altitude = altitude - } -} - +#[derive(Clone, PartialEq)] pub struct UpstreamKeyerFlyKeyFrame { key_frame_id: f64, @@ -304,178 +83,13 @@ pub struct UpstreamKeyerFlyKeyFrame { pub light_source_altitude: f64, } -impl UpstreamKeyerMaskSettings for UpstreamKeyerFlyKeyFrame { - fn get_mask_enabled(&self) -> bool { - self.mask_enabled - } - - fn set_mask_enabled(&mut self, enabled: bool) { - self.mask_enabled = enabled - } - - fn get_mask_top(&self) -> f64 { - self.mask_top - } - - fn set_mask_top(&mut self, mask: f64) { - self.mask_top = mask - } - - fn get_mask_bottom(&self) -> f64 { - self.mask_bottom - } - - fn set_mask_bottom(&mut self, mask: f64) { - self.mask_bottom = mask - } - - fn get_mask_left(&self) -> f64 { - self.mask_left - } - - fn set_mask_right(&mut self, mask: f64) { - self.mask_left = mask - } -} - -impl UpstreamKeyerDVEBase for UpstreamKeyerFlyKeyFrame { - fn get_size_x(&self) -> f64 { - self.size_x - } - - fn set_size_x(&mut self, size_x: f64) { - self.size_x = size_x - } - - fn get_size_y(&self) -> f64 { - self.size_y - } - - fn set_size_y(&mut self, size_y: f64) { - self.size_y = size_y - } - - fn get_position_x(&self) -> f64 { - self.position_x - } - - fn set_position_x(&mut self, position_x: f64) { - self.position_x = position_x - } - - fn get_position_y(&self) -> f64 { - self.position_y - } - - fn set_position_y(&mut self, position_y: f64) { - self.position_y = position_y - } - - fn get_rotation(&self) -> f64 { - self.rotation - } - - fn set_rotation(&mut self, rotation: f64) { - self.rotation = rotation - } - - fn get_border_outer_width(&self) -> f64 { - self.border_outer_width - } - - fn set_border_outer_width(&mut self, width: f64) { - self.border_outer_width = width - } - - fn get_border_inner_width(&self) -> f64 { - self.border_inner_width - } - - fn set_border_inner_width(&mut self, width: f64) { - self.border_inner_width = width - } - - fn get_border_outer_softness(&self) -> f64 { - self.border_outer_softness - } - - fn set_border_outer_softness(&mut self, softness: f64) { - self.border_outer_softness = softness - } - - fn get_border_inner_softness(&self) -> f64 { - self.border_inner_softness - } - - fn set_border_inner_softness(&mut self, softness: f64) { - self.border_inner_softness = softness - } - - fn get_border_bevel_softness(&self) -> f64 { - self.border_bevel_softness - } - - fn set_border_bevel_softness(&mut self, softness: f64) { - self.border_bevel_softness = softness - } - - fn get_border_bevel_position(&self) -> f64 { - self.border_bevel_position - } - - fn set_border_bevel_position(&mut self, position: f64) { - self.border_bevel_position = position - } - - fn get_border_opacity(&self) -> f64 { - self.border_opacity - } - - fn set_border_opacity(&mut self, opacity: f64) { - self.border_opacity = opacity - } - - fn get_border_hue(&self) -> f64 { - self.border_hue - } - - fn set_border_hue(&mut self, hue: f64) { - self.border_hue = hue - } - - fn get_border_saturation(&self) -> f64 { - self.border_saturation - } - - fn set_border_saturation(&mut self, saturation: f64) { - self.border_saturation = saturation - } - - fn get_border_luma(&self) -> f64 { - self.border_luma - } - - fn set_border_luma(&mut self, luma: f64) { - self.border_luma = luma - } - - fn get_light_source_direction(&self) -> f64 { - self.light_source_direction - } - - fn set_light_source_direction(&mut self, direction: f64) { - self.light_source_direction = direction - } - - fn get_light_source_altitude(&self) -> f64 { - self.light_source_altitude - } - - fn set_light_source_altitude(&mut self, altitude: f64) { - self.light_source_altitude = altitude - } +#[derive(Clone, PartialEq)] +pub enum UpstreamKeyerMaskSettings { + DVE(UpstreamKeyerDVESettings), + FlyKeyFrame(UpstreamKeyerFlyKeyFrame), } +#[derive(Clone, PartialEq)] pub struct UpstreamKeyerChromaSettings { pub hue: f64, pub gain: f64, @@ -484,11 +98,13 @@ pub struct UpstreamKeyerChromaSettings { pub narrow: bool, } +#[derive(Clone, PartialEq)] pub struct UpstreamKeyerAdvancedChromaSettings { pub properties: Option, pub sample: Option, } +#[derive(Clone, PartialEq)] pub struct UpstreamKeyerAdvancedChromaProperties { pub foreground_level: f64, pub background_level: f64, @@ -505,6 +121,7 @@ pub struct UpstreamKeyerAdvancedChromaProperties { pub blue: f64, } +#[derive(Clone, PartialEq)] pub struct UpstreamKeyerAdvancedChromaSample { pub enable_cursor: bool, pub preview: bool, @@ -516,6 +133,7 @@ pub struct UpstreamKeyerAdvancedChromaSample { pub sampled_cr: f64, } +#[derive(Clone, PartialEq)] pub struct UpstreamKeyerLumaSettings { pub pre_multiplied: bool, pub clip: f64, @@ -523,6 +141,7 @@ pub struct UpstreamKeyerLumaSettings { pub invert: bool, } +#[derive(Clone, PartialEq)] pub struct UpstreamKeyerPatternSettings { pub style: enums::Pattern, pub size: f64, @@ -533,6 +152,7 @@ pub struct UpstreamKeyerPatternSettings { pub invert: bool, } +#[derive(Clone, PartialEq)] pub struct UpstreamKeyerFlySettings { is_a_set: bool, is_b_set: bool, @@ -540,6 +160,7 @@ pub struct UpstreamKeyerFlySettings { run_to_infinite_index: f64, } +#[derive(Clone, PartialEq)] pub struct UpstreamKeyer { pub mix_effect_key_type: enums::MixEffectKeyType, pub fly_enabled: bool, @@ -556,7 +177,7 @@ pub struct UpstreamKeyer { pub pattern_settings: Option, pub fly_keyframes: [Option; 2], pub fly_properties: Option, - pub mask_settings: Box, + pub mask_settings: UpstreamKeyerMaskSettings, pub on_air: bool, pub mask_enabled: bool, diff --git a/atem-connection-rs/src/tally.rs b/atem-connection-rs/src/tally.rs new file mode 100644 index 0000000..4b25bb0 --- /dev/null +++ b/atem-connection-rs/src/tally.rs @@ -0,0 +1,13 @@ +#[derive(Debug)] +pub struct TallyEvent { + tally_state: TallyState, + source_index: usize, + source_label: String, + source_name: String, +} + +#[derive(Debug)] +enum TallyState { + Program, + Preview, +} diff --git a/atem-test/Cargo.toml b/atem-test/Cargo.toml index 76f7cb0..6496268 100644 --- a/atem-test/Cargo.toml +++ b/atem-test/Cargo.toml @@ -7,6 +7,9 @@ edition = "2021" [dependencies] atem-connection-rs = { path = "../atem-connection-rs" } +clap = { version = "4.4.18", features = ["derive"] } color-eyre = "0.5.11" env_logger = "0.9.0" +log = "0.4.14" tokio = "1.14.0" +tokio-util = "0.7.10" diff --git a/atem-test/src/main.rs b/atem-test/src/main.rs index 3780807..3fe8795 100644 --- a/atem-test/src/main.rs +++ b/atem-test/src/main.rs @@ -1,44 +1,69 @@ -use std::time::Duration; - -use atem_connection_rs::{ - atem_lib::atem_socket::AtemSocket, - commands::{ - command_base::{BasicWritableCommand, SerializableCommand}, - mix_effects::ProgramInput, - }, +use std::{ + net::{Ipv4Addr, SocketAddrV4}, + str::FromStr, + sync::Arc, + time::Duration, }; +use atem_connection_rs::{ + atem::Atem, + atem_lib::atem_socket::{AtemSocket, AtemSocketMessage}, + commands::mix_effects::program_input::ProgramInput, +}; + +use clap::Parser; use color_eyre::Report; -use tokio::time::sleep; +use tokio::{select, time::sleep}; +use tokio_util::sync::CancellationToken; + +/// ATEM Rust Library Test App +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +struct Args { + /// IP of the ATEM to connect to + #[arg(short, long)] + ip: String, +} #[tokio::main] async fn main() { + let args = Args::parse(); + setup_logging().unwrap(); - let switch_to_source_1 = ProgramInput::new(0, 1); - let switch_to_source_2 = ProgramInput::new(0, 2); + let (socket_message_tx, socket_message_rx) = + tokio::sync::mpsc::channel::(10); + let (atem_event_tx, atem_event_rx) = tokio::sync::mpsc::unbounded_channel(); + let cancel = CancellationToken::new(); - let mut atem = AtemSocket::new(); - atem.connect("127.0.0.1".to_string(), 9910).await.ok(); + let mut atem_socket = AtemSocket::new(socket_message_rx, atem_event_tx); - let mut tracking_id = 0; - loop { - sleep(Duration::from_millis(5000)).await; - atem.send_command( - &switch_to_source_1.payload(atem_connection_rs::enums::ProtocolVersion::Unknown), - switch_to_source_1.get_raw_name(), - tracking_id, - ) - .await; - tracking_id += 1; - sleep(Duration::from_millis(5000)).await; - atem.send_command( - &switch_to_source_2.payload(atem_connection_rs::enums::ProtocolVersion::Unknown), - switch_to_source_2.get_raw_name(), - tracking_id, - ) - .await; - tracking_id += 1; + let atem = Arc::new(Atem::new(atem_socket, socket_message_tx)); + let atem_thread = atem.clone(); + let atem_run = atem_thread.run(atem_event_rx, cancel); + + let switch_loop = tokio::spawn(async move { + let address = Ipv4Addr::from_str(&args.ip).unwrap(); + let socket = SocketAddrV4::new(address, 9910); + atem.connect(socket.into()).await; + + loop { + sleep(Duration::from_millis(5000)).await; + log::info!("Switch to source 1"); + atem.send_commands(vec![Box::new(ProgramInput::new(0, 1))]) + .await; + log::info!("Switched to source 1"); + sleep(Duration::from_millis(5000)).await; + log::info!("Switch to source 2"); + atem.send_commands(vec![Box::new(ProgramInput::new(0, 2))]) + .await; + log::info!("Switched to source 2"); + } + }); + + select! { + _ = atem_run => {}, + _ = switch_loop => {} } } diff --git a/flake.lock b/flake.lock index 5fa71ef..73a9e2f 100644 --- a/flake.lock +++ b/flake.lock @@ -2,15 +2,14 @@ "nodes": { "devshell": { "inputs": { - "flake-utils": "flake-utils", "nixpkgs": "nixpkgs" }, "locked": { - "lastModified": 1705332421, - "narHash": "sha256-USpGLPme1IuqG78JNqSaRabilwkCyHmVWY0M9vYyqEA=", + "lastModified": 1741473158, + "narHash": "sha256-kWNaq6wQUbUMlPgw8Y+9/9wP0F8SHkjy24/mN3UAppg=", "owner": "numtide", "repo": "devshell", - "rev": "83cb93d6d063ad290beee669f4badf9914cc16ec", + "rev": "7c9e793ebe66bcba8292989a68c0419b737a22a0", "type": "github" }, "original": { @@ -19,52 +18,16 @@ "type": "github" } }, - "flake-utils": { - "inputs": { - "systems": "systems" - }, - "locked": { - "lastModified": 1701680307, - "narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "4022d587cbbfd70fe950c1e2083a02621806a725", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "flake-utils_2": { - "inputs": { - "systems": "systems_2" - }, - "locked": { - "lastModified": 1681202837, - "narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "cfacdce06f30d2b68473a46042957675eebb3401", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, "naersk": { "inputs": { "nixpkgs": "nixpkgs_2" }, "locked": { - "lastModified": 1698420672, - "narHash": "sha256-/TdeHMPRjjdJub7p7+w55vyABrsJlt5QkznPYy55vKA=", + "lastModified": 1745925850, + "narHash": "sha256-cyAAMal0aPrlb1NgzMxZqeN1mAJ2pJseDhm2m6Um8T0=", "owner": "nix-community", "repo": "naersk", - "rev": "aeb58d5e8faead8980a807c840232697982d47b9", + "rev": "38bc60bbc157ae266d4a0c96671c6c742ee17a5f", "type": "github" }, "original": { @@ -75,11 +38,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1704161960, - "narHash": "sha256-QGua89Pmq+FBAro8NriTuoO/wNaUtugt29/qqA8zeeM=", + "lastModified": 1722073938, + "narHash": "sha256-OpX0StkL8vpXyWOGUD6G+MA26wAXK6SpT94kLJXo6B4=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "63143ac2c9186be6d9da6035fa22620018c85932", + "rev": "e36e9f57337d0ff0cf77aceb58af4c805472bfae", "type": "github" }, "original": { @@ -91,26 +54,26 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1705883077, - "narHash": "sha256-ByzHHX3KxpU1+V0erFy8jpujTufimh6KaS/Iv3AciHk=", + "lastModified": 1749401433, + "narHash": "sha256-HXIQzULIG/MEUW2Q/Ss47oE3QrjxvpUX7gUl4Xp6lnc=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "5f5210aa20e343b7e35f40c033000db0ef80d7b9", + "rev": "08fcb0dcb59df0344652b38ea6326a2d8271baff", "type": "github" }, "original": { - "id": "nixpkgs", - "type": "indirect" + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" } }, "nixpkgs_3": { "locked": { - "lastModified": 1705883077, - "narHash": "sha256-ByzHHX3KxpU1+V0erFy8jpujTufimh6KaS/Iv3AciHk=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "5f5210aa20e343b7e35f40c033000db0ef80d7b9", - "type": "github" + "lastModified": 0, + "narHash": "sha256-DDe16FJk18sadknQKKG/9FbwEro7A57tg9vB5kxZ8kY=", + "path": "/nix/store/2d1ahim48jhzg4bbm97mvjlb4p7fpan3-source", + "type": "path" }, "original": { "id": "nixpkgs", @@ -119,11 +82,11 @@ }, "nixpkgs_4": { "locked": { - "lastModified": 1681358109, - "narHash": "sha256-eKyxW4OohHQx9Urxi7TQlFBTDWII+F+x2hklDOQPB50=", + "lastModified": 1744536153, + "narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "96ba1c52e54e74c3197f4d43026b3f3d92e83ff9", + "rev": "18dd725c29603f582cf1900e0d25f9f1063dbf11", "type": "github" }, "original": { @@ -144,15 +107,14 @@ }, "rust-overlay": { "inputs": { - "flake-utils": "flake-utils_2", "nixpkgs": "nixpkgs_4" }, "locked": { - "lastModified": 1705976279, - "narHash": "sha256-Zx97bJ3+O8IP70uJPD//rRsr8bcxICISMTZUT/L9eFk=", + "lastModified": 1749436897, + "narHash": "sha256-OkDtaCGQQVwVFz5HWfbmrMJR99sFIMXHCHEYXzUJEJY=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "f889dc31ef97835834bdc3662394ebdb3c96b974", + "rev": "e7876c387e35dc834838aff254d8e74cf5bd4f19", "type": "github" }, "original": { @@ -176,46 +138,16 @@ "type": "github" } }, - "systems_2": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - }, - "systems_3": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - }, "utils": { "inputs": { - "systems": "systems_3" + "systems": "systems" }, "locked": { - "lastModified": 1705309234, - "narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=", + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", "owner": "numtide", "repo": "flake-utils", - "rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 4092e4f..e7101ef 100644 --- a/flake.nix +++ b/flake.nix @@ -43,6 +43,8 @@ }; in pkgs.devshell.mkShell { + motd = "Hello you wonderful person, I hope you are having a lovely day 💜"; + packages = with pkgs; [(rust.override {extensions = ["rust-src"];}) rust-analyzer gcc]; }; formatter = pkgs.alejandra;