Initial version of stream command

This commit is contained in:
Marcin Kulik
2024-02-07 14:05:00 +01:00
parent 781f97f40d
commit e742183159
13 changed files with 4036 additions and 25 deletions

526
Cargo.lock generated
View File

@@ -88,10 +88,14 @@ version = "3.0.0-beta.2"
dependencies = [
"anyhow",
"avt",
"axum",
"clap",
"config",
"futures-util",
"mime_guess",
"nix",
"reqwest",
"rust-embed",
"rustyline",
"scraper",
"serde",
@@ -99,6 +103,9 @@ dependencies = [
"signal-hook",
"tempfile",
"termion",
"tokio",
"tokio-stream",
"tower-http",
"uuid",
"which",
]
@@ -143,6 +150,64 @@ dependencies = [
"serde",
]
[[package]]
name = "axum"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1236b4b292f6c4d6dc34604bb5120d85c3fe1d1aa596bd5cc52ca054d13e7b9e"
dependencies = [
"async-trait",
"axum-core",
"base64",
"bytes",
"futures-util",
"http 1.0.0",
"http-body 1.0.0",
"http-body-util",
"hyper 1.1.0",
"hyper-util",
"itoa",
"matchit",
"memchr",
"mime",
"percent-encoding",
"pin-project-lite",
"rustversion",
"serde",
"serde_json",
"serde_path_to_error",
"serde_urlencoded",
"sha1",
"sync_wrapper",
"tokio",
"tokio-tungstenite",
"tower",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "axum-core"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3"
dependencies = [
"async-trait",
"bytes",
"futures-util",
"http 1.0.0",
"http-body 1.0.0",
"http-body-util",
"mime",
"pin-project-lite",
"rustversion",
"sync_wrapper",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "backtrace"
version = "0.3.69"
@@ -176,6 +241,15 @@ version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07"
[[package]]
name = "block-buffer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
"generic-array",
]
[[package]]
name = "bumpalo"
version = "3.14.0"
@@ -307,6 +381,15 @@ version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
[[package]]
name = "cpufeatures"
version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504"
dependencies = [
"libc",
]
[[package]]
name = "crc32fast"
version = "1.3.2"
@@ -316,6 +399,16 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "crypto-common"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "cssparser"
version = "0.29.6"
@@ -343,6 +436,12 @@ dependencies = [
"syn 2.0.38",
]
[[package]]
name = "data-encoding"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5"
[[package]]
name = "derive_more"
version = "0.99.17"
@@ -356,6 +455,16 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
]
[[package]]
name = "dlv-list"
version = "0.3.0"
@@ -489,36 +598,49 @@ dependencies = [
[[package]]
name = "futures-core"
version = "0.3.29"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c"
checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
[[package]]
name = "futures-io"
version = "0.3.29"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa"
checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
[[package]]
name = "futures-macro"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.38",
]
[[package]]
name = "futures-sink"
version = "0.3.29"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817"
checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5"
[[package]]
name = "futures-task"
version = "0.3.29"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2"
checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004"
[[package]]
name = "futures-util"
version = "0.3.29"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104"
checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
dependencies = [
"futures-core",
"futures-io",
"futures-macro",
"futures-sink",
"futures-task",
"memchr",
"pin-project-lite",
@@ -535,6 +657,16 @@ dependencies = [
"byteorder",
]
[[package]]
name = "generic-array"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "getrandom"
version = "0.1.16"
@@ -574,7 +706,26 @@ dependencies = [
"futures-core",
"futures-sink",
"futures-util",
"http",
"http 0.2.11",
"indexmap",
"slab",
"tokio",
"tokio-util",
"tracing",
]
[[package]]
name = "h2"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31d030e59af851932b72ceebadf4a2b5986dba4c3b99dd2493f8273a0f151943"
dependencies = [
"bytes",
"fnv",
"futures-core",
"futures-sink",
"futures-util",
"http 1.0.0",
"indexmap",
"slab",
"tokio",
@@ -643,6 +794,17 @@ dependencies = [
"itoa",
]
[[package]]
name = "http"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b32afd38673a8016f7c9ae69e5af41a58f81b1d31689040f2f1959594ce194ea"
dependencies = [
"bytes",
"fnv",
"itoa",
]
[[package]]
name = "http-body"
version = "0.4.6"
@@ -650,7 +812,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2"
dependencies = [
"bytes",
"http",
"http 0.2.11",
"pin-project-lite",
]
[[package]]
name = "http-body"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643"
dependencies = [
"bytes",
"http 1.0.0",
]
[[package]]
name = "http-body-util"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41cb79eb393015dadd30fc252023adb0b2400a0caee0fa2a077e6e21a551e840"
dependencies = [
"bytes",
"futures-util",
"http 1.0.0",
"http-body 1.0.0",
"pin-project-lite",
]
@@ -676,9 +861,9 @@ dependencies = [
"futures-channel",
"futures-core",
"futures-util",
"h2",
"http",
"http-body",
"h2 0.3.24",
"http 0.2.11",
"http-body 0.4.6",
"httparse",
"httpdate",
"itoa",
@@ -690,6 +875,25 @@ dependencies = [
"want",
]
[[package]]
name = "hyper"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb5aa53871fc917b1a9ed87b683a5d86db645e23acb32c2e0785a353e522fb75"
dependencies = [
"bytes",
"futures-channel",
"futures-util",
"h2 0.4.2",
"http 1.0.0",
"http-body 1.0.0",
"httparse",
"httpdate",
"itoa",
"pin-project-lite",
"tokio",
]
[[package]]
name = "hyper-rustls"
version = "0.24.2"
@@ -697,13 +901,31 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590"
dependencies = [
"futures-util",
"http",
"hyper",
"http 0.2.11",
"hyper 0.14.28",
"rustls",
"tokio",
"tokio-rustls",
]
[[package]]
name = "hyper-util"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bdea9aac0dbe5a9240d68cfd9501e2db94222c6dc06843e06640b9e07f0fdc67"
dependencies = [
"bytes",
"futures-channel",
"futures-util",
"http 1.0.0",
"http-body 1.0.0",
"hyper 1.1.0",
"pin-project-lite",
"socket2",
"tokio",
"tracing",
]
[[package]]
name = "idna"
version = "0.5.0"
@@ -805,6 +1027,12 @@ version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
[[package]]
name = "matchit"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
[[package]]
name = "memchr"
version = "2.6.4"
@@ -844,9 +1072,9 @@ dependencies = [
[[package]]
name = "mio"
version = "0.8.8"
version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2"
checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09"
dependencies = [
"libc",
"wasi 0.11.0+wasi-snapshot-preview1",
@@ -1063,6 +1291,26 @@ dependencies = [
"siphasher",
]
[[package]]
name = "pin-project"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0302c4a0442c456bd56f841aee5c3bfd17967563f6fadc9ceb9f9c23cf3807e0"
dependencies = [
"pin-project-internal",
]
[[package]]
name = "pin-project-internal"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "266c042b60c9c76b8d53061e52b2e0d1116abc57cefc8c5cd671619a56ac3690"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.38",
]
[[package]]
name = "pin-project-lite"
version = "0.2.13"
@@ -1241,10 +1489,10 @@ dependencies = [
"encoding_rs",
"futures-core",
"futures-util",
"h2",
"http",
"http-body",
"hyper",
"h2 0.3.24",
"http 0.2.11",
"http-body 0.4.6",
"hyper 0.14.28",
"hyper-rustls",
"ipnet",
"js-sys",
@@ -1295,6 +1543,40 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "rust-embed"
version = "8.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a82c0bbc10308ed323529fd3c1dce8badda635aa319a5ff0e6466f33b8101e3f"
dependencies = [
"rust-embed-impl",
"rust-embed-utils",
"walkdir",
]
[[package]]
name = "rust-embed-impl"
version = "8.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6227c01b1783cdfee1bcf844eb44594cd16ec71c35305bf1c9fb5aade2735e16"
dependencies = [
"proc-macro2",
"quote",
"rust-embed-utils",
"syn 2.0.38",
"walkdir",
]
[[package]]
name = "rust-embed-utils"
version = "8.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8cb0a25bfbb2d4b4402179c2cf030387d9990857ce08a32592c6238db9fa8665"
dependencies = [
"sha2",
"walkdir",
]
[[package]]
name = "rust-ini"
version = "0.18.0"
@@ -1364,6 +1646,12 @@ dependencies = [
"untrusted",
]
[[package]]
name = "rustversion"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4"
[[package]]
name = "rustyline"
version = "13.0.0"
@@ -1392,6 +1680,15 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]]
name = "scopeguard"
version = "1.2.0"
@@ -1478,6 +1775,16 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_path_to_error"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4beec8bce849d58d06238cb50db2e1c417cfeafa4c63f692b15c82b7c80f8335"
dependencies = [
"itoa",
"serde",
]
[[package]]
name = "serde_urlencoded"
version = "0.7.1"
@@ -1500,6 +1807,28 @@ dependencies = [
"stable_deref_trait",
]
[[package]]
name = "sha1"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "sha2"
version = "0.10.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "signal-hook"
version = "0.3.17"
@@ -1616,6 +1945,12 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "sync_wrapper"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
[[package]]
name = "system-configuration"
version = "0.5.1"
@@ -1673,6 +2008,26 @@ dependencies = [
"redox_termios",
]
[[package]]
name = "thiserror"
version = "1.0.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e3de26b0965292219b4287ff031fcba86837900fe9cd2b34ea8ad893c0953d2"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "268026685b2be38d7103e9e507c938a1fcb3d7e6eb15e87870b617bf37b6d581"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.38",
]
[[package]]
name = "tinyvec"
version = "1.6.0"
@@ -1690,20 +2045,34 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.33.0"
version = "1.35.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f38200e3ef7995e5ef13baec2f432a6da0aa9ac495b2c0e8f3b7eec2c92d653"
checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104"
dependencies = [
"backtrace",
"bytes",
"libc",
"mio",
"num_cpus",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
"socket2",
"tokio-macros",
"windows-sys 0.48.0",
]
[[package]]
name = "tokio-macros"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.38",
]
[[package]]
name = "tokio-rustls"
version = "0.24.1"
@@ -1714,6 +2083,30 @@ dependencies = [
"tokio",
]
[[package]]
name = "tokio-stream"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842"
dependencies = [
"futures-core",
"pin-project-lite",
"tokio",
"tokio-util",
]
[[package]]
name = "tokio-tungstenite"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38"
dependencies = [
"futures-util",
"log",
"tokio",
"tungstenite",
]
[[package]]
name = "tokio-util"
version = "0.7.10"
@@ -1737,6 +2130,44 @@ dependencies = [
"serde",
]
[[package]]
name = "tower"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
dependencies = [
"futures-core",
"futures-util",
"pin-project",
"pin-project-lite",
"tokio",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "tower-http"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0da193277a4e2c33e59e09b5861580c33dd0a637c3883d0fa74ba40c0374af2e"
dependencies = [
"bitflags 2.4.1",
"bytes",
"http 1.0.0",
"http-body 1.0.0",
"http-body-util",
"pin-project-lite",
"tower-layer",
"tower-service",
]
[[package]]
name = "tower-layer"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0"
[[package]]
name = "tower-service"
version = "0.3.2"
@@ -1749,6 +2180,7 @@ version = "0.1.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
dependencies = [
"log",
"pin-project-lite",
"tracing-core",
]
@@ -1768,6 +2200,31 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "tungstenite"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1"
dependencies = [
"byteorder",
"bytes",
"data-encoding",
"http 1.0.0",
"httparse",
"log",
"rand 0.8.5",
"sha1",
"thiserror",
"url",
"utf-8",
]
[[package]]
name = "typenum"
version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
[[package]]
name = "unicase"
version = "2.7.0"
@@ -1854,6 +2311,16 @@ version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "walkdir"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee"
dependencies = [
"same-file",
"winapi-util",
]
[[package]]
name = "want"
version = "0.3.1"
@@ -1986,6 +2453,15 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596"
dependencies = [
"winapi",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"

View File

@@ -26,3 +26,10 @@ which = "5.0.0"
tempfile = "3.9.0"
scraper = { version = "0.15.0", default-features = false }
avt = "0.9.0"
axum = { version = "0.7.4", features = ["ws"] }
tokio = { version = "1.35.1", features = ["full"] }
futures-util = "0.3.30"
tokio-stream = { version = "0.1.14", features = ["sync"] }
rust-embed = "8.2.0"
mime_guess = "2.0.4"
tower-http = "0.5.1"

2835
assets/asciinema-player.css Normal file

File diff suppressed because it is too large Load Diff

1
assets/asciinema-player.min.js vendored Normal file

File diff suppressed because one or more lines are too long

67
assets/index.html Normal file
View File

@@ -0,0 +1,67 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="asciinema-player.css">
<style>
html, body {
height: 100%;
margin: 0;
overflow: hidden;
}
html {
padding: 0;
}
body {
box-sizing: border-box;
padding: 12pt;
background-color: #222;
}
</style>
</head>
<body>
<script src="asciinema-player.min.js"></script>
<script>
const loc = window.location;
const params = new URLSearchParams(loc.hash.replace('#', '?'));
let bufferTime = params.get('bufferTime');
if (bufferTime === null) {
if (loc.hostname === 'localhost' || loc.hostname === '127.0.0.1') {
bufferTime = 0.01;
} else {
bufferTime = 0.1;
}
} else {
bufferTime = parseFloat(bufferTime);
};
const src = {
driver: 'websocket',
url: loc.protocol.replace("http", "ws") + '//' + loc.host + '/ws',
bufferTime
};
const fit = params.get('fit');
const terminalLineHeight = params.get('terminalLineHeight');
const opts = {
logger: console,
fit: fit === null ? 'both' : fit,
theme: params.get('theme'),
autoPlay: params.get('autoPlay') !== 'false',
terminalFontFamily: params.get('terminalFontFamily'),
terminalLineHeight: terminalLineHeight === null ? undefined : parseFloat(terminalLineHeight)
};
console.debug('initializing the player', { src, opts });
window.player = AsciinemaPlayer.create(src, document.body, opts);
</script>
</body>
</html>

View File

@@ -3,6 +3,7 @@ pub mod cat;
pub mod convert;
pub mod play;
pub mod rec;
pub mod stream;
pub mod upload;
use crate::config::Config;
use crate::notifier;

88
src/cmd/stream.rs Normal file
View File

@@ -0,0 +1,88 @@
use crate::config::Config;
use crate::locale;
use crate::logger;
use crate::pty;
use crate::streamer::{self, KeyBindings};
use crate::tty;
use anyhow::Result;
use clap::Args;
#[derive(Debug, Args)]
pub struct Cli {
/// Enable input capture
#[arg(long, short = 'I', alias = "stdin")]
input: bool,
/// Command to stream [default: $SHELL]
#[arg(short, long)]
command: Option<String>,
/// Override terminal size for the session
#[arg(long, value_name = "COLSxROWS")]
tty_size: Option<pty::WinsizeOverride>,
}
impl Cli {
pub fn run(self, config: &Config) -> Result<()> {
locale::check_utf8_locale()?;
let command = self.get_command(config);
let keys = get_key_bindings(config)?;
let notifier = super::get_notifier(config);
let record_input = self.input || config.cmd_stream_input();
let exec_command = super::build_exec_command(command.as_ref().cloned());
let exec_extra_env = super::build_exec_extra_env();
let mut streamer = streamer::Streamer::new(record_input, keys, notifier);
logger::info!(
"Streaming session started, listening on {}",
"127.0.0.1:3000" // TODO
);
if command.is_none() {
logger::info!("Press <ctrl+d> or type 'exit' to end");
}
{
let mut tty: Box<dyn tty::Tty> = if let Ok(dev_tty) = tty::DevTty::open() {
Box::new(dev_tty)
} else {
logger::info!("TTY not available, streaming in headless mode");
Box::new(tty::NullTty::open()?)
};
pty::exec(
&exec_command,
&exec_extra_env,
&mut *tty,
self.tty_size,
&mut streamer,
)?;
}
logger::info!("Streaming session ended");
Ok(())
}
fn get_command(&self, config: &Config) -> Option<String> {
self.command
.as_ref()
.cloned()
.or(config.cmd_stream_command())
}
}
fn get_key_bindings(config: &Config) -> Result<KeyBindings> {
let mut keys = KeyBindings::default();
if let Some(key) = config.cmd_stream_prefix_key()? {
keys.prefix = key;
}
if let Some(key) = config.cmd_stream_pause_key()? {
keys.pause = key;
}
Ok(keys)
}

View File

@@ -31,6 +31,7 @@ pub struct Server {
pub struct Cmd {
rec: Rec,
play: Play,
stream: Stream,
}
#[derive(Debug, Deserialize, Default)]
@@ -55,6 +56,16 @@ pub struct Play {
pub next_marker_key: Option<String>,
}
#[derive(Debug, Deserialize, Default)]
#[allow(unused)]
pub struct Stream {
pub command: Option<String>,
pub input: bool,
pub env: Option<String>,
pub prefix_key: Option<String>,
pub pause_key: Option<String>,
}
#[derive(Debug, Deserialize)]
#[allow(unused)]
pub struct Notifications {
@@ -68,6 +79,7 @@ impl Config {
.set_default("server.url", None::<Option<String>>)?
.set_default("cmd.rec.input", false)?
.set_default("cmd.play.speed", None::<Option<f64>>)?
.set_default("cmd.stream.input", false)?
.set_default("notifications.enabled", true)?
.add_source(config::File::with_name("/etc/asciinema/config.toml").required(false))
.add_source(
@@ -175,6 +187,32 @@ impl Config {
.map(parse_key)
.transpose()
}
pub fn cmd_stream_command(&self) -> Option<String> {
self.cmd.stream.command.as_ref().cloned()
}
pub fn cmd_stream_input(&self) -> bool {
self.cmd.stream.input
}
pub fn cmd_stream_prefix_key(&self) -> Result<Option<Key>> {
self.cmd
.stream
.prefix_key
.as_ref()
.map(parse_key)
.transpose()
}
pub fn cmd_stream_pause_key(&self) -> Result<Option<Key>> {
self.cmd
.stream
.pause_key
.as_ref()
.map(parse_key)
.transpose()
}
}
fn ask_for_server_url() -> Result<String> {

View File

@@ -9,6 +9,7 @@ mod notifier;
mod player;
mod pty;
mod recorder;
mod streamer;
mod tty;
mod util;
use crate::config::Config;
@@ -35,6 +36,9 @@ enum Commands {
/// Replay a terminal session
Play(cmd::play::Cli),
/// Stream a terminal session
Stream(cmd::stream::Cli),
/// Concatenate multiple recordings
Cat(cmd::cat::Cli),
@@ -55,6 +59,7 @@ fn main() -> Result<()> {
match cli.command {
Commands::Rec(record) => record.run(&config),
Commands::Play(play) => play.run(&config),
Commands::Stream(stream) => stream.run(&config),
Commands::Cat(cat) => cat.run(),
Commands::Convert(convert) => convert.run(),
Commands::Upload(upload) => upload.run(&config),

70
src/streamer/alis.rs Normal file
View File

@@ -0,0 +1,70 @@
use super::session;
use anyhow::Result;
use futures_util::{stream, Stream, StreamExt, TryStreamExt};
use std::future;
use tokio::sync::mpsc;
use tokio_stream::wrappers::errors::BroadcastStreamRecvError;
static HEADER: &str = "ALiS\x01\x00\x00\x00\x00\x00";
static SECOND: f64 = 1_000_000.0;
pub async fn stream(
clients_tx: &mpsc::Sender<session::Client>,
) -> Result<impl Stream<Item = Result<Vec<u8>, BroadcastStreamRecvError>>> {
let header = stream::once(future::ready(Ok(HEADER.into())));
let events = session::stream(clients_tx).await?.map_ok(encode_event);
Ok(header.chain(events))
}
fn encode_event(event: session::Event) -> Vec<u8> {
use session::Event::*;
match event {
Init(size, time, init) => {
let (cols, rows): (u16, u16) = (size.0, size.1);
let cols_bytes = cols.to_le_bytes();
let rows_bytes = rows.to_le_bytes();
let time_bytes = ((time as f64 / SECOND) as f32).to_le_bytes();
let init = init.unwrap_or_else(|| "".to_owned());
let init_len = init.len() as u32;
let init_len_bytes = init_len.to_le_bytes();
let mut msg = vec![0x01]; // 1 byte
msg.extend_from_slice(&cols_bytes); // 2 bytes
msg.extend_from_slice(&rows_bytes); // 2 bytes
msg.extend_from_slice(&time_bytes); // 4 bytes
msg.extend_from_slice(&init_len_bytes); // 4 bytes
msg.extend_from_slice(init.as_bytes()); // init_len bytes
msg
}
Stdout(time, text) => {
let time_bytes = ((time as f64 / SECOND) as f32).to_le_bytes();
let text_len = text.len() as u32;
let text_len_bytes = text_len.to_le_bytes();
let mut msg = vec![b'o']; // 1 byte
msg.extend_from_slice(&time_bytes); // 4 bytes
msg.extend_from_slice(&text_len_bytes); // 4 bytes
msg.extend_from_slice(text.as_bytes()); // text_len bytes
msg
}
Resize(time, size) => {
let (cols, rows): (u16, u16) = (size.0, size.1);
let time_bytes = ((time as f64 / SECOND) as f32).to_le_bytes();
let cols_bytes = cols.to_le_bytes();
let rows_bytes = rows.to_le_bytes();
let mut msg = vec![b'r']; // 1 byte
msg.extend_from_slice(&time_bytes); // 4 bytes
msg.extend_from_slice(&cols_bytes); // 2 bytes
msg.extend_from_slice(&rows_bytes); // 2 bytes
msg
}
}
}

214
src/streamer/mod.rs Normal file
View File

@@ -0,0 +1,214 @@
mod alis;
mod server;
mod session;
use crate::config::Key;
use crate::notifier::Notifier;
use crate::pty;
use crate::tty;
use crate::util;
use std::io;
use std::net::TcpListener;
use std::thread;
use std::time::Instant;
use tokio::sync::{mpsc, oneshot};
pub struct Streamer {
record_input: bool,
keys: KeyBindings,
notifier: Option<Box<dyn Notifier>>,
notifier_tx: std::sync::mpsc::Sender<String>,
notifier_rx: Option<std::sync::mpsc::Receiver<String>>,
notifier_handle: Option<util::JoinHandle>,
pty_tx: mpsc::UnboundedSender<Event>,
pty_rx: Option<mpsc::UnboundedReceiver<Event>>,
event_loop_handle: Option<util::JoinHandle>,
start_time: Instant,
paused: bool,
prefix_mode: bool,
}
enum Event {
Output(u64, String),
Input(u64, String),
Resize(u64, tty::TtySize),
}
impl Streamer {
pub fn new(record_input: bool, keys: KeyBindings, notifier: Box<dyn Notifier>) -> Self {
let (notifier_tx, notifier_rx) = std::sync::mpsc::channel();
let (pty_tx, pty_rx) = mpsc::unbounded_channel();
Self {
record_input,
keys,
notifier: Some(notifier),
notifier_tx,
notifier_rx: Some(notifier_rx),
notifier_handle: None,
pty_tx,
pty_rx: Some(pty_rx),
event_loop_handle: None,
start_time: Instant::now(),
paused: false,
prefix_mode: false,
}
}
fn elapsed_time(&self) -> u64 {
self.start_time.elapsed().as_micros() as u64
}
fn notify<S: ToString>(&self, message: S) {
self.notifier_tx
.send(message.to_string())
.expect("notification send should succeed");
}
}
impl pty::Recorder for Streamer {
fn start(&mut self, tty_size: tty::TtySize) -> io::Result<()> {
let pty_rx = self.pty_rx.take().unwrap();
let (clients_tx, mut clients_rx) = mpsc::channel(1);
let (server_shutdown_tx, server_shutdown_rx) = oneshot::channel::<()>();
let listener = TcpListener::bind("0.0.0.0:3000")?;
let runtime = build_tokio_runtime();
let server = runtime.spawn(server::serve(listener, clients_tx, server_shutdown_rx));
self.event_loop_handle = wrap_thread_handle(thread::spawn(move || {
runtime.block_on(async move {
event_loop(pty_rx, &mut clients_rx, tty_size).await;
let _ = server_shutdown_tx.send(());
let _ = server.await;
let _ = clients_rx.recv().await;
});
}));
let mut notifier = self.notifier.take().unwrap();
let notifier_rx = self.notifier_rx.take().unwrap();
self.notifier_handle = wrap_thread_handle(thread::spawn(move || {
for message in notifier_rx {
let _ = notifier.notify(message);
}
}));
self.start_time = Instant::now();
Ok(())
}
fn output(&mut self, data: &[u8]) {
if self.paused {
return;
}
let data = String::from_utf8_lossy(data).to_string();
let event = Event::Output(self.elapsed_time(), data);
self.pty_tx.send(event).expect("output send should succeed");
}
fn input(&mut self, data: &[u8]) -> bool {
let prefix_key = self.keys.prefix.as_ref();
let pause_key = self.keys.pause.as_ref();
if !self.prefix_mode && prefix_key.is_some_and(|key| data == key) {
self.prefix_mode = true;
return false;
}
if self.prefix_mode || prefix_key.is_none() {
self.prefix_mode = false;
if pause_key.is_some_and(|key| data == key) {
if self.paused {
self.paused = false;
self.notify("Resumed streaming");
} else {
self.paused = true;
self.notify("Paused streaming");
}
return false;
}
}
if self.record_input && !self.paused {
// TODO ignore OSC responses
let data = String::from_utf8_lossy(data).to_string();
let event = Event::Input(self.elapsed_time(), data);
self.pty_tx.send(event).expect("input send should succeed");
}
true
}
fn resize(&mut self, size: crate::tty::TtySize) {
let event = Event::Resize(self.elapsed_time(), size);
self.pty_tx.send(event).expect("resize send should succeed");
}
}
async fn event_loop(
mut events: mpsc::UnboundedReceiver<Event>,
clients: &mut mpsc::Receiver<session::Client>,
tty_size: tty::TtySize,
) {
let mut session = session::Session::new(tty_size);
loop {
tokio::select! {
event = events.recv() => {
match event {
Some(Event::Output(time, data)) => {
session.output(time, data);
}
Some(Event::Input(time, data)) => {
session.input(time, data);
}
Some(Event::Resize(time, new_tty_size)) => {
session.resize(time, new_tty_size);
}
None => break,
}
}
client = clients.recv() => {
match client {
Some(client) => {
client.accept(session.subscribe());
}
None => break,
}
}
}
}
}
fn build_tokio_runtime() -> tokio::runtime::Runtime {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap()
}
fn wrap_thread_handle(handle: thread::JoinHandle<()>) -> Option<util::JoinHandle> {
Some(util::JoinHandle::new(handle))
}
pub struct KeyBindings {
pub prefix: Key,
pub pause: Key,
}
impl Default for KeyBindings {
fn default() -> Self {
Self {
prefix: None,
pause: Some(vec![0x1c]), // ^\
}
}
}

105
src/streamer/server.rs Normal file
View File

@@ -0,0 +1,105 @@
use super::alis;
use super::session;
use axum::{
extract::connect_info::ConnectInfo,
extract::ws,
extract::State,
http::{header, StatusCode, Uri},
response::IntoResponse,
routing::get,
Router,
};
use futures_util::{stream, StreamExt};
use rust_embed::RustEmbed;
use std::borrow::Cow;
use std::future;
use std::io;
use std::net::SocketAddr;
use tokio::sync::{mpsc, oneshot};
use tokio_stream::wrappers::errors::BroadcastStreamRecvError;
#[derive(RustEmbed)]
#[folder = "assets/"]
struct Assets;
pub async fn serve(
listener: std::net::TcpListener,
clients_tx: mpsc::Sender<session::Client>,
shutdown_rx: oneshot::Receiver<()>,
) -> io::Result<()> {
listener.set_nonblocking(true)?;
let listener = tokio::net::TcpListener::from_std(listener)?;
let app = Router::new()
.route("/ws", get(ws_handler))
.with_state(clients_tx)
.fallback(static_handler);
let signal = async {
let _ = shutdown_rx.await;
};
axum::serve(
listener,
app.into_make_service_with_connect_info::<SocketAddr>(),
)
.with_graceful_shutdown(signal)
.await
}
async fn static_handler(uri: Uri) -> impl IntoResponse {
let mut path = uri.path().trim_start_matches('/');
if path.is_empty() {
path = "index.html";
}
match Assets::get(path) {
Some(content) => {
let mime = mime_guess::from_path(path).first_or_octet_stream();
([(header::CONTENT_TYPE, mime.as_ref())], content.data).into_response()
}
None => (StatusCode::NOT_FOUND, "404").into_response(),
}
}
async fn ws_handler(
ws: ws::WebSocketUpgrade,
ConnectInfo(_addr): ConnectInfo<SocketAddr>,
State(clients_tx): State<mpsc::Sender<session::Client>>,
) -> impl IntoResponse {
ws.on_upgrade(move |socket| async move {
let _ = handle_socket(socket, clients_tx).await;
})
}
async fn handle_socket(
socket: ws::WebSocket,
clients_tx: mpsc::Sender<session::Client>,
) -> anyhow::Result<()> {
alis::stream(&clients_tx)
.await?
.map(ws_result)
.chain(stream::once(future::ready(Ok(close_message()))))
.forward(socket)
.await?;
Ok(())
}
fn close_message() -> ws::Message {
ws::Message::Close(Some(ws::CloseFrame {
code: ws::close_code::NORMAL,
reason: Cow::from("ended"),
}))
}
fn ws_result(m: Result<Vec<u8>, BroadcastStreamRecvError>) -> Result<ws::Message, axum::Error> {
match m {
Ok(bytes) => Ok(ws::Message::Binary(bytes)),
Err(e) => Err(axum::Error::new(e)),
}
}

104
src/streamer/session.rs Normal file
View File

@@ -0,0 +1,104 @@
use crate::tty;
use anyhow::Result;
use futures_util::{stream, Stream, StreamExt};
use std::{future, time::Instant};
use tokio::sync::{broadcast, mpsc, oneshot};
use tokio_stream::wrappers::{errors::BroadcastStreamRecvError, BroadcastStream};
pub struct Session {
vt: avt::Vt,
broadcast_tx: broadcast::Sender<Event>,
stream_time: u64,
last_event_time: Instant,
tty_size: tty::TtySize,
}
#[derive(Clone)]
pub enum Event {
Init(tty::TtySize, u64, Option<String>),
Stdout(u64, String),
Resize(u64, tty::TtySize),
}
pub struct Client(oneshot::Sender<Subscription>);
pub struct Subscription {
init: Event,
broadcast_rx: broadcast::Receiver<Event>,
}
impl Session {
pub fn new(tty_size: tty::TtySize) -> Self {
let (broadcast_tx, _) = broadcast::channel(1024);
Self {
vt: build_vt(tty_size),
broadcast_tx,
stream_time: 0,
last_event_time: Instant::now(),
tty_size,
}
}
pub fn output(&mut self, time: u64, data: String) {
self.vt.feed_str(&data);
let _ = self.broadcast_tx.send(Event::Stdout(time, data));
self.stream_time = time;
self.last_event_time = Instant::now();
}
pub fn input(&mut self, time: u64, _data: String) {
self.stream_time = time;
self.last_event_time = Instant::now();
}
pub fn resize(&mut self, time: u64, tty_size: tty::TtySize) {
if tty_size != self.tty_size {
resize_vt(&mut self.vt, &tty_size);
let _ = self.broadcast_tx.send(Event::Resize(time, tty_size));
self.stream_time = time;
self.last_event_time = Instant::now();
self.tty_size = tty_size;
}
}
pub fn subscribe(&self) -> Subscription {
let init = Event::Init(self.tty_size, self.elapsed_time(), Some(self.vt.dump()));
let broadcast_rx = self.broadcast_tx.subscribe();
Subscription { init, broadcast_rx }
}
fn elapsed_time(&self) -> u64 {
self.stream_time + self.last_event_time.elapsed().as_micros() as u64
}
}
fn build_vt(tty_size: tty::TtySize) -> avt::Vt {
avt::Vt::builder()
.size(tty_size.0 as usize, tty_size.1 as usize)
.resizable(true)
.build()
}
fn resize_vt(vt: &mut avt::Vt, tty_size: &tty::TtySize) {
vt.feed_str(&format!("\x1b[8;{};{}t", tty_size.1, tty_size.0));
}
impl Client {
pub fn accept(self, subscription: Subscription) {
let _ = self.0.send(subscription);
}
}
pub async fn stream(
clients_tx: &mpsc::Sender<Client>,
) -> Result<impl Stream<Item = Result<Event, BroadcastStreamRecvError>>> {
let (sub_tx, sub_rx) = oneshot::channel();
clients_tx.send(Client(sub_tx)).await?;
let sub = sub_rx.await?;
let init = stream::once(future::ready(Ok(sub.init)));
let events = BroadcastStream::new(sub.broadcast_rx);
Ok(init.chain(events))
}