forked from IQ.Lvbs/IQ.Pilot
IQ.Pilot Release Commit @ 3fe374f
This commit is contained in:
1
artifacts/iqpilot_hephaestusd_private/.source_sha
Normal file
1
artifacts/iqpilot_hephaestusd_private/.source_sha
Normal file
@@ -0,0 +1 @@
|
||||
292ca77fab7ffaa87ab6a011206a1f5fdd3aeeb5
|
||||
@@ -161,8 +161,8 @@
|
||||
},
|
||||
"python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/helpers.py": {
|
||||
"mode": 420,
|
||||
"sha256": "ce5d23a33b0e2d9eb81525ad63a88823d20a0578191ceab48c20e24594f8f690",
|
||||
"size": 10952
|
||||
"sha256": "e4f524c7595754fd9db86724a7bd9b67524c630275ac78599ab6838d1aa7d809",
|
||||
"size": 11037
|
||||
},
|
||||
"python/_iqclosure/iqpilot/selfdrive/locationd/__init__.py": {
|
||||
"mode": 420,
|
||||
@@ -351,27 +351,27 @@
|
||||
},
|
||||
"python/iqpilot_private/konn3kt/backups/archive_codec.cpython-312-aarch64-linux-gnu.so": {
|
||||
"mode": 493,
|
||||
"sha256": "2ec8be1fb7b3b4e657a79c8085996729a7e7b1d9cf8c37f5008a62f4aa952aab",
|
||||
"sha256": "82183cba3d305058bda02b49949fe7b12d52cd906cf10107a2e091632173b82e",
|
||||
"size": 135552
|
||||
},
|
||||
"python/iqpilot_private/konn3kt/backups/backup_keys.cpython-312-aarch64-linux-gnu.so": {
|
||||
"mode": 493,
|
||||
"sha256": "87e315903c8dbe8803111e2fe21c2bd367ff69cfb05ab88458e12130022a6c2c",
|
||||
"sha256": "62b3742751af4c8fef9e3b0a439ba68dfc7acf6680340275894c4156c05de223",
|
||||
"size": 67664
|
||||
},
|
||||
"python/iqpilot_private/konn3kt/backups/backup_orchestrator.cpython-312-aarch64-linux-gnu.so": {
|
||||
"mode": 493,
|
||||
"sha256": "187b5e13f2b21def80888ee353be65dee1aced7b2b1c7a4f8c962ce8895f8f89",
|
||||
"sha256": "59a6ffcd63088e8b44c52d013f52d89fee01db9a3774b29a86de806c83960454",
|
||||
"size": 204640
|
||||
},
|
||||
"python/iqpilot_private/konn3kt/backups/cbc_vault.cpython-312-aarch64-linux-gnu.so": {
|
||||
"mode": 493,
|
||||
"sha256": "9338a83201734ff572d431de5f56c8279fc4a656d10d0a67ea44e8057bb88016",
|
||||
"sha256": "f8d9435ef9281a3fa268b504dc326a98962dd02bd52df0570b2a0904e77a961f",
|
||||
"size": 69768
|
||||
},
|
||||
"python/iqpilot_private/konn3kt/backups/imahelper.cpython-312-aarch64-linux-gnu.so": {
|
||||
"mode": 493,
|
||||
"sha256": "df671d37153130a8dcf67fd8a7ae7b024206d19aaa6db85b48811a259289481d",
|
||||
"sha256": "bcbcf79fb098d3647d856958be01bbf4b2cc28b57db0e4dfe4cd41d90b0ea040",
|
||||
"size": 203088
|
||||
},
|
||||
"python/iqpilot_private/konn3kt/flockd/__init__.py": {
|
||||
@@ -381,12 +381,12 @@
|
||||
},
|
||||
"python/iqpilot_private/konn3kt/flockd/flockd.cpython-312-aarch64-linux-gnu.so": {
|
||||
"mode": 493,
|
||||
"sha256": "a67c26048adfb32e76168bdb6a260e11dedbf05257a66eba5eef3b4a623ce1e6",
|
||||
"sha256": "83ac740a10c113e1342cb9eee311fd2c0cb88e550441468155318207bdf400b7",
|
||||
"size": 202184
|
||||
},
|
||||
"python/iqpilot_private/konn3kt/flockd/signatures.cpython-312-aarch64-linux-gnu.so": {
|
||||
"mode": 493,
|
||||
"sha256": "e09bb698e3128449a4da3c2d767621928e531140181b20acd2adcf4186617953",
|
||||
"sha256": "fbd898edf0446b6dcfb6787400dd6fd5bb344c429be491ac40318f8a1946d195",
|
||||
"size": 68080
|
||||
},
|
||||
"python/iqpilot_private/konn3kt/hephaestus/__init__.py": {
|
||||
@@ -396,67 +396,67 @@
|
||||
},
|
||||
"python/iqpilot_private/konn3kt/hephaestus/_vendor/localapi_runtime.zip": {
|
||||
"mode": 420,
|
||||
"sha256": "cb21ff749633ca4b01aef63c178df260d51fff631aed83923b223d70bf6533bd",
|
||||
"sha256": "9acc808279e1d740fd384ad2f31de7398c52c27baa8fa7a6e2f65b38cafc49e4",
|
||||
"size": 2100238
|
||||
},
|
||||
"python/iqpilot_private/konn3kt/hephaestus/ble_auth.cpython-312-aarch64-linux-gnu.so": {
|
||||
"mode": 493,
|
||||
"sha256": "85bf27b0c19387fdb52b493f259e84a23d31c73b5e275e964dbeb934d329c821",
|
||||
"sha256": "eef8e9fdaea989bd323b8a3dde2ce1cc1090a8c60d31a81cf3eec53dd3600071",
|
||||
"size": 268080
|
||||
},
|
||||
"python/iqpilot_private/konn3kt/hephaestus/ble_gatt.cpython-312-aarch64-linux-gnu.so": {
|
||||
"mode": 493,
|
||||
"sha256": "1525c92a7fe8dd898a07188fcdc05dc608a5b77b35ab2fc9d250fcc46bf69999",
|
||||
"sha256": "343423a3dbec1198f40745a6f303569c7b566d1a5eff8ff10ae5e3dacd0044b9",
|
||||
"size": 406176
|
||||
},
|
||||
"python/iqpilot_private/konn3kt/hephaestus/ble_rpc_dispatch.cpython-312-aarch64-linux-gnu.so": {
|
||||
"mode": 493,
|
||||
"sha256": "f5163ae4cbaa77c09ccbea174f659bf719fa9f9c403db478af33ab09a8a9673b",
|
||||
"sha256": "bf40085a06442714fb8386a77ac8e2d047eeadac084c09e80f3a5846137b2a16",
|
||||
"size": 68048
|
||||
},
|
||||
"python/iqpilot_private/konn3kt/hephaestus/ble_transportd.cpython-312-aarch64-linux-gnu.so": {
|
||||
"mode": 493,
|
||||
"sha256": "c69f659c3736c14a57065e118c35cf06c262520a35c37afea3e7bbc43844c129",
|
||||
"sha256": "a14d91fb1ec6150a2eb11e206301ca1275e2a28c5e759823ed2ede57da05e34b",
|
||||
"size": 335776
|
||||
},
|
||||
"python/iqpilot_private/konn3kt/hephaestus/bt_gamepad.cpython-312-aarch64-linux-gnu.so": {
|
||||
"mode": 493,
|
||||
"sha256": "543534177922974da9872b56ba91b5f63b26ab9cb3b275f8e7bd5517e2971ca6",
|
||||
"sha256": "2ba6c8cb47106c3073d72d3291957c7aeefb5d704b851732b532c4f20e9f6b71",
|
||||
"size": 271696
|
||||
},
|
||||
"python/iqpilot_private/konn3kt/hephaestus/cloud_routes.cpython-312-aarch64-linux-gnu.so": {
|
||||
"mode": 493,
|
||||
"sha256": "53a637f886ac6c465c2e3a5ec74b7f2d4e624bbfdb0186c11f29f8d50a6af8d2",
|
||||
"sha256": "a73ebe2e3dba51692fb8b227e3287209c2763a3f2d9fc8d5c7d62f7cdbf760dc",
|
||||
"size": 134408
|
||||
},
|
||||
"python/iqpilot_private/konn3kt/hephaestus/hephaestusd.cpython-312-aarch64-linux-gnu.so": {
|
||||
"mode": 493,
|
||||
"sha256": "52b6306bc0b6a69198ea139eb311e3c70a7f7d0b2093d17cae75e280dfdf52d1",
|
||||
"sha256": "5825a4f141a829f7b3a8bbaf848fd8c25f38d99c6bf8c97cf19ca75c1c025241",
|
||||
"size": 3715880
|
||||
},
|
||||
"python/iqpilot_private/konn3kt/hephaestus/kwp2000.cpython-312-aarch64-linux-gnu.so": {
|
||||
"mode": 493,
|
||||
"sha256": "1dab38fc494466cac932289b9805266d211b1b510fe838907f3cb3afe53ed93c",
|
||||
"sha256": "dc2147325ea4e1dafcaa3357c0878c3f39f3c4630417314f8bcb9af95abc7e91",
|
||||
"size": 136472
|
||||
},
|
||||
"python/iqpilot_private/konn3kt/hephaestus/manage_hephaestusd.cpython-312-aarch64-linux-gnu.so": {
|
||||
"mode": 493,
|
||||
"sha256": "1adfdd3fa1ebd5e0f83e147dadf6b0a91e044bce00ea2a8c90c539c1ad0970dc",
|
||||
"sha256": "c3755a8dcfda46fce09fb8eb69d6b9dccdc5487fe6aa94f4df49279ebeb41acc",
|
||||
"size": 68208
|
||||
},
|
||||
"python/iqpilot_private/konn3kt/hephaestus/motd.cpython-312-aarch64-linux-gnu.so": {
|
||||
"mode": 493,
|
||||
"sha256": "af1119b628946776d468bad4c6f05126a9f1b76010e76d84615f004fb10ea0cd",
|
||||
"sha256": "270b887b04eba51006aff5c71c6b57a29099813752a49a6a1942380edb25790a",
|
||||
"size": 67840
|
||||
},
|
||||
"python/iqpilot_private/konn3kt/hephaestus/tp20.cpython-312-aarch64-linux-gnu.so": {
|
||||
"mode": 493,
|
||||
"sha256": "e5e0fa2832d0ace95843e5d732ecd9e1de370c00d5dc41a55f33103236194125",
|
||||
"sha256": "fdc61d802b53f6233eab0a73f7ef732d4a67134a3849343e259871bc08fec0ef",
|
||||
"size": 135664
|
||||
},
|
||||
"python/iqpilot_private/konn3kt/hephaestus/vw_pq_flasher.cpython-312-aarch64-linux-gnu.so": {
|
||||
"mode": 493,
|
||||
"sha256": "f20bd4b330e641b481db8f545b72d472007402c709666b687d4e9897c42d0dc0",
|
||||
"sha256": "98770387cdda540ce42c8d0b0508fd76b1c03a457d675823aa827b5725d5f888",
|
||||
"size": 269600
|
||||
},
|
||||
"python/iqpilot_private/konn3kt/uploaderd/__init__.py": {
|
||||
@@ -466,7 +466,7 @@
|
||||
},
|
||||
"python/iqpilot_private/konn3kt/uploaderd/iquploaderd.cpython-312-aarch64-linux-gnu.so": {
|
||||
"mode": 493,
|
||||
"sha256": "eee4e9e58787962b9cb5e63f9b946fa07ac1121c3c29373f947d3d461db3f1e3",
|
||||
"sha256": "84394628653c76537d7e2be1938415eeea3decc0791d57b4296f6177e30588db",
|
||||
"size": 204296
|
||||
},
|
||||
"runtime": {
|
||||
@@ -510,25 +510,25 @@
|
||||
}
|
||||
},
|
||||
"signatures": {
|
||||
"python/iqpilot_private/konn3kt/backups/archive_codec.cpython-312-aarch64-linux-gnu.so": "l+KcwF57otKYCL17nXSTJ8s0so+z9q4b/SsOfagBc9G+TbGDO9qHYcbsMtWSKoIj7Np6NGwAhBBKGubGNA6NDw==",
|
||||
"python/iqpilot_private/konn3kt/backups/backup_keys.cpython-312-aarch64-linux-gnu.so": "7gttAqiZR2SOutahE8XnQEH+es5E7nHt8TprLg3QAv/q/aYVTvJMHM/uMYUr7CVqtNSk9NXzi4kuahry96A2DQ==",
|
||||
"python/iqpilot_private/konn3kt/backups/backup_orchestrator.cpython-312-aarch64-linux-gnu.so": "otqtacOKGvys+mxrA0GSs/C2gvL/RoyNVXXeTtwhYYBYsBwWEacuM6xEAANZtnkowt2EIrY22RdIDS9091KBBQ==",
|
||||
"python/iqpilot_private/konn3kt/backups/cbc_vault.cpython-312-aarch64-linux-gnu.so": "4qPIHhjLGAGjKTXtvAWbdrOqxLL5vJtZnF3xVUBQiB/3mQo20i0hPT8+6164HQXMyf596FsetmzUclPPlAeoBQ==",
|
||||
"python/iqpilot_private/konn3kt/backups/imahelper.cpython-312-aarch64-linux-gnu.so": "jPid/JOUWsCYqtCVG4JD5c86nLtadg9L3YICEhsodZiaVPx2Yga0wxPvS7wkaNKCBohIGD20QxJqBUgrKnXQCA==",
|
||||
"python/iqpilot_private/konn3kt/flockd/flockd.cpython-312-aarch64-linux-gnu.so": "Wl2IcgkvLI/bHQfozQP4vcMhbhMhph3rb/Zu4Mg7E6XKNfxw/N8/frq+8x8QV9N2kRro9/6eANyAamOsQUFuBw==",
|
||||
"python/iqpilot_private/konn3kt/flockd/signatures.cpython-312-aarch64-linux-gnu.so": "nzibDoeERknQpLGPUCUQ0qa7FxkoJ2UNXHA8pgcIFWCLMUXDPODjGbKdx2mpSQDU7Q5E5xNx6gjSU0JgZvpZAA==",
|
||||
"python/iqpilot_private/konn3kt/hephaestus/ble_auth.cpython-312-aarch64-linux-gnu.so": "uuROebZDc2abXyiuhL8kvQszS5/aADOnEG1wP0cssD2I6CRPqJb47y6mwXKlUJrgasod2XDhD22mCbn9sDuVDA==",
|
||||
"python/iqpilot_private/konn3kt/hephaestus/ble_gatt.cpython-312-aarch64-linux-gnu.so": "5wGiTrEA8uaDEkk5W0eHztI+gaixGHIT4sbgUKeiW8nUlJYAsC7pDXodOZMmcmcjtQTtgTFrzhDnA9O1aaC+Ag==",
|
||||
"python/iqpilot_private/konn3kt/hephaestus/ble_rpc_dispatch.cpython-312-aarch64-linux-gnu.so": "P6k37fMBW5ryhdHxReumrOSIuRnIb7rqaq+3OOVpgwol/lDwST0WL/wZfSNsnGB8tQL+YZOLOTw6daJe6BanBQ==",
|
||||
"python/iqpilot_private/konn3kt/hephaestus/ble_transportd.cpython-312-aarch64-linux-gnu.so": "lP6OEHgbOtQu8AskEdKgy+773g7F/NWGrkeD4LeC7zcdHmdmfg2dYcN+Wdrs8FrlvAjBilFqyv3wZVfoKv8dBg==",
|
||||
"python/iqpilot_private/konn3kt/hephaestus/bt_gamepad.cpython-312-aarch64-linux-gnu.so": "q1XPOv/SdBC4xfSsiDSf8yR2mdfgWbseTBrUbl9xilFYUd565cv8iauUqeh3/EsVV8qZvcIZViTWuCpy00VZAQ==",
|
||||
"python/iqpilot_private/konn3kt/hephaestus/cloud_routes.cpython-312-aarch64-linux-gnu.so": "zY8c7e8Qq5vSDlbymf3o6hsPz+slQ8oxqiLk8qpKDNFfP6hJxK2eUf/9WHQGGTvGg99d/QoQVyozCu1XRGoFCA==",
|
||||
"python/iqpilot_private/konn3kt/hephaestus/hephaestusd.cpython-312-aarch64-linux-gnu.so": "xu/P0Ekw0d1yjINg542+10BROwGUU5G6qktfwnhFymcF0jhWGaThSndJmPeFmWq0k9bs4asAXimgq5/BUAIABw==",
|
||||
"python/iqpilot_private/konn3kt/hephaestus/kwp2000.cpython-312-aarch64-linux-gnu.so": "zFPl0ip4i/baX0GJzDnPGrm9vAgeXXMIotBr0VArZOxtOM1pOyGwOaJpgWBtb4kVUsNLepjfPgva6UVhrvb6Dg==",
|
||||
"python/iqpilot_private/konn3kt/hephaestus/manage_hephaestusd.cpython-312-aarch64-linux-gnu.so": "Ai6K1+DGajQIi6tEBl0wfPoHWHA499mXbLrEZQZZb0X7dlASbwu+MVW1jovY3kgskMI9JcVANNEWPfSTvPnyAg==",
|
||||
"python/iqpilot_private/konn3kt/hephaestus/motd.cpython-312-aarch64-linux-gnu.so": "GMyhhOO0gGxbbLiB0yjUKVOwV6JeeTfhKNnjytjAboFDUj4ueh/Kuf9L6CRuFZf9xU6uvB6QXSBuUr+FHEYQDQ==",
|
||||
"python/iqpilot_private/konn3kt/hephaestus/tp20.cpython-312-aarch64-linux-gnu.so": "+fLA6hdx0m3WISNW+mvi8s8Lk/N+ZOvfyEF69ZmQ8lFWVdN08g52wbaH6lR2hh2bl8y8rekqFGSHKj38NnYcAg==",
|
||||
"python/iqpilot_private/konn3kt/hephaestus/vw_pq_flasher.cpython-312-aarch64-linux-gnu.so": "DGU9axrJW6Mtcpt43w07LgP9u+fk67pOZ4pfmiTGddnsZkw1IEJJZneguVTO5mUN7OZq9iWH2KsqV+N1aaLbDw==",
|
||||
"python/iqpilot_private/konn3kt/uploaderd/iquploaderd.cpython-312-aarch64-linux-gnu.so": "SSw7fJoNsbQ1GuomiW9rETXgMVqHKgqv7wuPIbzL+Y+pNJyYgvmikeyTFaloTIhllvr9n0XzSp/KT+qr41weCw=="
|
||||
"python/iqpilot_private/konn3kt/backups/archive_codec.cpython-312-aarch64-linux-gnu.so": "NAbCqbvyXIPYE7Il+5iOFO4gAWCSn+45RUlrGjxJydXtONcibCK7MMZmwC/bxvf94FdwaayTc5Bk2viE4sR3Aw==",
|
||||
"python/iqpilot_private/konn3kt/backups/backup_keys.cpython-312-aarch64-linux-gnu.so": "GmZl9B5gnEtrfR2puNGucdznMZ1QAdTwI1T+HkJeXooHRdnhSh9/GarqxzjhOBT/br471jx9h7Qm8ZcYkc8VBw==",
|
||||
"python/iqpilot_private/konn3kt/backups/backup_orchestrator.cpython-312-aarch64-linux-gnu.so": "ShJ0b8MtUi1uupmlF26qPutyMxazGJFRmoI7phHNyYZaocv3iEtqtUPwO9icjO9Oh6q2dNZhYNUel+MpRW9oBw==",
|
||||
"python/iqpilot_private/konn3kt/backups/cbc_vault.cpython-312-aarch64-linux-gnu.so": "J01e8wupmVBf6jEqodutokSkmkSAbMCeoiTPkhM1ME125NtHrSbJOuoka9c3wdUXVeVz3itnt2KkiayDwCvmBA==",
|
||||
"python/iqpilot_private/konn3kt/backups/imahelper.cpython-312-aarch64-linux-gnu.so": "DKRmG1/vNDmW/fM5LDEfiRFRKCUXWl/zs/w9pyLuP3P0uX+NOvVP4V3niGVjdfpcVQsoeSds+Y/2ml2BVhepDg==",
|
||||
"python/iqpilot_private/konn3kt/flockd/flockd.cpython-312-aarch64-linux-gnu.so": "z+x9JhNkR5wwo+oN64kBuDfeGmWQtrm73iqmK8e4KYDi6bW5EbR17YGJd2w2Qpy3Y+O4hqWPTzimC76NvvgoBg==",
|
||||
"python/iqpilot_private/konn3kt/flockd/signatures.cpython-312-aarch64-linux-gnu.so": "6+o1p0LzuO2fyXgzmKQ8yqIxTPoww9wkE5BBQNiTthEw/KSa8mFn1QxmN/5Rc+O3fmgNydxEdjMIwcsmQpL/DA==",
|
||||
"python/iqpilot_private/konn3kt/hephaestus/ble_auth.cpython-312-aarch64-linux-gnu.so": "YxpFvXNm0HIXYVVaW1zDXwWOsh5nz7eZS6DdsMlnMdl8OHaSgabWZBN7RF4iWJP/GU9S0x6u9hOBXLosf12wDQ==",
|
||||
"python/iqpilot_private/konn3kt/hephaestus/ble_gatt.cpython-312-aarch64-linux-gnu.so": "FX+aDGY2Xs5Y6ekJBuRhjlw/CxxwSfC/ZwOyY+k/v7PULJ9T5fEJjuHE8hAmkdPK4ED7GT5VO5FB4twOvOiiCw==",
|
||||
"python/iqpilot_private/konn3kt/hephaestus/ble_rpc_dispatch.cpython-312-aarch64-linux-gnu.so": "uPcSh8CGEYHeu9ApsPIn0rNCFz+EBTyhuqQyGLGjU/RSnoWhkhHmqpbeCfgZTBmkwLol7Onfex0w/ELEjUyrDQ==",
|
||||
"python/iqpilot_private/konn3kt/hephaestus/ble_transportd.cpython-312-aarch64-linux-gnu.so": "BJgU130UhimX2PSMYbNZJ0/nUFHI0MolmTrk0d5wTJ7uvmIT0wGsaO78PGp66krMtXaDFdhg/UJUz/KRyVPCBg==",
|
||||
"python/iqpilot_private/konn3kt/hephaestus/bt_gamepad.cpython-312-aarch64-linux-gnu.so": "UzuHr0ym4jsHBzBW4e5XPcP+46zeds6Ec+7FK9hqMjJx+CVRfg9/1CfLCwqCUhe7QC4puqt0R8AQ6tCWppflCg==",
|
||||
"python/iqpilot_private/konn3kt/hephaestus/cloud_routes.cpython-312-aarch64-linux-gnu.so": "aPUVcHH/ashhYDylUw7AblIIYPOzavy+NHAwxhCkavILBvEwyvpoZAQuq1ajAUrsqUUoVDBRGUT8qQpcXQslBw==",
|
||||
"python/iqpilot_private/konn3kt/hephaestus/hephaestusd.cpython-312-aarch64-linux-gnu.so": "9LDSMTtL21Ec4iVjYxBL8UjclFf16M/vTSehYveVHOnVOaZPiki9mhf1UJScCxbTK24EqLMD14AdKzZ44qokBA==",
|
||||
"python/iqpilot_private/konn3kt/hephaestus/kwp2000.cpython-312-aarch64-linux-gnu.so": "hjQ9KQsNSTO3Sj2DpZ9RQHMIgzvGQMhCcFsMs7fjV2v4gzd4AblfY09I2/Jg/WmghjB9q2onqaA+ARKixvAvCA==",
|
||||
"python/iqpilot_private/konn3kt/hephaestus/manage_hephaestusd.cpython-312-aarch64-linux-gnu.so": "agjSn1SbGK6VW8X/dDRFpqz5sTpjvBnlnMKnLXf/o0ApUeECcgujLuOAZu+TPyfmcEVcPyE8TYYamCM0VGY0Dw==",
|
||||
"python/iqpilot_private/konn3kt/hephaestus/motd.cpython-312-aarch64-linux-gnu.so": "MUiDt9IKQ1YTyGTAVYO2bcG8rfvkbECFQOAq5q6m1y+V37y9O0dN6OK8YyHPDRbshBCDupcaUx3xdu2Rh5toAA==",
|
||||
"python/iqpilot_private/konn3kt/hephaestus/tp20.cpython-312-aarch64-linux-gnu.so": "1U+W1BeavzaoaMEQxCFBPB4RleaE704xgf7a9tsqMfvScfU80nvge/VuvgIF64OPxYZIoYv9s0oMrzTNy6ZfDw==",
|
||||
"python/iqpilot_private/konn3kt/hephaestus/vw_pq_flasher.cpython-312-aarch64-linux-gnu.so": "RzZ8ZwI9zExHqNph32fB54/fs1oZZHHFoYWlqR5P53QPKws15FJ/hPalVSaijO9rgspgNSHMOZ6Z/ln3Z3JpDg==",
|
||||
"python/iqpilot_private/konn3kt/uploaderd/iquploaderd.cpython-312-aarch64-linux-gnu.so": "3W7enZXiH/fkwzwm3YMt3lw8OvetDzHK08jeZdNvQ6JMTjojSiO/jgulmOplMaNOrdp4aC/C1jOER70DwzxPCQ=="
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
aT5ZgWVbq0qWHt3m8wQmPWVfaFivYO6zCFB2LxmMC6bn1ZS+k/tNYZTbIANNFwhKGM4Q6zdQeGVicoDreqtYBw==
|
||||
cQ9jTWO/J9039E1TJAs/oACuUTbhSRciZy07VantcDjTofWJPxCZ32Go8eqQPHipjbeXU8Oo4jJlNdjsSg2DAg==
|
||||
|
||||
@@ -29,6 +29,7 @@ _ACTIVE_BUNDLE_KEY = "ModelManager_ActiveBundle"
|
||||
_MODELS_CACHE_KEY = "ModelManager_ModelsCache"
|
||||
_RUNNER_CACHE_KEY = "ModelRunnerTypeCache"
|
||||
_DOWNLOAD_INDEX_KEY = "ModelManager_DownloadIndex"
|
||||
_PENDING_INDEX_KEY = "ModelManager_PendingIndex"
|
||||
_PENDING_MODEL_RESTORE_FILE = "/data/k3_pending_model_restore"
|
||||
_STOCK_RUNNER = int(Runner.stock)
|
||||
_TINYGRAD_RUNNER = int(Runner.tinygrad)
|
||||
@@ -226,6 +227,7 @@ def select_default_model(params: Params = None) -> None:
|
||||
bundle_dict = _load_default_bundle_dict()
|
||||
ensure_default_model_files(bundle_dict)
|
||||
params.remove(_DOWNLOAD_INDEX_KEY)
|
||||
params.remove(_PENDING_INDEX_KEY)
|
||||
params.put(_ACTIVE_BUNDLE_KEY, bundle_dict)
|
||||
params.remove(_RUNNER_CACHE_KEY)
|
||||
params.put(_RUNNER_CACHE_KEY, _TINYGRAD_RUNNER)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -46,8 +46,8 @@
|
||||
},
|
||||
"python/_iqclosure/iqpilot/common/realtime.py": {
|
||||
"mode": 420,
|
||||
"sha256": "ff112acc1fdc227e2ffa4b0fa74f27b967a488bb72fe21c5be82fc70d1104f02",
|
||||
"size": 3488
|
||||
"sha256": "f137ef62c1602a1f7c329731883dc518c62288bd25596891858d460ae3209ae2",
|
||||
"size": 3966
|
||||
},
|
||||
"python/_iqclosure/iqpilot/common/swaglog.py": {
|
||||
"mode": 420,
|
||||
@@ -91,18 +91,18 @@
|
||||
},
|
||||
"python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/__init__.py": {
|
||||
"mode": 420,
|
||||
"sha256": "e5ddcbc7b4dc348957c3d141c6bf8e7c321067f2ac56aecec24f6de107f9c2d3",
|
||||
"size": 81
|
||||
"sha256": "5865243a95ca4557d2a960302124a9898545cc3a8387e88ef569308e3d0f7077",
|
||||
"size": 119
|
||||
},
|
||||
"python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/fetcher.py": {
|
||||
"mode": 420,
|
||||
"sha256": "0487789aec270f9a712a19afafe0b686f825f06f57006c2346c052d3622ac5f8",
|
||||
"size": 619
|
||||
"sha256": "23622cdec9eed5649a05422ba19eae956ba16a63850927af4a222c3d9030a1bb",
|
||||
"size": 399
|
||||
},
|
||||
"python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/helpers.py": {
|
||||
"mode": 420,
|
||||
"sha256": "5ddd486e47cdf811c0c293c460aba2d3b15bb30059ff6da8da775c5cd236465b",
|
||||
"size": 10945
|
||||
"sha256": "e4f524c7595754fd9db86724a7bd9b67524c630275ac78599ab6838d1aa7d809",
|
||||
"size": 11037
|
||||
},
|
||||
"python/_iqclosure/iqpilot/system/__init__.py": {
|
||||
"mode": 420,
|
||||
@@ -121,8 +121,8 @@
|
||||
},
|
||||
"python/_iqclosure/iqpilot/system/hardware/hw.py": {
|
||||
"mode": 420,
|
||||
"sha256": "32da46465ff0a232d21c6d54a54cf387db5e8441e568fe6c169abf9b8a016053",
|
||||
"size": 3441
|
||||
"sha256": "c69185d8f6482c8b021f687b793246d2e2ede853e89a88748c64b85f60711b59",
|
||||
"size": 3638
|
||||
},
|
||||
"python/_iqclosure/iqpilot/system/hardware/pc/__init__.py": {
|
||||
"mode": 420,
|
||||
@@ -146,8 +146,8 @@
|
||||
},
|
||||
"python/_iqclosure/iqpilot/system/hardware/tici/hardware.py": {
|
||||
"mode": 420,
|
||||
"sha256": "49d817c9df4b25524190fd92c6899c4c6bfb0f101f2c58a53f0a99a7933f93b9",
|
||||
"size": 25530
|
||||
"sha256": "f1ddffa9bdb5071a49fd65eb4b49958b60b1873e1ec70e6f3f2e2b2501abce2f",
|
||||
"size": 25584
|
||||
},
|
||||
"python/_iqclosure/iqpilot/system/hardware/tici/iwlist.py": {
|
||||
"mode": 420,
|
||||
@@ -166,8 +166,8 @@
|
||||
},
|
||||
"python/_iqclosure/iqpilot/system/version.py": {
|
||||
"mode": 420,
|
||||
"sha256": "b39a686854c9bfa3c6db9158f68734d7044760cf7ee79980102b7d23230836fd",
|
||||
"size": 5909
|
||||
"sha256": "3a6bc7520dae7944a1da6f911cd090704a463e011f39949ea6c9df51b0e8c616",
|
||||
"size": 6033
|
||||
},
|
||||
"python/iqpilot_private/__init__.py": {
|
||||
"mode": 420,
|
||||
@@ -176,7 +176,7 @@
|
||||
},
|
||||
"python/iqpilot_private/models/__init__.cpython-312-aarch64-linux-gnu.so": {
|
||||
"mode": 493,
|
||||
"sha256": "6aa9c6c725971a6ea6e1781dcc752297b072599a8a5d1ba78605ea69617a0a18",
|
||||
"sha256": "c0d8a584bfeb79f0a109108ff463a95a57db24635f48d936461dddf4673e25b4",
|
||||
"size": 67616
|
||||
},
|
||||
"python/iqpilot_private/models/__init__.py": {
|
||||
@@ -186,23 +186,23 @@
|
||||
},
|
||||
"python/iqpilot_private/models/fetcher.cpython-312-aarch64-linux-gnu.so": {
|
||||
"mode": 493,
|
||||
"sha256": "dfbd700d0d637203969c39f9270d3aa2b8e1fb882ca3e852ef8c6055c828119b",
|
||||
"size": 136176
|
||||
"sha256": "fe34e89be0539fd173457cadc1fdf3a1467d6fefd965031c37cc3d5566f8b1bb",
|
||||
"size": 136200
|
||||
},
|
||||
"python/iqpilot_private/models/git_auth.cpython-312-aarch64-linux-gnu.so": {
|
||||
"mode": 493,
|
||||
"sha256": "532beea0658a2cc045b3039b6aa5d2c16e0225c5046b86e48fba745f93419f32",
|
||||
"sha256": "9e7dd5fbf72f7737130fb36718a2b212afa55757b39ad6ad99d7e4643fa852fa",
|
||||
"size": 67864
|
||||
},
|
||||
"python/iqpilot_private/models/helpers.cpython-312-aarch64-linux-gnu.so": {
|
||||
"mode": 493,
|
||||
"sha256": "650f1b94ad3bd468718096d194c69768779541cf7c20069cbb9bac4c06e1e016",
|
||||
"size": 137936
|
||||
"sha256": "8bedb7406b942dbee7274cd97bb448ded69284911f9760dd702bb90e44325d20",
|
||||
"size": 137944
|
||||
},
|
||||
"python/iqpilot_private/models/manager.cpython-312-aarch64-linux-gnu.so": {
|
||||
"mode": 493,
|
||||
"sha256": "6ad8abc92158cf45e27999520d7712d69c7246cf2e5a0682fc9f15d2f6ed6b33",
|
||||
"size": 274232
|
||||
"sha256": "65f503a85a00b729f0f4eeb033601f7978a0aba81fd4ba3ea58c0a3d18e45dad",
|
||||
"size": 340000
|
||||
},
|
||||
"runtime": {
|
||||
"entries": {
|
||||
@@ -213,10 +213,10 @@
|
||||
}
|
||||
},
|
||||
"signatures": {
|
||||
"python/iqpilot_private/models/__init__.cpython-312-aarch64-linux-gnu.so": "xO18IfX1kJN00FcBH4UqAWLU5qE+K2z/tTYRkkCf1W+hEUyZTcID1xFwhA1wafpMnBNCd0mVJUMc9oaYaFdjBA==",
|
||||
"python/iqpilot_private/models/fetcher.cpython-312-aarch64-linux-gnu.so": "gk48ywZ8vN9RZCxpNEG7OKM1eWTJfb3qMRWX2/BfGJ5HGshPOkKN48EUmvJgpClA9AXaIRQlGppzsRdG8iOOAg==",
|
||||
"python/iqpilot_private/models/git_auth.cpython-312-aarch64-linux-gnu.so": "u5H2u7uxdyRaoYvXSkfosXJZc5gLmE2H8kmwTMWquuo2gKrqbXDB6noRGgJbj7qDtzZ5Q9k9RReLTfmOqRnJBQ==",
|
||||
"python/iqpilot_private/models/helpers.cpython-312-aarch64-linux-gnu.so": "ihqRyFfd5K62WCGstETp5xyFbkFwafUmBBobxke7NyFPtKH3Qxbr6rUvNRilfGl5NzB3aT6mpmxbbomKj1iUAw==",
|
||||
"python/iqpilot_private/models/manager.cpython-312-aarch64-linux-gnu.so": "xRQGJvIRdFO/RHyqpWKXuh6AWAERHVs+0tts4UiAR7s9gzQIdFc96DXtHqmE2b1zTQLKUy8K22kX7gr4JovUAw=="
|
||||
"python/iqpilot_private/models/__init__.cpython-312-aarch64-linux-gnu.so": "9lPKvj8ZTe6wKhWcX4Bha8oHtb/BbWQThP610EWKYWHYNDm6mkUVKGFcyhkH+tmy+PSk5eGDjBnxS0tq/TstBw==",
|
||||
"python/iqpilot_private/models/fetcher.cpython-312-aarch64-linux-gnu.so": "BbwoBwc9jhIgfty30cSgr1mD0u633HyT9MmO7Um7/CrcF9bO5uyhE5hDHsotvHaQmwKnTBGW4+D9hLs8xNo0Bw==",
|
||||
"python/iqpilot_private/models/git_auth.cpython-312-aarch64-linux-gnu.so": "D4SIcunDU2v7PQ+DExObejMtufrJVn8LQDl6SwyRr9EzoemViY0dS/Ff7tTz0KwbvzpHz10ksOCYn2/3t6uxAg==",
|
||||
"python/iqpilot_private/models/helpers.cpython-312-aarch64-linux-gnu.so": "g2oM0JvMIJOvhat23cMmWq8QY+jAAsrQFEFZSk9sC/Kq4aqDZZ/KthrmlLQaNclXtkATJShOxfIlPuEVFzpzAg==",
|
||||
"python/iqpilot_private/models/manager.cpython-312-aarch64-linux-gnu.so": "ZGu3WKX4AELSUXdXpPu4IF+gjzLeRqx1jdi+WGcnCJK17dKtvui9OQ0s3uF3j37DnvBMjpjxnnvwwIQjcEjoAQ=="
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
ryT5nYzhX7vJdIvgT2U4dWDom4BNtUtffuIUrVZbwhtQHJVBNawplodSLlHxOvQEyd+u6+xKP81NhSoTEpMqAg==
|
||||
rL5ip/O3aYwgySr2d0kyzVfFnyOTfAAwVbDl8lxmNJIjPkpSLsEDrgglLQ5MWjmL4s2oHeJA70f2ahx31WgVBw==
|
||||
|
||||
@@ -41,6 +41,12 @@ def config_realtime_process(cores: int | list[int], priority: int) -> None:
|
||||
set_core_affinity(c)
|
||||
|
||||
|
||||
def config_background_thread() -> None:
|
||||
if sys.platform == 'linux' and not PC:
|
||||
os.sched_setscheduler(0, os.SCHED_OTHER, os.sched_param(0))
|
||||
set_core_affinity(list(range(os.cpu_count() or 1)))
|
||||
|
||||
|
||||
def lock_memory() -> None:
|
||||
"""mlockall this process so memory reclaim/compaction can't stall it. RT control
|
||||
procs only (locking ui/modeld would worsen pressure). Best-effort."""
|
||||
@@ -76,6 +82,13 @@ class Ratekeeper:
|
||||
self.avg_dt = MovingAverage(100)
|
||||
self.avg_dt.add_value(self._interval)
|
||||
|
||||
def reset(self) -> None:
|
||||
self._remaining = 0.0
|
||||
self._last_monitor_time = -1.
|
||||
self._next_frame_time = -1.
|
||||
self.avg_dt = MovingAverage(100)
|
||||
self.avg_dt.add_value(self._interval)
|
||||
|
||||
@property
|
||||
def frame(self) -> int:
|
||||
return self._frame
|
||||
@@ -84,6 +97,10 @@ class Ratekeeper:
|
||||
def remaining(self) -> float:
|
||||
return self._remaining
|
||||
|
||||
@property
|
||||
def lag(self) -> float:
|
||||
return max(0., -self._remaining)
|
||||
|
||||
@property
|
||||
def lagging(self) -> bool:
|
||||
expected_dt = self._interval * (1 / 0.9)
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
"""
|
||||
IQ model selection and runner support that is actively used by iqmodeld.
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Copyright (c) IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
|
||||
|
||||
Public entry point for the model-manifest fetcher: prefers the compiled private
|
||||
bundle, falling back to the in-tree source. The default-runner fallback lives in
|
||||
ManifestDecoder now, so no post-import patching is needed.
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
|
||||
from iqpilot._proprietary_loader import ProprietaryModuleMissing, load_private_module
|
||||
|
||||
try:
|
||||
|
||||
@@ -29,6 +29,7 @@ _ACTIVE_BUNDLE_KEY = "ModelManager_ActiveBundle"
|
||||
_MODELS_CACHE_KEY = "ModelManager_ModelsCache"
|
||||
_RUNNER_CACHE_KEY = "ModelRunnerTypeCache"
|
||||
_DOWNLOAD_INDEX_KEY = "ModelManager_DownloadIndex"
|
||||
_PENDING_INDEX_KEY = "ModelManager_PendingIndex"
|
||||
_PENDING_MODEL_RESTORE_FILE = "/data/k3_pending_model_restore"
|
||||
_STOCK_RUNNER = int(Runner.stock)
|
||||
_TINYGRAD_RUNNER = int(Runner.tinygrad)
|
||||
@@ -40,7 +41,6 @@ _DEFAULT_BUNDLE_REF = "default"
|
||||
|
||||
|
||||
def get_default_model_bundle(_bundles):
|
||||
"""Legacy compatibility hook: stock default is preinstalled, not a manifest bundle."""
|
||||
return None
|
||||
|
||||
|
||||
@@ -227,6 +227,7 @@ def select_default_model(params: Params = None) -> None:
|
||||
bundle_dict = _load_default_bundle_dict()
|
||||
ensure_default_model_files(bundle_dict)
|
||||
params.remove(_DOWNLOAD_INDEX_KEY)
|
||||
params.remove(_PENDING_INDEX_KEY)
|
||||
params.put(_ACTIVE_BUNDLE_KEY, bundle_dict)
|
||||
params.remove(_RUNNER_CACHE_KEY)
|
||||
params.put(_RUNNER_CACHE_KEY, _TINYGRAD_RUNNER)
|
||||
@@ -239,10 +240,13 @@ def select_default_model(params: Params = None) -> None:
|
||||
|
||||
def seed_default_bundle_if_unset(params: Params = None) -> None:
|
||||
params = Params() if params is None else params
|
||||
if params.get(_ACTIVE_BUNDLE_KEY) or params.get(_DOWNLOAD_INDEX_KEY) is not None:
|
||||
if params.get(_ACTIVE_BUNDLE_KEY):
|
||||
return
|
||||
queued_download = params.get(_DOWNLOAD_INDEX_KEY)
|
||||
try:
|
||||
select_default_model(params)
|
||||
if queued_download is not None:
|
||||
params.put(_DOWNLOAD_INDEX_KEY, queued_download)
|
||||
cloudlog.warning("default_model: seeded Default (CD210) as active bundle")
|
||||
except Exception as e:
|
||||
cloudlog.exception(f"default_model: failed to seed default bundle: {e}")
|
||||
|
||||
@@ -28,6 +28,12 @@ class Paths:
|
||||
def comma_home() -> str:
|
||||
return os.path.join(str(Path.home()), ".comma" + os.environ.get("OPENPILOT_PREFIX", ""))
|
||||
|
||||
@staticmethod
|
||||
def params() -> str:
|
||||
if os.environ.get("PARAMS_ROOT"):
|
||||
return os.environ["PARAMS_ROOT"]
|
||||
return os.path.join(Paths.comma_home(), "params") if PC else "/data/params"
|
||||
|
||||
@staticmethod
|
||||
def log_root() -> str:
|
||||
if os.environ.get('LOG_ROOT', False):
|
||||
|
||||
@@ -515,7 +515,7 @@ class Tici(HardwareBase):
|
||||
try:
|
||||
pid = subprocess.check_output(["pgrep", "-f", "spi0"], encoding='utf8').strip()
|
||||
subprocess.call(["sudo", "chrt", "-f", "-p", "1", pid])
|
||||
subprocess.call(["sudo", "taskset", "-pc", "3", pid])
|
||||
subprocess.call(["sudo", "taskset", "-pc", "3", pid], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
except subprocess.CalledProcessException as e:
|
||||
print(str(e))
|
||||
|
||||
|
||||
@@ -33,8 +33,13 @@ def get_version(path: str = BASEDIR) -> str:
|
||||
|
||||
|
||||
def get_release_notes(path: str = BASEDIR) -> str:
|
||||
with open(os.path.join(path, "docs", "CHANGELOG.md")) as f:
|
||||
return f.read().split('\n\n', 1)[0]
|
||||
for rel in (("iqpilot", "docs", "CHANGELOG.md"), ("docs", "CHANGELOG.md")):
|
||||
try:
|
||||
with open(os.path.join(path, *rel)) as f:
|
||||
return f.read().split('\n\n', 1)[0]
|
||||
except OSError:
|
||||
continue
|
||||
return ""
|
||||
|
||||
|
||||
@cache
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
163
artifacts/package_sources/teleoprtc/.gitignore
vendored
163
artifacts/package_sources/teleoprtc/.gitignore
vendored
@@ -1,163 +0,0 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
#pdm.lock
|
||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||
# in version control.
|
||||
# https://pdm.fming.dev/#use-with-ide
|
||||
.pdm.toml
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
*.swp
|
||||
*.swo
|
||||
@@ -1,20 +0,0 @@
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.5.0
|
||||
hooks:
|
||||
- id: check-ast
|
||||
- id: check-json
|
||||
- id: check-xml
|
||||
- id: check-yaml
|
||||
- id: check-merge-conflict
|
||||
- id: check-symlinks
|
||||
- id: check-executables-have-shebangs
|
||||
- id: check-shebang-scripts-are-executable
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v1.7.1
|
||||
hooks:
|
||||
- id: mypy
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.2.2
|
||||
hooks:
|
||||
- id: ruff
|
||||
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023, Comma.ai, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,4 +0,0 @@
|
||||
# teleoprtc
|
||||
|
||||
Set of abstractions for webRTC communication with [openpilot](https://github.com/commaai/openpilot).
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
## Face detection demo
|
||||
|
||||
Run simple face detection model on video stream from driver camera of comma three.
|
||||
|
||||
This example streams video frames, runs face-detection model and displays window with live detection results (bounding boxes).
|
||||
|
||||
```sh
|
||||
# pass the ip address of comma three, if running remotely (by default localhost)
|
||||
python3 face_detection.py [--host comma-ip-address]
|
||||
```
|
||||
@@ -1,119 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
|
||||
import aiortc
|
||||
import aiohttp
|
||||
import cv2
|
||||
import pygame
|
||||
|
||||
from teleoprtc import WebRTCOfferBuilder, StreamingOffer
|
||||
|
||||
|
||||
def pygame_should_quit():
|
||||
for event in pygame.event.get():
|
||||
if event.type == pygame.QUIT:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class WebrtcdConnectionProvider:
|
||||
"""
|
||||
Connection provider reaching webrtcd server on comma three
|
||||
"""
|
||||
def __init__(self, host, port=5001):
|
||||
self.url = f"http://{host}:{port}/stream"
|
||||
|
||||
async def __call__(self, offer: StreamingOffer) -> aiortc.RTCSessionDescription:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
body = {'sdp': offer.sdp, 'cameras': offer.video, 'bridge_services_in': [], 'bridge_services_out': []}
|
||||
async with session.post(self.url, json=body) as resp:
|
||||
payload = await resp.json()
|
||||
answer = aiortc.RTCSessionDescription(**payload)
|
||||
return answer
|
||||
|
||||
|
||||
class FaceDetector:
|
||||
"""
|
||||
Simple face detector using opencv
|
||||
"""
|
||||
def __init__(self):
|
||||
self.classifier = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_default.xml')
|
||||
|
||||
def detect(self, array):
|
||||
gray_array = cv2.cvtColor(array, cv2.COLOR_RGB2GRAY)
|
||||
faces = self.classifier.detectMultiScale(gray_array, scaleFactor=1.1, minNeighbors=5, minSize=(30, 30))
|
||||
return faces
|
||||
|
||||
def draw(self, array, faces):
|
||||
for (x, y, w, h) in faces:
|
||||
cv2.rectangle(array, (x, y), (x + w, y + h), (0, 255, 0), 2)
|
||||
return array
|
||||
|
||||
|
||||
async def run_face_detection(stream):
|
||||
# setup pygame window
|
||||
pygame.init()
|
||||
screen_width, screen_height = 1280, 720
|
||||
screen = pygame.display.set_mode((screen_width, screen_height))
|
||||
pygame.display.set_caption("Face detection demo")
|
||||
surface = pygame.Surface((screen_width, screen_height))
|
||||
|
||||
# get the driver camera video track from the stream
|
||||
# generally its better to reuse the track object instead of getting it every time
|
||||
track = stream.get_incoming_video_track("driver", buffered=False)
|
||||
# cv2 face detector
|
||||
detector = FaceDetector()
|
||||
while stream.is_connected_and_ready and not pygame_should_quit():
|
||||
try:
|
||||
# receive frame as pyAV VideoFrame, convert to rgb24 numpy array
|
||||
frame = await track.recv()
|
||||
array = frame.to_ndarray(format="rgb24")
|
||||
|
||||
# detect faces and draw rects around them
|
||||
resized_array = cv2.resize(array, (screen_width, screen_height))
|
||||
faces = detector.detect(resized_array)
|
||||
detector.draw(resized_array, faces)
|
||||
|
||||
# display the image
|
||||
pygame.surfarray.blit_array(surface, resized_array.swapaxes(0, 1))
|
||||
screen.blit(surface, (0, 0))
|
||||
pygame.display.flip()
|
||||
|
||||
print("Received frame from", "driver", frame.time)
|
||||
except aiortc.mediastreams.MediaStreamError:
|
||||
break
|
||||
|
||||
pygame.quit()
|
||||
await stream.stop()
|
||||
|
||||
|
||||
async def run(args):
|
||||
# build your own the offer stream
|
||||
builder = WebRTCOfferBuilder(WebrtcdConnectionProvider(args.host))
|
||||
# request video stream from drivers camera
|
||||
builder.offer_to_receive_video_stream("driver")
|
||||
# add cereal messaging streaming support
|
||||
builder.add_messaging()
|
||||
|
||||
stream = builder.stream()
|
||||
|
||||
# start the stream then wait for connection
|
||||
# server will receive the offer and attempt to fulfill it
|
||||
await stream.start()
|
||||
await stream.wait_for_connection()
|
||||
# all the tracks and channel are ready to be used at this point
|
||||
|
||||
assert stream.has_incoming_video_track("driver") and stream.has_messaging_channel()
|
||||
|
||||
# run face detection loop on the drivers camera
|
||||
await run_face_detection(stream)
|
||||
|
||||
|
||||
if __name__=='__main__':
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--host", default="localhost", help="Host for webrtcd server")
|
||||
|
||||
args = parser.parse_args()
|
||||
asyncio.run(run(args))
|
||||
@@ -1 +0,0 @@
|
||||
# VideoStream CLI
|
||||
@@ -1,111 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import dataclasses
|
||||
import json
|
||||
import logging
|
||||
|
||||
import aiortc
|
||||
from aiortc.mediastreams import VideoStreamTrack, AudioStreamTrack
|
||||
|
||||
from teleoprtc import WebRTCOfferBuilder, WebRTCAnswerBuilder
|
||||
from teleoprtc.stream import StreamingOffer
|
||||
from teleoprtc.info import parse_info_from_offer
|
||||
|
||||
|
||||
async def async_input():
|
||||
return await asyncio.to_thread(input)
|
||||
|
||||
|
||||
async def StdioConnectionProvider(offer: StreamingOffer) -> aiortc.RTCSessionDescription:
|
||||
print("-- Please send this JSON to server --")
|
||||
print(json.dumps(dataclasses.asdict(offer)))
|
||||
print("-- Press enter when the answer is ready --")
|
||||
raw_payload = await async_input()
|
||||
payload = json.loads(raw_payload)
|
||||
answer = aiortc.RTCSessionDescription(**payload)
|
||||
|
||||
return answer
|
||||
|
||||
|
||||
async def run_answer(args):
|
||||
streams = []
|
||||
while True:
|
||||
print("-- Please enter a JSON from client --")
|
||||
raw_payload = await async_input()
|
||||
|
||||
payload = json.loads(raw_payload)
|
||||
offer = StreamingOffer(**payload)
|
||||
info = parse_info_from_offer(offer.sdp)
|
||||
assert len(offer.video) == info.n_expected_camera_tracks
|
||||
video_tracks = [VideoStreamTrack() for _ in offer.video]
|
||||
audio_tracks = [AudioStreamTrack()] if info.expected_audio_track else []
|
||||
|
||||
stream_builder = WebRTCAnswerBuilder(offer.sdp)
|
||||
for cam, track in zip(offer.video, video_tracks, strict=True):
|
||||
stream_builder.add_video_stream(cam, track)
|
||||
for track in audio_tracks:
|
||||
stream_builder.add_audio_stream(track)
|
||||
stream = stream_builder.stream()
|
||||
answer = await stream.start()
|
||||
streams.append(stream)
|
||||
|
||||
print("-- Please send this JSON to client --")
|
||||
print(json.dumps({"sdp": answer.sdp, "type": answer.type}))
|
||||
|
||||
await stream.wait_for_connection()
|
||||
|
||||
|
||||
async def run_offer(args):
|
||||
stream_builder = WebRTCOfferBuilder(StdioConnectionProvider)
|
||||
for cam in args.cameras:
|
||||
stream_builder.offer_to_receive_video_stream(cam)
|
||||
if args.audio:
|
||||
stream_builder.offer_to_receive_audio_stream()
|
||||
if args.messaging:
|
||||
stream_builder.add_messaging()
|
||||
stream = stream_builder.stream()
|
||||
_ = await stream.start()
|
||||
await stream.wait_for_connection()
|
||||
print("Connection established and all tracks are ready")
|
||||
|
||||
video_tracks = [stream.get_incoming_video_track(cam, False) for cam in args.cameras]
|
||||
audio_track = None
|
||||
if stream.has_incoming_audio_track():
|
||||
audio_track = stream.get_incoming_audio_track(False)
|
||||
while True:
|
||||
try:
|
||||
frames = await asyncio.gather(*[track.recv() for track in video_tracks])
|
||||
for key, frame in zip(args.cameras, frames, strict=True):
|
||||
print("Received frame from", key, frame.time)
|
||||
if audio_track:
|
||||
frame = await audio_track.recv()
|
||||
print("Received frame from audio", frame.time)
|
||||
except aiortc.mediastreams.MediaStreamError:
|
||||
return
|
||||
print("=====================================")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
offer_parser = subparsers.add_parser("offer", description="Create offer stream")
|
||||
offer_parser.add_argument("--audio", action="store_true", help="Offer to receive audio")
|
||||
offer_parser.add_argument("--messaging", action="store_true", help="Add messaging support")
|
||||
offer_parser.add_argument("cameras", metavar="CAMERA", type=str, nargs="+", default=[], help="Camera types to stream")
|
||||
|
||||
answer_parser = subparsers.add_parser("answer", description="Create answer stream")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
logging.basicConfig(level=logging.CRITICAL, handlers=[logging.StreamHandler()])
|
||||
logger = logging.getLogger("WebRTCStream")
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
if args.command == "offer":
|
||||
loop.run_until_complete(run_offer(args))
|
||||
elif args.command == "answer":
|
||||
loop.run_until_complete(run_answer(args))
|
||||
@@ -1,52 +0,0 @@
|
||||
[build-system]
|
||||
requires = ["setuptools"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "teleoprtc"
|
||||
version = "1.0.1"
|
||||
authors = [{ name="Vehicle Researcher", email="user@comma.ai" }]
|
||||
description = "Comma webRTC abstractions"
|
||||
readme = "README.md"
|
||||
license = { file="LICENSE" }
|
||||
requires-python = ">=3.8"
|
||||
classifiers = [
|
||||
"Programming Language :: Python :: 3",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Operating System :: OS Independent",
|
||||
]
|
||||
dependencies = [
|
||||
"aiortc>=1.6.0",
|
||||
"aiohttp>=3.7.0",
|
||||
"av>=11.0.0,<13.0.0",
|
||||
"numpy>=1.19.0",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"parameterized>=0.8",
|
||||
"pre-commit",
|
||||
"pytest",
|
||||
"pytest-asyncio",
|
||||
"pytest-xdist"
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
"Homepage" = "https://github.com/commaai/teleoprtc"
|
||||
"Bug Tracker" = "https://github.com/commaai/teleoprtc/issues"
|
||||
|
||||
# https://beta.ruff.rs/docs/configuration/#using-pyprojecttoml
|
||||
[tool.ruff]
|
||||
line-length = 160
|
||||
target-version="py38"
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = ["E", "F", "W", "PIE", "C4", "ISC", "RUF008", "RUF100", "A", "B", "TID251"]
|
||||
ignore = ["W292", "E741", "E402", "C408", "ISC003", "B027", "B024"]
|
||||
flake8-implicit-str-concat.allow-multiline=false
|
||||
|
||||
[tool.ruff.lint.flake8-tidy-imports.banned-api]
|
||||
"unittest".msg = "Use pytest"
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
addopts = "--durations=10 -n auto"
|
||||
@@ -1,29 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
BRANCH="$(git branch --show-current)"
|
||||
TOML_VERSION="$(python3 -c 'import tomllib; print(tomllib.load(open("pyproject.toml", "rb"))["project"]["version"])')"
|
||||
LATEST_TAG_VERSION="$(git tag --list | sort -V -r | head -n 1)"
|
||||
TAGGED_VERSION=""
|
||||
|
||||
if [[ "$BRANCH" != "master" ]]; then
|
||||
echo "Not on master branch."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$TOML_VERSION" == "$LATEST_TAG_VERSION" ]]; then
|
||||
TAGGED_VERSION=$(echo "$TOML_VERSION" | python3 -c "v = input().split('.'); v[-1]=str(int(v[-1])+1); print('.'.join(v))")
|
||||
sed -i "s/version = \"$TOML_VERSION\"/version = \"$TAGGED_VERSION\"/" pyproject.toml
|
||||
elif [[ -z "$LATEST_TAG_VERSION" ]] || printf "$LATEST_TAG_VERSION\n$TOML_VERSION" | sort -V -C; then
|
||||
TAGGED_VERSION="$TOML_VERSION"
|
||||
else
|
||||
echo "Version in pyproject.toml is lower than the latest tag version."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Tagging $TAGGED_VERSION..."
|
||||
if [[ -n "$(git ls-files -m | grep pyproject.toml)" ]]; then
|
||||
echo "Commiting pyproject.toml..."
|
||||
git add pyproject.toml
|
||||
git commit --no-verify -m "Bump version to $TAGGED_VERSION"
|
||||
fi
|
||||
git tag "$TAGGED_VERSION"
|
||||
@@ -1,23 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
if [[ -z "$1" ]]; then
|
||||
echo "Usage: $0 <PyPI token>"
|
||||
exit 1
|
||||
fi
|
||||
PYPI_TOKEN="$1"
|
||||
|
||||
# install required packages
|
||||
pip install --upgrade twine build
|
||||
|
||||
# build the package
|
||||
python3 -m build
|
||||
|
||||
# upload to PyPI
|
||||
REPOSITORY=""
|
||||
if [[ -n "$TEST_UPLOAD" ]]; then
|
||||
REPOSITORY="--repository testpypi"
|
||||
fi
|
||||
|
||||
python3 -m twine upload $REPOSITORY --username __token__ --password "$PYPI_TOKEN" dist/*
|
||||
@@ -1,2 +0,0 @@
|
||||
from teleoprtc.builder import WebRTCOfferBuilder, WebRTCAnswerBuilder # noqa
|
||||
from teleoprtc.stream import WebRTCBaseStream, StreamingOffer, ConnectionProvider, MessageHandler # noqa
|
||||
@@ -1,83 +0,0 @@
|
||||
import abc
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
import aiortc
|
||||
|
||||
from teleoprtc.stream import WebRTCBaseStream, WebRTCOfferStream, WebRTCAnswerStream, ConnectionProvider
|
||||
from teleoprtc.tracks import TiciVideoStreamTrack, TiciTrackWrapper
|
||||
|
||||
|
||||
class WebRTCStreamBuilder(abc.ABC):
|
||||
@abc.abstractmethod
|
||||
def stream(self) -> WebRTCBaseStream:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class WebRTCOfferBuilder(WebRTCStreamBuilder):
|
||||
def __init__(self, connection_provider: ConnectionProvider, ice_servers: Optional[List[dict]] = None):
|
||||
self.connection_provider = connection_provider
|
||||
self.ice_servers = ice_servers
|
||||
self.requested_camera_types: List[str] = []
|
||||
self.requested_audio = False
|
||||
self.audio_tracks: List[aiortc.MediaStreamTrack] = []
|
||||
self.messaging_enabled = False
|
||||
|
||||
def offer_to_receive_video_stream(self, camera_type: str):
|
||||
assert camera_type in ["driver", "wideRoad", "road"]
|
||||
self.requested_camera_types.append(camera_type)
|
||||
|
||||
def offer_to_receive_audio_stream(self):
|
||||
self.requested_audio = True
|
||||
|
||||
def add_audio_stream(self, track: aiortc.MediaStreamTrack):
|
||||
assert len(self.audio_tracks) == 0
|
||||
self.audio_tracks = [track]
|
||||
|
||||
def add_messaging(self):
|
||||
self.messaging_enabled = True
|
||||
|
||||
def stream(self) -> WebRTCBaseStream:
|
||||
return WebRTCOfferStream(
|
||||
self.connection_provider,
|
||||
consumed_camera_types=self.requested_camera_types,
|
||||
consume_audio=self.requested_audio,
|
||||
video_producer_tracks=[],
|
||||
audio_producer_tracks=self.audio_tracks,
|
||||
should_add_data_channel=self.messaging_enabled,
|
||||
ice_servers=self.ice_servers,
|
||||
)
|
||||
|
||||
|
||||
class WebRTCAnswerBuilder(WebRTCStreamBuilder):
|
||||
def __init__(self, offer_sdp: str, ice_servers: Optional[List[dict]] = None):
|
||||
self.offer_sdp = offer_sdp
|
||||
self.ice_servers = ice_servers
|
||||
self.video_tracks: Dict[str, aiortc.MediaStreamTrack] = dict()
|
||||
self.requested_audio = False
|
||||
self.audio_tracks: List[aiortc.MediaStreamTrack] = []
|
||||
|
||||
def offer_to_receive_audio_stream(self):
|
||||
self.requested_audio = True
|
||||
|
||||
def add_video_stream(self, camera_type: str, track: aiortc.MediaStreamTrack):
|
||||
assert camera_type not in self.video_tracks
|
||||
assert camera_type in ["driver", "wideRoad", "road"]
|
||||
if not isinstance(track, TiciVideoStreamTrack):
|
||||
track = TiciTrackWrapper(camera_type, track)
|
||||
self.video_tracks[camera_type] = track
|
||||
|
||||
def add_audio_stream(self, track: aiortc.MediaStreamTrack):
|
||||
assert len(self.audio_tracks) == 0
|
||||
self.audio_tracks = [track]
|
||||
|
||||
def stream(self) -> WebRTCBaseStream:
|
||||
description = aiortc.RTCSessionDescription(sdp=self.offer_sdp, type="offer")
|
||||
return WebRTCAnswerStream(
|
||||
description,
|
||||
consumed_camera_types=[],
|
||||
consume_audio=self.requested_audio,
|
||||
video_producer_tracks=list(self.video_tracks.values()),
|
||||
audio_producer_tracks=self.audio_tracks,
|
||||
should_add_data_channel=False,
|
||||
ice_servers=self.ice_servers,
|
||||
)
|
||||
@@ -1,26 +0,0 @@
|
||||
import dataclasses
|
||||
|
||||
import aiortc
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class StreamingMediaInfo:
|
||||
n_expected_camera_tracks: int
|
||||
expected_audio_track: bool
|
||||
incoming_audio_track: bool
|
||||
incoming_datachannel: bool
|
||||
|
||||
|
||||
def parse_info_from_offer(sdp: str) -> StreamingMediaInfo:
|
||||
"""
|
||||
helper function to parse info about outgoing and incoming streams from an offer sdp
|
||||
"""
|
||||
desc = aiortc.sdp.SessionDescription.parse(sdp)
|
||||
audio_tracks = [m for m in desc.media if m.kind == "audio"]
|
||||
video_tracks = [m for m in desc.media if m.kind == "video" and m.direction in ["recvonly", "sendrecv"]]
|
||||
application_tracks = [m for m in desc.media if m.kind == "application"]
|
||||
has_incoming_audio_track = next((t for t in audio_tracks if t.direction in ["sendonly", "sendrecv"]), None) is not None
|
||||
has_incoming_datachannel = len(application_tracks) > 0
|
||||
expects_outgoing_audio_track = next((t for t in audio_tracks if t.direction in ["recvonly", "sendrecv"]), None) is not None
|
||||
|
||||
return StreamingMediaInfo(len(video_tracks), expects_outgoing_audio_track, has_incoming_audio_track, has_incoming_datachannel)
|
||||
@@ -1,334 +0,0 @@
|
||||
import abc
|
||||
import asyncio
|
||||
import dataclasses
|
||||
import logging
|
||||
from typing import Any, Awaitable, Callable, Dict, List, Optional
|
||||
|
||||
import aiortc
|
||||
from aiortc.contrib.media import MediaRelay
|
||||
|
||||
from teleoprtc.tracks import parse_video_track_id
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class StreamingOffer:
|
||||
sdp: str
|
||||
video: List[str]
|
||||
|
||||
|
||||
ConnectionProvider = Callable[[StreamingOffer], Awaitable[aiortc.RTCSessionDescription]]
|
||||
MessageHandler = Callable[[bytes], Awaitable[None]]
|
||||
|
||||
|
||||
def _rtc_configuration_from_ice_servers(ice_servers: Optional[List[Dict[str, Any]]]) -> Optional[aiortc.RTCConfiguration]:
|
||||
if ice_servers is None:
|
||||
return None
|
||||
|
||||
servers: List[aiortc.RTCIceServer] = []
|
||||
for s in ice_servers:
|
||||
if not isinstance(s, dict):
|
||||
continue
|
||||
urls = s.get("urls") or s.get("url")
|
||||
if not urls:
|
||||
continue
|
||||
username = s.get("username")
|
||||
credential = s.get("credential")
|
||||
servers.append(aiortc.RTCIceServer(urls=urls, username=username, credential=credential))
|
||||
|
||||
return aiortc.RTCConfiguration(iceServers=servers)
|
||||
|
||||
|
||||
class WebRTCBaseStream(abc.ABC):
|
||||
def __init__(self,
|
||||
consumed_camera_types: List[str],
|
||||
consume_audio: bool,
|
||||
video_producer_tracks: List[aiortc.MediaStreamTrack],
|
||||
audio_producer_tracks: List[aiortc.MediaStreamTrack],
|
||||
should_add_data_channel: bool,
|
||||
ice_servers: Optional[List[Dict[str, Any]]] = None):
|
||||
configuration = _rtc_configuration_from_ice_servers(ice_servers)
|
||||
self.peer_connection = aiortc.RTCPeerConnection(configuration=configuration) if configuration else aiortc.RTCPeerConnection()
|
||||
self.media_relay = MediaRelay()
|
||||
self.expected_incoming_camera_types = consumed_camera_types
|
||||
self.expected_incoming_audio = consume_audio
|
||||
self.expected_number_of_incoming_media: Optional[int] = None
|
||||
|
||||
self.incoming_camera_tracks: Dict[str, aiortc.MediaStreamTrack] = dict()
|
||||
self.incoming_audio_tracks: List[aiortc.MediaStreamTrack] = []
|
||||
self.outgoing_video_tracks: List[aiortc.MediaStreamTrack] = video_producer_tracks
|
||||
self.outgoing_audio_tracks: List[aiortc.MediaStreamTrack] = audio_producer_tracks
|
||||
|
||||
self.should_add_data_channel = should_add_data_channel
|
||||
self.messaging_channel: Optional[aiortc.RTCDataChannel] = None
|
||||
self.incoming_message_handlers: List[MessageHandler] = []
|
||||
|
||||
self.incoming_media_ready_event = asyncio.Event()
|
||||
self.messaging_channel_ready_event = asyncio.Event()
|
||||
self.connection_attempted_event = asyncio.Event()
|
||||
self.connection_stopped_event = asyncio.Event()
|
||||
|
||||
self.peer_connection.on("connectionstatechange", self._on_connectionstatechange)
|
||||
self.peer_connection.on("datachannel", self._on_incoming_datachannel)
|
||||
self.peer_connection.on("track", self._on_incoming_track)
|
||||
|
||||
self.logger = logging.getLogger("WebRTCStream")
|
||||
|
||||
async def _wait_for_ice_gathering_complete(self, timeout_s: float = 5.0):
|
||||
# Non-trickle ICE: wait for candidates to be included in SDP.
|
||||
if self.peer_connection.iceGatheringState == "complete":
|
||||
return
|
||||
|
||||
loop = asyncio.get_running_loop()
|
||||
end = loop.time() + timeout_s
|
||||
while self.peer_connection.iceGatheringState != "complete" and loop.time() < end:
|
||||
await asyncio.sleep(0.05)
|
||||
|
||||
def _log_debug(self, msg: Any, *args):
|
||||
self.logger.debug(f"{type(self)}() {msg}", *args)
|
||||
|
||||
@property
|
||||
def _number_of_incoming_media(self) -> int:
|
||||
media = len(self.incoming_camera_tracks) + len(self.incoming_audio_tracks)
|
||||
# if stream does not add data_channel, then it means its incoming
|
||||
media += int(self.messaging_channel is not None) if not self.should_add_data_channel else 0
|
||||
return media
|
||||
|
||||
def _add_consumer_transceivers(self):
|
||||
for _ in self.expected_incoming_camera_types:
|
||||
self.peer_connection.addTransceiver("video", direction="recvonly")
|
||||
if self.expected_incoming_audio:
|
||||
self.peer_connection.addTransceiver("audio", direction="recvonly")
|
||||
|
||||
def _find_trackless_transceiver(self, kind: str) -> Optional[aiortc.RTCRtpTransceiver]:
|
||||
transceivers = self.peer_connection.getTransceivers()
|
||||
target_transceiver = None
|
||||
for t in transceivers:
|
||||
if t.kind == kind and t.sender.track is None:
|
||||
target_transceiver = t
|
||||
break
|
||||
|
||||
return target_transceiver
|
||||
|
||||
def _add_producer_tracks(self):
|
||||
for track in self.outgoing_video_tracks:
|
||||
target_transceiver = self._find_trackless_transceiver(track.kind)
|
||||
if target_transceiver is None:
|
||||
self.peer_connection.addTransceiver(track.kind, direction="sendonly")
|
||||
|
||||
sender = self.peer_connection.addTrack(track)
|
||||
if hasattr(track, "codec_preference") and track.codec_preference() is not None:
|
||||
transceiver = next(t for t in self.peer_connection.getTransceivers() if t.sender == sender)
|
||||
self._force_codec(transceiver, track.codec_preference(), "video")
|
||||
for track in self.outgoing_audio_tracks:
|
||||
target_transceiver = self._find_trackless_transceiver(track.kind)
|
||||
if target_transceiver is None:
|
||||
self.peer_connection.addTransceiver(track.kind, direction="sendonly")
|
||||
|
||||
self.peer_connection.addTrack(track)
|
||||
|
||||
def _add_messaging_channel(self, channel: Optional[aiortc.RTCDataChannel] = None):
|
||||
if not channel:
|
||||
channel = self.peer_connection.createDataChannel("data", ordered=True)
|
||||
|
||||
for handler in self.incoming_message_handlers:
|
||||
channel.on("message", handler)
|
||||
|
||||
if channel.readyState == "open":
|
||||
self.messaging_channel_ready_event.set()
|
||||
else:
|
||||
channel.on("open", lambda: self.messaging_channel_ready_event.set())
|
||||
self.messaging_channel = channel
|
||||
|
||||
def _force_codec(self, transceiver: aiortc.RTCRtpTransceiver, codec: str, stream_type: str):
|
||||
codec_mime = f"{stream_type}/{codec.upper()}"
|
||||
rtp_codecs = aiortc.RTCRtpSender.getCapabilities(stream_type).codecs
|
||||
rtp_codec = [c for c in rtp_codecs if c.mimeType == codec_mime]
|
||||
transceiver.setCodecPreferences(rtp_codec)
|
||||
|
||||
def _on_connectionstatechange(self):
|
||||
self._log_debug("connection state is %s", self.peer_connection.connectionState)
|
||||
if self.peer_connection.connectionState in ['connected', 'failed']:
|
||||
self.connection_attempted_event.set()
|
||||
if self.peer_connection.connectionState in ['disconnected', 'closed', 'failed']:
|
||||
self.connection_stopped_event.set()
|
||||
|
||||
def _on_incoming_track(self, track: aiortc.MediaStreamTrack):
|
||||
self._log_debug("got track: %s %s", track.kind, track.id)
|
||||
if track.kind == "video":
|
||||
camera_type, _ = parse_video_track_id(track.id)
|
||||
if camera_type in self.expected_incoming_camera_types:
|
||||
self.incoming_camera_tracks[camera_type] = track
|
||||
elif track.kind == "audio":
|
||||
if self.expected_incoming_audio:
|
||||
self.incoming_audio_tracks.append(track)
|
||||
self._on_after_media()
|
||||
|
||||
def _on_incoming_datachannel(self, channel: aiortc.RTCDataChannel):
|
||||
self._log_debug("got data channel: %s", channel.label)
|
||||
if channel.label == "data" and self.messaging_channel is None:
|
||||
self._add_messaging_channel(channel)
|
||||
self._on_after_media()
|
||||
|
||||
def _on_after_media(self):
|
||||
if self._number_of_incoming_media == self.expected_number_of_incoming_media:
|
||||
self.incoming_media_ready_event.set()
|
||||
|
||||
def _parse_incoming_streams(self, remote_sdp: str):
|
||||
desc = aiortc.sdp.SessionDescription.parse(remote_sdp)
|
||||
sending_medias = [m for m in desc.media if m.direction in ["sendonly", "sendrecv"]]
|
||||
incoming_media_count = len(sending_medias)
|
||||
if not self.should_add_data_channel:
|
||||
channel_medias = [m for m in desc.media if m.kind == "application"]
|
||||
incoming_media_count += len(channel_medias)
|
||||
self.expected_number_of_incoming_media = incoming_media_count
|
||||
|
||||
def has_incoming_video_track(self, camera_type: str) -> bool:
|
||||
return camera_type in self.incoming_camera_tracks
|
||||
|
||||
def has_incoming_audio_track(self) -> bool:
|
||||
return len(self.incoming_audio_tracks) > 0
|
||||
|
||||
def has_messaging_channel(self) -> bool:
|
||||
return self.messaging_channel is not None
|
||||
|
||||
def get_incoming_video_track(self, camera_type: str, buffered: bool = False) -> aiortc.MediaStreamTrack:
|
||||
assert camera_type in self.incoming_camera_tracks, "Video tracks are not enabled on this stream"
|
||||
assert self.is_started, "Stream must be started"
|
||||
|
||||
track = self.incoming_camera_tracks[camera_type]
|
||||
relay_track = self.media_relay.subscribe(track, buffered=buffered)
|
||||
return relay_track
|
||||
|
||||
def get_incoming_audio_track(self, buffered: bool = False) -> aiortc.MediaStreamTrack:
|
||||
assert len(self.incoming_audio_tracks) > 0, "Audio tracks are not enabled on this stream"
|
||||
assert self.is_started, "Stream must be started"
|
||||
|
||||
track = self.incoming_audio_tracks[0]
|
||||
relay_track = self.media_relay.subscribe(track, buffered=buffered)
|
||||
return relay_track
|
||||
|
||||
def get_messaging_channel(self) -> aiortc.RTCDataChannel:
|
||||
assert self.messaging_channel is not None, "Messaging channel is not enabled on this stream"
|
||||
assert self.is_started, "Stream must be started"
|
||||
|
||||
return self.messaging_channel
|
||||
|
||||
def set_message_handler(self, message_handler: MessageHandler):
|
||||
self.incoming_message_handlers.append(message_handler)
|
||||
if self.messaging_channel is not None:
|
||||
self.messaging_channel.on("message", message_handler)
|
||||
|
||||
@property
|
||||
def is_started(self) -> bool:
|
||||
return self.peer_connection is not None and \
|
||||
self.peer_connection.localDescription is not None and \
|
||||
self.peer_connection.remoteDescription is not None and \
|
||||
self.peer_connection.connectionState != "closed"
|
||||
|
||||
@property
|
||||
def is_connected_and_ready(self) -> bool:
|
||||
return self.peer_connection is not None and \
|
||||
self.peer_connection.connectionState == "connected" and \
|
||||
(self.expected_number_of_incoming_media == 0 or self.incoming_media_ready_event.is_set())
|
||||
|
||||
async def wait_for_connection(self):
|
||||
assert self.is_started
|
||||
await self.connection_attempted_event.wait()
|
||||
if self.peer_connection.connectionState != 'connected':
|
||||
raise ValueError("Connection failed.")
|
||||
if self.expected_number_of_incoming_media:
|
||||
await self.incoming_media_ready_event.wait()
|
||||
if self.messaging_channel is not None:
|
||||
await self.messaging_channel_ready_event.wait()
|
||||
|
||||
async def wait_for_disconnection(self):
|
||||
assert self.is_connected_and_ready, "Stream is not connected/ready yet (make sure wait_for_connection was awaited)"
|
||||
await self.connection_stopped_event.wait()
|
||||
|
||||
async def stop(self):
|
||||
await self.peer_connection.close()
|
||||
|
||||
@abc.abstractmethod
|
||||
async def start(self) -> aiortc.RTCSessionDescription:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class WebRTCOfferStream(WebRTCBaseStream):
|
||||
def __init__(self, session_provider: ConnectionProvider, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.session_provider = session_provider
|
||||
|
||||
async def start(self) -> aiortc.RTCSessionDescription:
|
||||
self._add_consumer_transceivers()
|
||||
if self.should_add_data_channel:
|
||||
self._add_messaging_channel()
|
||||
self._add_producer_tracks()
|
||||
|
||||
offer = await self.peer_connection.createOffer()
|
||||
await self.peer_connection.setLocalDescription(offer)
|
||||
await self._wait_for_ice_gathering_complete()
|
||||
actual_offer = self.peer_connection.localDescription
|
||||
|
||||
streaming_offer = StreamingOffer(
|
||||
sdp=actual_offer.sdp,
|
||||
video=list(self.expected_incoming_camera_types),
|
||||
)
|
||||
remote_answer = await self.session_provider(streaming_offer)
|
||||
self._parse_incoming_streams(remote_sdp=remote_answer.sdp)
|
||||
await self.peer_connection.setRemoteDescription(remote_answer)
|
||||
actual_answer = self.peer_connection.remoteDescription
|
||||
|
||||
return actual_answer
|
||||
|
||||
|
||||
class WebRTCAnswerStream(WebRTCBaseStream):
|
||||
def __init__(self, session: aiortc.RTCSessionDescription, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.session = session
|
||||
|
||||
def _probe_video_codecs(self) -> List[str]:
|
||||
codecs = []
|
||||
for track in self.outgoing_video_tracks:
|
||||
if hasattr(track, "codec_preference") and track.codec_preference() is not None:
|
||||
codecs.append(track.codec_preference())
|
||||
|
||||
return codecs
|
||||
|
||||
def _override_incoming_video_codecs(self, remote_sdp: str, codecs: List[str]) -> str:
|
||||
desc = aiortc.sdp.SessionDescription.parse(remote_sdp)
|
||||
codec_mimes = [f"video/{c}" for c in codecs]
|
||||
for m in desc.media:
|
||||
if m.kind != "video":
|
||||
continue
|
||||
|
||||
preferred_codecs: List[aiortc.RTCRtpCodecParameters] = [c for c in m.rtp.codecs if c.mimeType in codec_mimes]
|
||||
if len(preferred_codecs) == 0:
|
||||
raise ValueError(f"None of {preferred_codecs} codecs is supported in remote SDP")
|
||||
|
||||
m.rtp.codecs = preferred_codecs
|
||||
m.fmt = [c.payloadType for c in preferred_codecs]
|
||||
|
||||
return str(desc)
|
||||
|
||||
async def start(self) -> aiortc.RTCSessionDescription:
|
||||
assert self.peer_connection.remoteDescription is None, "Connection already established"
|
||||
|
||||
self._add_consumer_transceivers()
|
||||
|
||||
# since we sent already encoded frames in some cases (e.g. livestream video tracks are in H264), we need to force aiortc to actually use it
|
||||
# we do that by overriding supported codec information on incoming sdp
|
||||
preferred_codecs = self._probe_video_codecs()
|
||||
if len(preferred_codecs) > 0:
|
||||
self.session.sdp = self._override_incoming_video_codecs(self.session.sdp, preferred_codecs)
|
||||
|
||||
self._parse_incoming_streams(remote_sdp=self.session.sdp)
|
||||
await self.peer_connection.setRemoteDescription(self.session)
|
||||
|
||||
self._add_producer_tracks()
|
||||
|
||||
answer = await self.peer_connection.createAnswer()
|
||||
await self.peer_connection.setLocalDescription(answer)
|
||||
await self._wait_for_ice_gathering_complete()
|
||||
actual_answer = self.peer_connection.localDescription
|
||||
|
||||
return actual_answer
|
||||
@@ -1,76 +0,0 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import time
|
||||
import fractions
|
||||
from typing import Any, Optional, Tuple
|
||||
|
||||
import aiortc
|
||||
from aiortc.mediastreams import VIDEO_CLOCK_RATE, VIDEO_TIME_BASE
|
||||
|
||||
|
||||
def video_track_id(camera_type: str, track_id: str) -> str:
|
||||
return f"{camera_type}:{track_id}"
|
||||
|
||||
|
||||
def parse_video_track_id(track_id: str) -> Tuple[str, str]:
|
||||
parts = track_id.split(":")
|
||||
if len(parts) != 2:
|
||||
raise ValueError(f"Invalid video track id: {track_id}")
|
||||
|
||||
camera_type, track_id = parts
|
||||
return camera_type, track_id
|
||||
|
||||
|
||||
class TiciVideoStreamTrack(aiortc.MediaStreamTrack):
|
||||
"""
|
||||
Abstract video track which associates video track with camera_type
|
||||
"""
|
||||
kind = "video"
|
||||
|
||||
def __init__(self, camera_type: str, dt: float, time_base: fractions.Fraction = VIDEO_TIME_BASE, clock_rate: int = VIDEO_CLOCK_RATE):
|
||||
assert camera_type in ["driver", "wideRoad", "road"]
|
||||
super().__init__()
|
||||
# override track id to include camera type - client needs that for identification
|
||||
self._id: str = video_track_id(camera_type, self._id)
|
||||
self._dt: float = dt
|
||||
self._time_base: fractions.Fraction = time_base
|
||||
self._clock_rate: int = clock_rate
|
||||
self._start: Optional[float] = None
|
||||
self._logger = logging.getLogger("WebRTCStream")
|
||||
|
||||
def log_debug(self, msg: Any, *args):
|
||||
self._logger.debug(f"{type(self)}() {msg}", *args)
|
||||
|
||||
async def next_pts(self, current_pts) -> float:
|
||||
pts: float = current_pts + self._dt * self._clock_rate
|
||||
|
||||
data_time = pts * self._time_base
|
||||
if self._start is None:
|
||||
self._start = time.time() - data_time
|
||||
else:
|
||||
wait_time = self._start + data_time - time.time()
|
||||
await asyncio.sleep(wait_time)
|
||||
|
||||
return pts
|
||||
|
||||
def codec_preference(self) -> Optional[str]:
|
||||
return None
|
||||
|
||||
|
||||
class TiciTrackWrapper(aiortc.MediaStreamTrack):
|
||||
"""
|
||||
Associates video track with camera_type
|
||||
"""
|
||||
def __init__(self, camera_type: str, track: aiortc.MediaStreamTrack):
|
||||
assert track.kind == "video"
|
||||
assert not isinstance(track, TiciVideoStreamTrack)
|
||||
super().__init__()
|
||||
self._id = video_track_id(camera_type, track.id)
|
||||
self._track = track
|
||||
|
||||
@property
|
||||
def kind(self) -> str:
|
||||
return self._track.kind
|
||||
|
||||
async def recv(self):
|
||||
return await self._track.recv()
|
||||
@@ -1,146 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from teleoprtc.info import parse_info_from_offer
|
||||
|
||||
|
||||
def lf2crlf(x):
|
||||
return x.replace("\n", "\r\n")
|
||||
|
||||
|
||||
class TestStream:
|
||||
def test_double_video_tracks(self):
|
||||
sdp = """v=0
|
||||
o=- 3910210993 3910210993 IN IP4 0.0.0.0
|
||||
s=-
|
||||
t=0 0
|
||||
a=group:BUNDLE 0 1
|
||||
a=msid-semantic:WMS *
|
||||
m=video 9 UDP/TLS/RTP/SAVPF 97 98 99 100 101 102
|
||||
c=IN IP4 0.0.0.0
|
||||
a=recvonly
|
||||
a=extmap:1 urn:ietf:params:rtp-hdrext:sdes:mid
|
||||
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
|
||||
a=mid:0
|
||||
a=msid:e123f852-010c-4b7b-8761-71b72fbfd013 2b75cb0e-6b34-48d6-8bf9-21b809f2e08e
|
||||
a=rtcp:9 IN IP4 0.0.0.0
|
||||
a=rtcp-mux
|
||||
a=ssrc-group:FID 1048118556 4149054509
|
||||
a=ssrc:1048118556 cname:61992fce-bab5-42a0-ab8c-7112adfb1857
|
||||
a=ssrc:4149054509 cname:61992fce-bab5-42a0-ab8c-7112adfb1857
|
||||
a=rtpmap:97 VP8/90000
|
||||
a=rtcp-fb:97 nack
|
||||
a=rtcp-fb:97 nack pli
|
||||
a=rtcp-fb:97 goog-remb
|
||||
a=rtpmap:98 rtx/90000
|
||||
a=fmtp:98 apt=97
|
||||
a=rtpmap:99 H264/90000
|
||||
a=rtcp-fb:99 nack
|
||||
a=rtcp-fb:99 nack pli
|
||||
a=rtcp-fb:99 goog-remb
|
||||
a=fmtp:99 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f
|
||||
a=rtpmap:100 rtx/90000
|
||||
a=fmtp:100 apt=99
|
||||
a=rtpmap:101 H264/90000
|
||||
a=rtcp-fb:101 nack
|
||||
a=rtcp-fb:101 nack pli
|
||||
a=rtcp-fb:101 goog-remb
|
||||
a=fmtp:101 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f
|
||||
a=rtpmap:102 rtx/90000
|
||||
a=fmtp:102 apt=101
|
||||
a=ice-ufrag:jxQW
|
||||
a=ice-pwd:KpJ0tfaY2RxnIYpTHqPSSv
|
||||
a=fingerprint:sha-256 70:3A:2D:37:3C:52:96:0E:10:F6:4D:7A:EB:18:38:1B:FD:CA:A5:90:D7:6C:DA:A9:39:76:C9:2F:FB:FF:56:0C
|
||||
a=setup:actpass
|
||||
m=video 9 UDP/TLS/RTP/SAVPF 97 98 99 100 101 102
|
||||
c=IN IP4 0.0.0.0
|
||||
a=recvonly
|
||||
a=extmap:1 urn:ietf:params:rtp-hdrext:sdes:mid
|
||||
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
|
||||
a=mid:1
|
||||
a=msid:e123f852-010c-4b7b-8761-71b72fbfd013 311db759-8d51-479c-a5b4-5c8d055c43ec
|
||||
a=rtcp:9 IN IP4 0.0.0.0
|
||||
a=rtcp-mux
|
||||
a=ssrc-group:FID 4096183284 2713379498
|
||||
a=ssrc:4096183284 cname:61992fce-bab5-42a0-ab8c-7112adfb1857
|
||||
a=ssrc:2713379498 cname:61992fce-bab5-42a0-ab8c-7112adfb1857
|
||||
a=rtpmap:97 VP8/90000
|
||||
a=rtcp-fb:97 nack
|
||||
a=rtcp-fb:97 nack pli
|
||||
a=rtcp-fb:97 goog-remb
|
||||
a=rtpmap:98 rtx/90000
|
||||
a=fmtp:98 apt=97
|
||||
a=rtpmap:99 H264/90000
|
||||
a=rtcp-fb:99 nack
|
||||
a=rtcp-fb:99 nack pli
|
||||
a=rtcp-fb:99 goog-remb
|
||||
a=fmtp:99 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f
|
||||
a=rtpmap:100 rtx/90000
|
||||
a=fmtp:100 apt=99
|
||||
a=rtpmap:101 H264/90000
|
||||
a=rtcp-fb:101 nack
|
||||
a=rtcp-fb:101 nack pli
|
||||
a=rtcp-fb:101 goog-remb
|
||||
a=fmtp:101 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f
|
||||
a=rtpmap:102 rtx/90000
|
||||
a=fmtp:102 apt=101
|
||||
a=ice-ufrag:1234
|
||||
a=ice-pwd:1234
|
||||
a=fingerprint:sha-256 70:3A:2D:37:3C:52:96:0E:10:F6:4D:7A:EB:18:38:1B:FD:CA:A5:90:D7:6C:DA:A9:39:76:C9:2F:FB:FF:56:0C
|
||||
a=setup:actpass"""
|
||||
info = parse_info_from_offer(lf2crlf(sdp))
|
||||
assert info.n_expected_camera_tracks == 2
|
||||
assert not info.expected_audio_track
|
||||
assert not info.incoming_audio_track
|
||||
assert not info.incoming_datachannel
|
||||
|
||||
def test_recvonly_audio(self):
|
||||
sdp = """v=0
|
||||
o=- 3910210904 3910210904 IN IP4 0.0.0.0
|
||||
s=-
|
||||
t=0 0
|
||||
a=group:BUNDLE 0
|
||||
a=msid-semantic:WMS *
|
||||
m=audio 9 UDP/TLS/RTP/SAVPF 96 0 8
|
||||
c=IN IP4 0.0.0.0
|
||||
a=recvonly
|
||||
a=extmap:1 urn:ietf:params:rtp-hdrext:sdes:mid
|
||||
a=extmap:2 urn:ietf:params:rtp-hdrext:ssrc-audio-level
|
||||
a=mid:0
|
||||
a=msid:eb1d3f1a-569a-465f-b419-319477bfded6 e44eecb2-1a04-4547-97d8-481389f50d5b
|
||||
a=rtcp:9 IN IP4 0.0.0.0
|
||||
a=rtcp-mux
|
||||
a=ssrc:1233332626 cname:ca4dede8-4994-4a6d-9ae3-923b28177ca5
|
||||
a=rtpmap:96 opus/48000/2
|
||||
a=rtpmap:0 PCMU/8000
|
||||
a=rtpmap:8 PCMA/8000
|
||||
a=ice-ufrag:1234
|
||||
a=ice-pwd:1234
|
||||
a=fingerprint:sha-256 40:4B:14:CF:70:B8:67:E1:B1:FF:7E:F9:22:6E:60:7D:73:B5:1E:38:4B:10:20:9C:CD:1C:47:02:52:ED:45:25
|
||||
a=setup:actpass"""
|
||||
info = parse_info_from_offer(lf2crlf(sdp))
|
||||
assert info.n_expected_camera_tracks == 0
|
||||
assert info.expected_audio_track
|
||||
assert not info.incoming_audio_track
|
||||
assert not info.incoming_datachannel
|
||||
|
||||
def test_incoming_datachanel(self):
|
||||
sdp = """v=0
|
||||
o=- 3910211092 3910211092 IN IP4 0.0.0.0
|
||||
s=-
|
||||
t=0 0
|
||||
a=group:BUNDLE 0
|
||||
a=msid-semantic:WMS *
|
||||
m=application 9 DTLS/SCTP 5000
|
||||
c=IN IP4 0.0.0.0
|
||||
a=mid:0
|
||||
a=sctpmap:5000 webrtc-datachannel 65535
|
||||
a=max-message-size:65536
|
||||
a=ice-ufrag:1234
|
||||
a=ice-pwd:1234
|
||||
a=fingerprint:sha-256 9B:C0:F3:35:8E:05:A1:15:DB:F8:39:0E:B0:E0:0C:EB:82:E4:B9:26:18:A6:43:2D:B9:9A:23:96:0A:59:B6:58
|
||||
a=setup:actpass"""
|
||||
info = parse_info_from_offer(lf2crlf(sdp))
|
||||
assert info.n_expected_camera_tracks == 0
|
||||
assert not info.expected_audio_track
|
||||
assert not info.incoming_audio_track
|
||||
assert info.incoming_datachannel
|
||||
@@ -1,131 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import pytest
|
||||
import asyncio
|
||||
import sys
|
||||
|
||||
from aiortc.mediastreams import AudioStreamTrack, VideoStreamTrack
|
||||
from teleoprtc.builder import WebRTCOfferBuilder, WebRTCAnswerBuilder
|
||||
from teleoprtc.stream import StreamingOffer
|
||||
from teleoprtc.info import parse_info_from_offer
|
||||
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
timeout = asyncio.timeout
|
||||
else:
|
||||
class Timeout:
|
||||
def __init__(self, delay: float):
|
||||
self._delay = delay
|
||||
self._task = None
|
||||
self._timeout_handle = None
|
||||
|
||||
def _timeout(self):
|
||||
if self._task:
|
||||
self._task.cancel()
|
||||
|
||||
async def __aenter__(self):
|
||||
self._task = asyncio.current_task()
|
||||
loop = asyncio.events.get_running_loop()
|
||||
self._timeout_handle = loop.call_later(self._delay, self._timeout)
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc, tb):
|
||||
if self._timeout_handle:
|
||||
self._timeout_handle.cancel()
|
||||
if exc_type is asyncio.CancelledError and self._task and self._task.cancelled():
|
||||
raise asyncio.TimeoutError from exc
|
||||
return False
|
||||
|
||||
def timeout(delay):
|
||||
return Timeout(delay)
|
||||
|
||||
|
||||
class SimpleAnswerProvider:
|
||||
def __init__(self):
|
||||
self.stream = None
|
||||
|
||||
async def __call__(self, offer: StreamingOffer):
|
||||
assert self.stream is None, "This may only be called once"
|
||||
|
||||
info = parse_info_from_offer(offer.sdp)
|
||||
|
||||
builder = WebRTCAnswerBuilder(offer.sdp, ice_servers=[])
|
||||
for cam in offer.video:
|
||||
builder.add_video_stream(cam, VideoStreamTrack())
|
||||
if info.expected_audio_track:
|
||||
builder.add_audio_stream(AudioStreamTrack())
|
||||
if info.incoming_audio_track:
|
||||
builder.offer_to_receive_audio_stream()
|
||||
|
||||
self.stream = builder.stream()
|
||||
answer = await self.stream.start()
|
||||
|
||||
return answer
|
||||
|
||||
|
||||
class TestStreamIntegration:
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("name,cameras,recv_audio,add_messaging", [
|
||||
("multi_camera", ["driver", "wideRoad", "road"], False, False),
|
||||
("camera_and_audio", ["driver"], True, False),
|
||||
("camera_and__messaging", ["driver"], False, True),
|
||||
("camera_and_audio_and_messaging", ["driver", "wideRoad", "road"], True, True),
|
||||
])
|
||||
async def test_multi_camera(self, name, cameras, recv_audio, add_messaging):
|
||||
simple_answerer = SimpleAnswerProvider()
|
||||
offer_builder = WebRTCOfferBuilder(simple_answerer, ice_servers=[])
|
||||
for cam in cameras:
|
||||
offer_builder.offer_to_receive_video_stream(cam)
|
||||
if recv_audio:
|
||||
offer_builder.offer_to_receive_audio_stream()
|
||||
if add_messaging:
|
||||
offer_builder.add_messaging()
|
||||
stream = offer_builder.stream()
|
||||
|
||||
try:
|
||||
await stream.start()
|
||||
assert stream.is_started
|
||||
|
||||
try:
|
||||
async with timeout(2):
|
||||
await stream.wait_for_connection()
|
||||
except TimeoutError:
|
||||
pytest.fail("Timed out waiting for connection")
|
||||
assert stream.is_connected_and_ready
|
||||
|
||||
assert stream.has_messaging_channel() == add_messaging
|
||||
if stream.has_messaging_channel():
|
||||
channel = stream.get_messaging_channel()
|
||||
assert channel is not None
|
||||
assert channel.readyState == "open"
|
||||
|
||||
assert stream.has_incoming_audio_track() == recv_audio
|
||||
if stream.has_incoming_audio_track():
|
||||
track = stream.get_incoming_audio_track(False)
|
||||
assert track is not None
|
||||
assert track.readyState == "live"
|
||||
assert track.kind == "audio"
|
||||
try:
|
||||
async with timeout(1):
|
||||
await track.recv()
|
||||
except TimeoutError:
|
||||
pytest.fail("Timed out waiting for audio frame")
|
||||
|
||||
for cam in cameras:
|
||||
assert stream.has_incoming_video_track(cam)
|
||||
if stream.has_incoming_video_track(cam):
|
||||
track = stream.get_incoming_video_track(cam, False)
|
||||
assert track is not None
|
||||
assert track.readyState == "live"
|
||||
assert track.kind == "video"
|
||||
try:
|
||||
async with timeout(1):
|
||||
await track.recv()
|
||||
except TimeoutError:
|
||||
pytest.fail("Timed out waiting for video frame")
|
||||
finally:
|
||||
await stream.stop()
|
||||
if simple_answerer.stream is not None:
|
||||
await simple_answerer.stream.stop()
|
||||
assert not stream.is_started
|
||||
assert not stream.is_connected_and_ready
|
||||
@@ -1,167 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import pytest
|
||||
|
||||
import aiortc
|
||||
from aiortc.mediastreams import AudioStreamTrack
|
||||
|
||||
from teleoprtc.builder import WebRTCOfferBuilder, WebRTCAnswerBuilder
|
||||
from teleoprtc.info import parse_info_from_offer
|
||||
from teleoprtc.stream import _rtc_configuration_from_ice_servers
|
||||
from teleoprtc.tracks import TiciVideoStreamTrack
|
||||
|
||||
|
||||
class OfferCapture:
|
||||
def __init__(self):
|
||||
self.offer = None
|
||||
|
||||
async def __call__(self, offer):
|
||||
self.offer = offer
|
||||
raise Exception("Offer captured")
|
||||
|
||||
|
||||
class DummyH264VideoStreamTrack(TiciVideoStreamTrack):
|
||||
kind = "video"
|
||||
|
||||
async def recv(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
def codec_preference(self):
|
||||
return "H264"
|
||||
|
||||
|
||||
def test_explicit_empty_ice_servers_disable_defaults():
|
||||
assert _rtc_configuration_from_ice_servers(None) is None
|
||||
assert _rtc_configuration_from_ice_servers([]).iceServers == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestOfferStream:
|
||||
async def test_offer_stream_sdp_recvonly_audio(self):
|
||||
capture = OfferCapture()
|
||||
builder = WebRTCOfferBuilder(capture, ice_servers=[])
|
||||
builder.offer_to_receive_audio_stream()
|
||||
stream = builder.stream()
|
||||
|
||||
try:
|
||||
with pytest.raises(Exception, match="Offer captured"):
|
||||
await stream.start()
|
||||
finally:
|
||||
await stream.stop()
|
||||
|
||||
info = parse_info_from_offer(capture.offer.sdp)
|
||||
assert info.expected_audio_track
|
||||
assert not info.incoming_audio_track
|
||||
|
||||
async def test_offer_stream_sdp_sendonly_audio(self):
|
||||
capture = OfferCapture()
|
||||
builder = WebRTCOfferBuilder(capture, ice_servers=[])
|
||||
builder.add_audio_stream(AudioStreamTrack())
|
||||
stream = builder.stream()
|
||||
|
||||
try:
|
||||
with pytest.raises(Exception, match="Offer captured"):
|
||||
await stream.start()
|
||||
finally:
|
||||
await stream.stop()
|
||||
|
||||
info = parse_info_from_offer(capture.offer.sdp)
|
||||
assert not info.expected_audio_track
|
||||
assert info.incoming_audio_track
|
||||
|
||||
async def test_offer_stream_sdp_channel(self):
|
||||
capture = OfferCapture()
|
||||
builder = WebRTCOfferBuilder(capture, ice_servers=[])
|
||||
builder.add_messaging()
|
||||
stream = builder.stream()
|
||||
|
||||
try:
|
||||
with pytest.raises(Exception, match="Offer captured"):
|
||||
await stream.start()
|
||||
finally:
|
||||
await stream.stop()
|
||||
|
||||
info = parse_info_from_offer(capture.offer.sdp)
|
||||
assert info.incoming_datachannel
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestAnswerStream:
|
||||
async def test_codec_preference(self):
|
||||
offer_sdp = """v=0
|
||||
o=- 3910274679 3910274679 IN IP4 0.0.0.0
|
||||
s=-
|
||||
t=0 0
|
||||
a=group:BUNDLE 0
|
||||
a=msid-semantic:WMS *
|
||||
m=video 1337 UDP/TLS/RTP/SAVPF 97 98 99 100 101 102
|
||||
c=IN IP4 0.0.0.0
|
||||
a=recvonly
|
||||
a=mid:0
|
||||
a=msid:34803878-98f8-4245-b45c-f773e5f926df 881dbc20-356a-499c-b4e8-695303bb901d
|
||||
a=rtcp:9 IN IP4 0.0.0.0
|
||||
a=rtcp-mux
|
||||
a=ssrc-group:FID 1303546896 3784011659
|
||||
a=ssrc:1303546896 cname:a59185ac-c115-48d3-b39b-db7d615a6966
|
||||
a=ssrc:3784011659 cname:a59185ac-c115-48d3-b39b-db7d615a6966
|
||||
a=rtpmap:97 VP8/90000
|
||||
a=rtcp-fb:97 nack
|
||||
a=rtcp-fb:97 nack pli
|
||||
a=rtcp-fb:97 goog-remb
|
||||
a=rtpmap:99 H264/90000
|
||||
a=rtcp-fb:99 nack
|
||||
a=rtcp-fb:99 nack pli
|
||||
a=rtcp-fb:99 goog-remb
|
||||
a=fmtp:99 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f
|
||||
a=ice-ufrag:1234
|
||||
a=ice-pwd:1234
|
||||
a=fingerprint:sha-256 15:F3:F0:23:67:44:EE:2C:AA:8C:D9:50:95:26:42:7C:67:EA:1F:D2:92:C5:97:01:7B:2E:57:C9:A3:13:00:4A
|
||||
a=setup:actpass"""
|
||||
|
||||
builder = WebRTCAnswerBuilder(offer_sdp, ice_servers=[])
|
||||
builder.add_video_stream("road", DummyH264VideoStreamTrack("road", 0.05))
|
||||
stream = builder.stream()
|
||||
try:
|
||||
answer = await stream.start()
|
||||
sdp_desc = aiortc.sdp.SessionDescription.parse(answer.sdp)
|
||||
video_desc = [m for m in sdp_desc.media if m.kind == "video"][0]
|
||||
codecs = video_desc.rtp.codecs
|
||||
assert codecs[0].mimeType == "video/H264"
|
||||
finally:
|
||||
await stream.stop()
|
||||
|
||||
async def test_fail_if_preferred_codec_not_in_offer(self):
|
||||
offer_sdp = """v=0
|
||||
o=- 3910274679 3910274679 IN IP4 0.0.0.0
|
||||
s=-
|
||||
t=0 0
|
||||
a=group:BUNDLE 0
|
||||
a=msid-semantic:WMS *
|
||||
m=video 1337 UDP/TLS/RTP/SAVPF 97 98 99 100 101 102
|
||||
c=IN IP4 0.0.0.0
|
||||
a=recvonly
|
||||
a=mid:0
|
||||
a=msid:34803878-98f8-4245-b45c-f773e5f926df 881dbc20-356a-499c-b4e8-695303bb901d
|
||||
a=rtcp:9 IN IP4 0.0.0.0
|
||||
a=rtcp-mux
|
||||
a=ssrc-group:FID 1303546896 3784011659
|
||||
a=ssrc:1303546896 cname:a59185ac-c115-48d3-b39b-db7d615a6966
|
||||
a=ssrc:3784011659 cname:a59185ac-c115-48d3-b39b-db7d615a6966
|
||||
a=rtpmap:97 VP8/90000
|
||||
a=rtcp-fb:97 nack
|
||||
a=rtcp-fb:97 nack pli
|
||||
a=rtcp-fb:97 goog-remb
|
||||
a=ice-ufrag:1234
|
||||
a=ice-pwd:1234
|
||||
a=fingerprint:sha-256 15:F3:F0:23:67:44:EE:2C:AA:8C:D9:50:95:26:42:7C:67:EA:1F:D2:92:C5:97:01:7B:2E:57:C9:A3:13:00:4A
|
||||
a=setup:actpass"""
|
||||
|
||||
builder = WebRTCAnswerBuilder(offer_sdp, ice_servers=[])
|
||||
builder.add_video_stream("road", DummyH264VideoStreamTrack("road", 0.05))
|
||||
stream = builder.stream()
|
||||
|
||||
try:
|
||||
with pytest.raises(ValueError):
|
||||
await stream.start()
|
||||
finally:
|
||||
await stream.stop()
|
||||
@@ -1,34 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import pytest
|
||||
|
||||
import aiortc
|
||||
|
||||
from teleoprtc.tracks import video_track_id, parse_video_track_id, TiciVideoStreamTrack, TiciTrackWrapper
|
||||
|
||||
|
||||
class TestTracks:
|
||||
def test_track_id(self):
|
||||
expected_camera_type, expected_track_id = "driver", "test"
|
||||
track_id = video_track_id(expected_camera_type, expected_track_id)
|
||||
camera_type, track_id = parse_video_track_id(track_id)
|
||||
assert expected_camera_type == camera_type
|
||||
assert expected_track_id == track_id
|
||||
|
||||
def test_track_id_invalid(self):
|
||||
with pytest.raises(ValueError):
|
||||
parse_video_track_id("test")
|
||||
|
||||
def test_tici_track_id(self):
|
||||
class VideoStream(TiciVideoStreamTrack):
|
||||
async def recv(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
track = VideoStream("driver", 0.1)
|
||||
camera_type, _ = parse_video_track_id(track.id)
|
||||
assert "driver" == camera_type
|
||||
|
||||
def test_tici_wrapper_id(self):
|
||||
track = TiciTrackWrapper("driver", aiortc.mediastreams.VideoStreamTrack())
|
||||
camera_type, _ = parse_video_track_id(track.id)
|
||||
assert "driver" == camera_type
|
||||
@@ -35,59 +35,84 @@ sync_python_env() {
|
||||
fi
|
||||
|
||||
PYTHONPATH="$DIR" /usr/local/venv/bin/python3 -c "from iqpilot.common.git_creds import install_credential_helper; install_credential_helper('$DIR')" 2>/dev/null || true
|
||||
PACKAGE_LOCK_SHA="$(sha256sum "$DIR/uv.lock" | awk '{print $1}')"
|
||||
PACKAGE_SOURCES_ID="$(git -C "$DIR" rev-parse --verify --quiet "HEAD:artifacts/package_sources" 2>/dev/null || true)"
|
||||
PACKAGE_LOCK_SHA="$(printf '%s\n%s\n' "$(sha256sum "$DIR/uv.lock" | awk '{print $1}')" "$PACKAGE_SOURCES_ID" | sha256sum | awk '{print $1}')"
|
||||
INSTALLED_PACKAGE_LOCK_SHA="$(cat "$DIR/.iqpilot-package-lock-sha256" 2>/dev/null || true)"
|
||||
mapfile -t RUNTIME_WHEELS < <(/usr/local/venv/bin/python3 "$DIR/iqpilot/system/runtime_wheel_requirements.py" "$DIR")
|
||||
mapfile -t RUNTIME_WHEEL_SOURCES < <(/usr/local/venv/bin/python3 "$DIR/iqpilot/system/runtime_wheel_requirements.py" "$DIR" --sources)
|
||||
RUNTIME_WHEEL_LOCK_SHA="$(printf '%s\n' "${RUNTIME_WHEELS[@]}" | sha256sum | awk '{print $1}')"
|
||||
INSTALLED_RUNTIME_WHEEL_LOCK_SHA="$(cat "$DIR/.iqpilot-runtime-wheel-lock-sha256" 2>/dev/null || true)"
|
||||
BASE_SITE_PACKAGES="$(/usr/local/venv/bin/python3 -c 'import site; print(site.getsitepackages()[0])')"
|
||||
PROJECT_RAYLIB="$("$DIR/.venv/bin/python3" -c 'import importlib.metadata; print(importlib.metadata.distribution("raylib").locate_file(""))' 2>/dev/null || true)"
|
||||
if [[ "$PROJECT_RAYLIB" = "$DIR/.venv"/* ]]; then
|
||||
sudo rm -rf "$DIR/.venv"
|
||||
rm -f "$DIR/.iqpilot-package-lock-sha256"
|
||||
rm -f "$DIR/.iqpilot-package-lock-sha256" "$DIR/.iqpilot-runtime-wheel-lock-sha256"
|
||||
INSTALLED_PACKAGE_LOCK_SHA=""
|
||||
INSTALLED_RUNTIME_WHEEL_LOCK_SHA=""
|
||||
fi
|
||||
VENV_SITE_PACKAGES="$("$DIR/.venv/bin/python3" -c 'import site; print(site.getsitepackages()[0])' 2>/dev/null || true)"
|
||||
PACKAGES_READY=0
|
||||
if [ -d "$DIR/artifacts/package_runtime" ] && PYTHONPATH="$DIR/artifacts/package_runtime" /usr/local/venv/bin/python3 -c "import iqdbc, msgq, panda, teleoprtc, tinygrad" 2>/dev/null; then
|
||||
if [ -d "$DIR/artifacts/package_runtime" ] && PYTHONPATH="$DIR/artifacts/package_runtime" /usr/local/venv/bin/python3 -c "import iqdbc, msgq, panda, tinygrad" 2>/dev/null; then
|
||||
PACKAGES_READY=1
|
||||
elif [ "$PACKAGE_LOCK_SHA" = "$INSTALLED_PACKAGE_LOCK_SHA" ] && "$DIR/.venv/bin/python3" -c "import iqdbc, msgq, panda, teleoprtc, tinygrad" 2>/dev/null \
|
||||
elif [ "$PACKAGE_LOCK_SHA" = "$INSTALLED_PACKAGE_LOCK_SHA" ] && "$DIR/.venv/bin/python3" -c "import iqdbc, msgq, panda, tinygrad" 2>/dev/null \
|
||||
&& "$DIR/.venv/bin/python3" "$DIR/iqpilot/system/runtime_packages_verify.py"; then
|
||||
# a top-level import passes on a partially extracted install (lazy backends),
|
||||
# so readiness also requires every wheel RECORD file to exist on disk
|
||||
PACKAGES_READY=1
|
||||
fi
|
||||
if [ "$PACKAGES_READY" != "1" ]; then
|
||||
RUNTIME_WHEELS_READY=0
|
||||
if [ -d "$DIR/artifacts/package_runtime" ] && PYTHONPATH="$DIR/artifacts/package_runtime" /usr/local/venv/bin/python3 -c "import libdatachannel" 2>/dev/null; then
|
||||
RUNTIME_WHEELS_READY=1
|
||||
elif [ "$RUNTIME_WHEEL_LOCK_SHA" = "$INSTALLED_RUNTIME_WHEEL_LOCK_SHA" ] && "$DIR/.venv/bin/python3" -c "import libdatachannel" 2>/dev/null \
|
||||
&& PYTHONPATH="$DIR" "$DIR/.venv/bin/python3" "$DIR/iqpilot/system/runtime_wheels_verify.py"; then
|
||||
RUNTIME_WHEELS_READY=1
|
||||
fi
|
||||
if [ "$PACKAGES_READY" != "1" ] || [ "$RUNTIME_WHEELS_READY" != "1" ]; then
|
||||
UV_CACHE_DIR="$DIR/.uv-cache"
|
||||
if [ ! -x "$DIR/.venv/bin/python3" ]; then
|
||||
UV_CACHE_DIR="$UV_CACHE_DIR" uv venv --python /usr/local/venv/bin/python3 "$DIR/.venv" || return 1
|
||||
VENV_SITE_PACKAGES="$("$DIR/.venv/bin/python3" -c 'import site; print(site.getsitepackages()[0])')"
|
||||
fi
|
||||
sudo chown -R "$(id -u):$(id -g)" "$DIR/.venv" "$UV_CACHE_DIR" "$DIR/artifacts/package_sources" 2>/dev/null || true
|
||||
IQDBC_PACKAGE_SOURCE=""
|
||||
PACKAGE_SOURCES=()
|
||||
while IFS=$'\t' read -r package_name package_source; do
|
||||
PACKAGE_SOURCES+=("$package_source")
|
||||
if [ "$package_name" = "iqdbc" ]; then
|
||||
IQDBC_PACKAGE_SOURCE="$package_source"
|
||||
fi
|
||||
done < <(/usr/local/venv/bin/python3 "$DIR/iqpilot/system/runtime_package_sources.py" "$DIR")
|
||||
if [ "${#PACKAGE_SOURCES[@]}" = "0" ] || [ -z "$IQDBC_PACKAGE_SOURCE" ]; then
|
||||
if [ "${#RUNTIME_WHEELS[@]}" = "0" ] || [ "${#RUNTIME_WHEEL_SOURCES[@]}" = "0" ]; then
|
||||
return 1
|
||||
fi
|
||||
PACKAGE_BUILD_PYTHONPATH="$BASE_SITE_PACKAGES:$VENV_SITE_PACKAGES"
|
||||
# The base AGNOS venv ships Eigen as a Python package instead of under
|
||||
# /usr/include, so source-package builds need its install directory on the compiler include path.
|
||||
# Resolve it through Python rather than pinning the Python minor version.
|
||||
EIGEN_INCLUDE_ROOT="$(/usr/local/venv/bin/python3 -c \
|
||||
'from pathlib import Path; import eigen; root = Path(eigen.__file__).resolve().parent / "install"; assert (root / "eigen3/Eigen/Dense").is_file(); print(root)' \
|
||||
2>/dev/null || true)"
|
||||
PACKAGE_BUILD_CPATH="${CPATH:-}"
|
||||
if [ -n "$EIGEN_INCLUDE_ROOT" ]; then
|
||||
PACKAGE_BUILD_CPATH="$EIGEN_INCLUDE_ROOT${PACKAGE_BUILD_CPATH:+:$PACKAGE_BUILD_CPATH}"
|
||||
if [ "$PACKAGES_READY" != "1" ]; then
|
||||
IQDBC_PACKAGE_SOURCE=""
|
||||
PACKAGE_SOURCES=()
|
||||
while IFS=$'\t' read -r package_name package_source; do
|
||||
PACKAGE_SOURCES+=("$package_source")
|
||||
if [ "$package_name" = "iqdbc" ]; then
|
||||
IQDBC_PACKAGE_SOURCE="$package_source"
|
||||
fi
|
||||
done < <(/usr/local/venv/bin/python3 "$DIR/iqpilot/system/runtime_package_sources.py" "$DIR")
|
||||
if [ "${#PACKAGE_SOURCES[@]}" = "0" ] || [ -z "$IQDBC_PACKAGE_SOURCE" ]; then
|
||||
return 1
|
||||
fi
|
||||
PACKAGE_BUILD_PYTHONPATH="$BASE_SITE_PACKAGES:$VENV_SITE_PACKAGES"
|
||||
# The base AGNOS venv ships Eigen as a Python package instead of under
|
||||
# /usr/include, so source-package builds need its install directory on the compiler include path.
|
||||
# Resolve it through Python rather than pinning the Python minor version.
|
||||
EIGEN_INCLUDE_ROOT="$(/usr/local/venv/bin/python3 -c \
|
||||
'from pathlib import Path; import eigen; root = Path(eigen.__file__).resolve().parent / "install"; assert (root / "eigen3/Eigen/Dense").is_file(); print(root)' \
|
||||
2>/dev/null || true)"
|
||||
PACKAGE_BUILD_CPATH="${CPATH:-}"
|
||||
if [ -n "$EIGEN_INCLUDE_ROOT" ]; then
|
||||
PACKAGE_BUILD_CPATH="$EIGEN_INCLUDE_ROOT${PACKAGE_BUILD_CPATH:+:$PACKAGE_BUILD_CPATH}"
|
||||
fi
|
||||
UV_CACHE_DIR="$UV_CACHE_DIR" PYTHONPATH="$PACKAGE_BUILD_PYTHONPATH" CPATH="$PACKAGE_BUILD_CPATH" PATH="/usr/local/venv/bin:/usr/bin:$PATH" \
|
||||
uv pip install --python "$DIR/.venv/bin/python" --no-build-isolation --no-deps --reinstall "$IQDBC_PACKAGE_SOURCE" || return 1
|
||||
UV_CACHE_DIR="$UV_CACHE_DIR" PYTHONPATH="$PACKAGE_BUILD_PYTHONPATH" CPATH="$PACKAGE_BUILD_CPATH" PATH="/usr/local/venv/bin:/usr/bin:$PATH" \
|
||||
uv pip install --python "$DIR/.venv/bin/python" --no-build-isolation --no-deps --reinstall "${PACKAGE_SOURCES[@]}" || return 1
|
||||
printf '%s\n' "$PACKAGE_LOCK_SHA" > "$DIR/.iqpilot-package-lock-sha256"
|
||||
fi
|
||||
if [ "$RUNTIME_WHEELS_READY" != "1" ]; then
|
||||
UV_CACHE_DIR="$UV_CACHE_DIR" PATH="/usr/local/venv/bin:/usr/bin:$PATH" \
|
||||
uv pip install --python "$DIR/.venv/bin/python" --no-deps --reinstall "${RUNTIME_WHEEL_SOURCES[@]}" || return 1
|
||||
"$DIR/.venv/bin/python3" -c "import libdatachannel" || return 1
|
||||
PYTHONPATH="$DIR" "$DIR/.venv/bin/python3" "$DIR/iqpilot/system/runtime_wheels_verify.py" || return 1
|
||||
printf '%s\n' "$RUNTIME_WHEEL_LOCK_SHA" > "$DIR/.iqpilot-runtime-wheel-lock-sha256"
|
||||
fi
|
||||
UV_CACHE_DIR="$UV_CACHE_DIR" PYTHONPATH="$PACKAGE_BUILD_PYTHONPATH" CPATH="$PACKAGE_BUILD_CPATH" PATH="/usr/local/venv/bin:/usr/bin:$PATH" \
|
||||
uv pip install --python "$DIR/.venv/bin/python" --no-build-isolation --no-deps --reinstall "$IQDBC_PACKAGE_SOURCE" || return 1
|
||||
UV_CACHE_DIR="$UV_CACHE_DIR" PYTHONPATH="$PACKAGE_BUILD_PYTHONPATH" CPATH="$PACKAGE_BUILD_CPATH" PATH="/usr/local/venv/bin:/usr/bin:$PATH" \
|
||||
uv pip install --python "$DIR/.venv/bin/python" --no-build-isolation --no-deps --reinstall "${PACKAGE_SOURCES[@]}" || return 1
|
||||
printf '%s\n' "$PACKAGE_LOCK_SHA" > "$DIR/.iqpilot-package-lock-sha256"
|
||||
fi
|
||||
if [ -n "$VENV_SITE_PACKAGES" ]; then
|
||||
printf 'import site; site.addsitedir("%s")\n' "$BASE_SITE_PACKAGES" | sudo tee "$VENV_SITE_PACKAGES/iqpilot-system-venv.pth" >/dev/null
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user