termdock 1.4.248 → 1.4.249
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/assets/{App-DA7cQLR7.js → App-ySHLLxfl.js} +35 -35
- package/dist/client/assets/App-ySHLLxfl.js.br +0 -0
- package/dist/client/assets/App-ySHLLxfl.js.gz +0 -0
- package/dist/client/assets/{ChangeWalkthroughPanel-vDiLbIdv.js → ChangeWalkthroughPanel-DrsLVbFw.js} +2 -2
- package/dist/client/assets/ChangeWalkthroughPanel-DrsLVbFw.js.br +0 -0
- package/dist/client/assets/ChangeWalkthroughPanel-DrsLVbFw.js.gz +0 -0
- package/dist/client/assets/{DagPlayground-BvY_KblL.js → DagPlayground-CBUX-s8X.js} +1 -1
- package/dist/client/assets/DagPlayground-CBUX-s8X.js.br +0 -0
- package/dist/client/assets/DagPlayground-CBUX-s8X.js.gz +0 -0
- package/dist/client/assets/{DiffLab-Dra7cEKx.js → DiffLab-DD8m9b0G.js} +1 -1
- package/dist/client/assets/DiffLab-DD8m9b0G.js.br +0 -0
- package/dist/client/assets/DiffLab-DD8m9b0G.js.gz +0 -0
- package/dist/client/assets/{DiffReview-v9_1IS2n.js → DiffReview-BEFjWY01.js} +1 -1
- package/dist/client/assets/DiffReview-BEFjWY01.js.br +0 -0
- package/dist/client/assets/DiffReview-BEFjWY01.js.gz +0 -0
- package/dist/client/assets/{DiffReviewLab-BF8Dada6.js → DiffReviewLab-BS1rFkx9.js} +1 -1
- package/dist/client/assets/DiffReviewLab-BS1rFkx9.js.br +0 -0
- package/dist/client/assets/DiffReviewLab-BS1rFkx9.js.gz +0 -0
- package/dist/client/assets/{DiffViewer-C2s4BXp0.js → DiffViewer-zb8C5YLR.js} +1 -1
- package/dist/client/assets/DiffViewer-zb8C5YLR.js.br +0 -0
- package/dist/client/assets/DiffViewer-zb8C5YLR.js.gz +0 -0
- package/dist/client/assets/{ModelPreview-D9pzW_YD.js → ModelPreview-BdY9A7GQ.js} +1 -1
- package/dist/client/assets/ModelPreview-BdY9A7GQ.js.br +0 -0
- package/dist/client/assets/ModelPreview-BdY9A7GQ.js.gz +0 -0
- package/dist/client/assets/RightSidebar-lV2Ca2-c.js +1678 -0
- package/dist/client/assets/RightSidebar-lV2Ca2-c.js.br +0 -0
- package/dist/client/assets/RightSidebar-lV2Ca2-c.js.gz +0 -0
- package/dist/client/assets/{abnfDiagram-VCTEODGH-DVOxqZDY.js → abnfDiagram-VCTEODGH-ChpMiLwB.js} +1 -1
- package/dist/client/assets/abnfDiagram-VCTEODGH-ChpMiLwB.js.br +0 -0
- package/dist/client/assets/abnfDiagram-VCTEODGH-ChpMiLwB.js.gz +0 -0
- package/dist/client/assets/{app-JK4pM9zv.js → app-DhyrRqRS.js} +11 -11
- package/dist/client/assets/app-DhyrRqRS.js.br +0 -0
- package/dist/client/assets/app-DhyrRqRS.js.gz +0 -0
- package/dist/client/assets/{arc-DY_WK1Ln.js → arc-CcPLW1-W.js} +1 -1
- package/dist/client/assets/arc-CcPLW1-W.js.br +0 -0
- package/dist/client/assets/arc-CcPLW1-W.js.gz +0 -0
- package/dist/client/assets/{architectureDiagram-5GKGNRK7-oyhVtQ5E.js → architectureDiagram-5GKGNRK7-CjWa2b5g.js} +1 -1
- package/dist/client/assets/architectureDiagram-5GKGNRK7-CjWa2b5g.js.br +0 -0
- package/dist/client/assets/{architectureDiagram-5GKGNRK7-oyhVtQ5E.js.gz → architectureDiagram-5GKGNRK7-CjWa2b5g.js.gz} +0 -0
- package/dist/client/assets/{blockDiagram-I7D4REHJ-DTMir8MU.js → blockDiagram-I7D4REHJ-D1AMj3Ey.js} +1 -1
- package/dist/client/assets/blockDiagram-I7D4REHJ-D1AMj3Ey.js.br +0 -0
- package/dist/client/assets/blockDiagram-I7D4REHJ-D1AMj3Ey.js.gz +0 -0
- package/dist/client/assets/{c4Diagram-7LVT6UL2-DpDHYMUW.js → c4Diagram-7LVT6UL2-Bo4f-Dyo.js} +1 -1
- package/dist/client/assets/c4Diagram-7LVT6UL2-Bo4f-Dyo.js.br +0 -0
- package/dist/client/assets/c4Diagram-7LVT6UL2-Bo4f-Dyo.js.gz +0 -0
- package/dist/client/assets/channel-B5npd3sc.js +1 -0
- package/dist/client/assets/{chunk-2Q5K7J3B-CZw9qV1g.js → chunk-2Q5K7J3B-EA_Ksody.js} +1 -1
- package/dist/client/assets/{chunk-5VM5RSS4-7oLZ2vUy.js → chunk-5VM5RSS4-D49nnzNg.js} +1 -1
- package/dist/client/assets/{chunk-F27PBJKO-8wDqj2MV.js → chunk-F27PBJKO-BiHLxDgP.js} +1 -1
- package/dist/client/assets/chunk-F27PBJKO-BiHLxDgP.js.br +0 -0
- package/dist/client/assets/chunk-F27PBJKO-BiHLxDgP.js.gz +0 -0
- package/dist/client/assets/{chunk-IMKFNOWR-BnRNosQc.js → chunk-IMKFNOWR-B_x-RlD-.js} +1 -1
- package/dist/client/assets/chunk-IMKFNOWR-B_x-RlD-.js.br +0 -0
- package/dist/client/assets/chunk-IMKFNOWR-B_x-RlD-.js.gz +0 -0
- package/dist/client/assets/{chunk-JWPE2WC7-DnbB0Lhi.js → chunk-JWPE2WC7-BO2gECxo.js} +1 -1
- package/dist/client/assets/{chunk-POPQ4Y6H-TXREax5x.js → chunk-POPQ4Y6H-CTJ8R53n.js} +1 -1
- package/dist/client/assets/{chunk-SVP7TREG-qPD0Q74z.js → chunk-SVP7TREG-jDMFhXej.js} +1 -1
- package/dist/client/assets/chunk-SVP7TREG-jDMFhXej.js.br +0 -0
- package/dist/client/assets/chunk-SVP7TREG-jDMFhXej.js.gz +0 -0
- package/dist/client/assets/{chunk-TICWLB2K-yzLwt_MH.js → chunk-TICWLB2K-DuOVjlA7.js} +1 -1
- package/dist/client/assets/chunk-TICWLB2K-DuOVjlA7.js.br +0 -0
- package/dist/client/assets/chunk-TICWLB2K-DuOVjlA7.js.gz +0 -0
- package/dist/client/assets/{chunk-XXDRQBXY-Dksq2NzZ.js → chunk-XXDRQBXY-Dg68j1ru.js} +1 -1
- package/dist/client/assets/classDiagram-ZZMXUADV-BxAMJQEG.js +1 -0
- package/dist/client/assets/classDiagram-v2-VYDZK3BY-BxAMJQEG.js +1 -0
- package/dist/client/assets/{cose-bilkent-JH36ORCC-BZyAGO6R.js → cose-bilkent-JH36ORCC-eC0PV-vF.js} +1 -1
- package/dist/client/assets/cose-bilkent-JH36ORCC-eC0PV-vF.js.br +0 -0
- package/dist/client/assets/cose-bilkent-JH36ORCC-eC0PV-vF.js.gz +0 -0
- package/dist/client/assets/{cynefin-OW5HDTMX-CyrkJWSU.js → cynefin-OW5HDTMX-CbwuuKCK.js} +1 -1
- package/dist/client/assets/cynefin-OW5HDTMX-CbwuuKCK.js.br +0 -0
- package/dist/client/assets/cynefin-OW5HDTMX-CbwuuKCK.js.gz +0 -0
- package/dist/client/assets/{cynefinDiagram-5FMLGOSQ-BrqS8ZGX.js → cynefinDiagram-5FMLGOSQ-DiL62xjJ.js} +1 -1
- package/dist/client/assets/cynefinDiagram-5FMLGOSQ-DiL62xjJ.js.br +0 -0
- package/dist/client/assets/cynefinDiagram-5FMLGOSQ-DiL62xjJ.js.gz +0 -0
- package/dist/client/assets/{dagre-GXQ25YYZ-ysmdkSNr.js → dagre-GXQ25YYZ-BoCcQeIF.js} +1 -1
- package/dist/client/assets/dagre-GXQ25YYZ-BoCcQeIF.js.br +0 -0
- package/dist/client/assets/dagre-GXQ25YYZ-BoCcQeIF.js.gz +0 -0
- package/dist/client/assets/{diagram-S7CK7UJ4-CWSuzX1d.js → diagram-S7CK7UJ4-BHxSkDxD.js} +1 -1
- package/dist/client/assets/diagram-S7CK7UJ4-BHxSkDxD.js.br +0 -0
- package/dist/client/assets/diagram-S7CK7UJ4-BHxSkDxD.js.gz +0 -0
- package/dist/client/assets/{diagram-UQ7AKVKN-BMxc5NDd.js → diagram-UQ7AKVKN-D2t82qXI.js} +1 -1
- package/dist/client/assets/diagram-UQ7AKVKN-D2t82qXI.js.br +0 -0
- package/dist/client/assets/diagram-UQ7AKVKN-D2t82qXI.js.gz +0 -0
- package/dist/client/assets/{diagram-VSXAHHWV-Cz5KpnGX.js → diagram-VSXAHHWV-DYQmQwL5.js} +1 -1
- package/dist/client/assets/diagram-VSXAHHWV-DYQmQwL5.js.br +0 -0
- package/dist/client/assets/diagram-VSXAHHWV-DYQmQwL5.js.gz +0 -0
- package/dist/client/assets/{diagram-VX7I27RA-CNk1h20b.js → diagram-VX7I27RA-6SfVR4Z9.js} +1 -1
- package/dist/client/assets/diagram-VX7I27RA-6SfVR4Z9.js.br +0 -0
- package/dist/client/assets/diagram-VX7I27RA-6SfVR4Z9.js.gz +0 -0
- package/dist/client/assets/{diagram-Z3DM3KII-C4lcgl6z.js → diagram-Z3DM3KII-CQiCDGAx.js} +1 -1
- package/dist/client/assets/diagram-Z3DM3KII-CQiCDGAx.js.br +0 -0
- package/dist/client/assets/diagram-Z3DM3KII-CQiCDGAx.js.gz +0 -0
- package/dist/client/assets/{ebnfDiagram-PWID7BFC-Cwo356mF.js → ebnfDiagram-PWID7BFC-C7YMHrC7.js} +1 -1
- package/dist/client/assets/ebnfDiagram-PWID7BFC-C7YMHrC7.js.br +0 -0
- package/dist/client/assets/ebnfDiagram-PWID7BFC-C7YMHrC7.js.gz +0 -0
- package/dist/client/assets/{erDiagram-RLTQ6QDP-B4DEocxu.js → erDiagram-RLTQ6QDP-C1-5SbSU.js} +1 -1
- package/dist/client/assets/erDiagram-RLTQ6QDP-C1-5SbSU.js.br +0 -0
- package/dist/client/assets/erDiagram-RLTQ6QDP-C1-5SbSU.js.gz +0 -0
- package/dist/client/assets/{flowDiagram-HODETNUW-CWP3R7jz.js → flowDiagram-HODETNUW-xNfUeMOl.js} +1 -1
- package/dist/client/assets/flowDiagram-HODETNUW-xNfUeMOl.js.br +0 -0
- package/dist/client/assets/flowDiagram-HODETNUW-xNfUeMOl.js.gz +0 -0
- package/dist/client/assets/{ganttDiagram-EL5Y4UJY-DBdMWa6L.js → ganttDiagram-EL5Y4UJY-CHiEXULk.js} +1 -1
- package/dist/client/assets/ganttDiagram-EL5Y4UJY-CHiEXULk.js.br +0 -0
- package/dist/client/assets/ganttDiagram-EL5Y4UJY-CHiEXULk.js.gz +0 -0
- package/dist/client/assets/{gestureArbiter-FNHriuhM.js → gestureArbiter-Q4qvTxJM.js} +1 -1
- package/dist/client/assets/gestureArbiter-Q4qvTxJM.js.br +0 -0
- package/dist/client/assets/gestureArbiter-Q4qvTxJM.js.gz +0 -0
- package/dist/client/assets/{gitGraphDiagram-WWUBYQGX-B6qG6eRT.js → gitGraphDiagram-WWUBYQGX-DGHj-HHA.js} +1 -1
- package/dist/client/assets/gitGraphDiagram-WWUBYQGX-DGHj-HHA.js.br +0 -0
- package/dist/client/assets/gitGraphDiagram-WWUBYQGX-DGHj-HHA.js.gz +0 -0
- package/dist/client/assets/{index-fjWWhVQp.css → index-CwuSutCd.css} +1 -1
- package/dist/client/assets/index-CwuSutCd.css.br +0 -0
- package/dist/client/assets/{index-fjWWhVQp.css.gz → index-CwuSutCd.css.gz} +0 -0
- package/dist/client/assets/{infoDiagram-27XIBGKW-DStk0sCn.js → infoDiagram-27XIBGKW-CkKjaZnz.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-5VMMS53U-Cz8OXUK9.js → ishikawaDiagram-5VMMS53U-BxwoaCAJ.js} +1 -1
- package/dist/client/assets/ishikawaDiagram-5VMMS53U-BxwoaCAJ.js.br +0 -0
- package/dist/client/assets/ishikawaDiagram-5VMMS53U-BxwoaCAJ.js.gz +0 -0
- package/dist/client/assets/{journeyDiagram-3NMN7TZE-CD4v2_eb.js → journeyDiagram-3NMN7TZE-Co0Qn6y1.js} +1 -1
- package/dist/client/assets/journeyDiagram-3NMN7TZE-Co0Qn6y1.js.br +0 -0
- package/dist/client/assets/journeyDiagram-3NMN7TZE-Co0Qn6y1.js.gz +0 -0
- package/dist/client/assets/{kanban-definition-UXKFOSKX-DwLWqTSY.js → kanban-definition-UXKFOSKX-BSOfc04T.js} +1 -1
- package/dist/client/assets/kanban-definition-UXKFOSKX-BSOfc04T.js.br +0 -0
- package/dist/client/assets/kanban-definition-UXKFOSKX-BSOfc04T.js.gz +0 -0
- package/dist/client/assets/{layout-C1EhlDX0.js → layout-BmHMb94h.js} +1 -1
- package/dist/client/assets/layout-BmHMb94h.js.br +0 -0
- package/dist/client/assets/layout-BmHMb94h.js.gz +0 -0
- package/dist/client/assets/{linear-DQLRPRnN.js → linear-y-Abd9JO.js} +1 -1
- package/dist/client/assets/linear-y-Abd9JO.js.br +0 -0
- package/dist/client/assets/linear-y-Abd9JO.js.gz +0 -0
- package/dist/client/assets/{mermaid.core-BzEGo4OD.js → mermaid.core-B4rzVonW.js} +5 -5
- package/dist/client/assets/mermaid.core-B4rzVonW.js.br +0 -0
- package/dist/client/assets/mermaid.core-B4rzVonW.js.gz +0 -0
- package/dist/client/assets/{mindmap-definition-YA3MSWOX-Dct8OiDY.js → mindmap-definition-YA3MSWOX-DY3bdl53.js} +1 -1
- package/dist/client/assets/mindmap-definition-YA3MSWOX-DY3bdl53.js.br +0 -0
- package/dist/client/assets/mindmap-definition-YA3MSWOX-DY3bdl53.js.gz +0 -0
- package/dist/client/assets/{passwordBootstrap-C7Gy7ATG.js → passwordBootstrap-kiljsfOw.js} +1 -1
- package/dist/client/assets/passwordBootstrap-kiljsfOw.js.br +0 -0
- package/dist/client/assets/passwordBootstrap-kiljsfOw.js.gz +0 -0
- package/dist/client/assets/{pegDiagram-XKGWAZYB-fIABDYj-.js → pegDiagram-XKGWAZYB-XdVcJb90.js} +1 -1
- package/dist/client/assets/pegDiagram-XKGWAZYB-XdVcJb90.js.br +0 -0
- package/dist/client/assets/pegDiagram-XKGWAZYB-XdVcJb90.js.gz +0 -0
- package/dist/client/assets/{pieDiagram-E7YTZNPT-D2EuLDIj.js → pieDiagram-E7YTZNPT-CgAHRPHX.js} +1 -1
- package/dist/client/assets/pieDiagram-E7YTZNPT-CgAHRPHX.js.br +0 -0
- package/dist/client/assets/pieDiagram-E7YTZNPT-CgAHRPHX.js.gz +0 -0
- package/dist/client/assets/{quadrantDiagram-AXDQQJYC-BjF42rwO.js → quadrantDiagram-AXDQQJYC-C-nwguRe.js} +1 -1
- package/dist/client/assets/quadrantDiagram-AXDQQJYC-C-nwguRe.js.br +0 -0
- package/dist/client/assets/quadrantDiagram-AXDQQJYC-C-nwguRe.js.gz +0 -0
- package/dist/client/assets/{railroadDiagram-O6MQD6OU-DjfSTDvw.js → railroadDiagram-O6MQD6OU-TcJFsMp6.js} +1 -1
- package/dist/client/assets/railroadDiagram-O6MQD6OU-TcJFsMp6.js.br +0 -0
- package/dist/client/assets/railroadDiagram-O6MQD6OU-TcJFsMp6.js.gz +0 -0
- package/dist/client/assets/{referenceLongPress-Cz9no5pI.js → referenceLongPress-DcD1aZqh.js} +1 -1
- package/dist/client/assets/referenceLongPress-DcD1aZqh.js.br +0 -0
- package/dist/client/assets/referenceLongPress-DcD1aZqh.js.gz +0 -0
- package/dist/client/assets/{requirementDiagram-BXWQKSXE-BewZ62Ou.js → requirementDiagram-BXWQKSXE-CDAoFFyT.js} +1 -1
- package/dist/client/assets/requirementDiagram-BXWQKSXE-CDAoFFyT.js.br +0 -0
- package/dist/client/assets/requirementDiagram-BXWQKSXE-CDAoFFyT.js.gz +0 -0
- package/dist/client/assets/{sankeyDiagram-P5KCCOFB-BhlchAkm.js → sankeyDiagram-P5KCCOFB-rscTAtOd.js} +1 -1
- package/dist/client/assets/sankeyDiagram-P5KCCOFB-rscTAtOd.js.br +0 -0
- package/dist/client/assets/sankeyDiagram-P5KCCOFB-rscTAtOd.js.gz +0 -0
- package/dist/client/assets/{sequenceDiagram-WJ2MYXX4-CFzratAc.js → sequenceDiagram-WJ2MYXX4-w_pdEULi.js} +1 -1
- package/dist/client/assets/sequenceDiagram-WJ2MYXX4-w_pdEULi.js.br +0 -0
- package/dist/client/assets/sequenceDiagram-WJ2MYXX4-w_pdEULi.js.gz +0 -0
- package/dist/client/assets/{sizeCapture-INFHLROL-DeLB3S5x.js → sizeCapture-INFHLROL-tqubtbTJ.js} +1 -1
- package/dist/client/assets/sizeCapture-INFHLROL-tqubtbTJ.js.br +0 -0
- package/dist/client/assets/sizeCapture-INFHLROL-tqubtbTJ.js.gz +0 -0
- package/dist/client/assets/{sparkles-BHsBDSo5.js → sparkles-BQVhXL1X.js} +1 -1
- package/dist/client/assets/{stateDiagram-D77RDMKH-smIkttxr.js → stateDiagram-D77RDMKH-DFm71XIp.js} +1 -1
- package/dist/client/assets/stateDiagram-D77RDMKH-DFm71XIp.js.br +0 -0
- package/dist/client/assets/stateDiagram-D77RDMKH-DFm71XIp.js.gz +0 -0
- package/dist/client/assets/stateDiagram-v2-MP3YSRHH-aTUiN9r3.js +1 -0
- package/dist/client/assets/{swimlanes-42K2YHIH-CVwGuPcK.js → swimlanes-42K2YHIH-Bg6TJPYj.js} +1 -1
- package/dist/client/assets/swimlanes-42K2YHIH-Bg6TJPYj.js.br +0 -0
- package/dist/client/assets/swimlanes-42K2YHIH-Bg6TJPYj.js.gz +0 -0
- package/dist/client/assets/swimlanesDiagram-VR7AAH4N-Bwf9yeLM.js +8 -0
- package/dist/client/assets/{timeline-definition-24CTP7MA-Dsn_Rxbd.js → timeline-definition-24CTP7MA-CwrKihuj.js} +1 -1
- package/dist/client/assets/timeline-definition-24CTP7MA-CwrKihuj.js.br +0 -0
- package/dist/client/assets/timeline-definition-24CTP7MA-CwrKihuj.js.gz +0 -0
- package/dist/client/assets/{triangle-alert-DexJX8jR.js → triangle-alert-DZyRaNWe.js} +1 -1
- package/dist/client/assets/{vennDiagram-4TSXK5OY-Dtdth9UN.js → vennDiagram-4TSXK5OY-WqUYcege.js} +1 -1
- package/dist/client/assets/vennDiagram-4TSXK5OY-WqUYcege.js.br +0 -0
- package/dist/client/assets/vennDiagram-4TSXK5OY-WqUYcege.js.gz +0 -0
- package/dist/client/assets/{wardleyDiagram-VM6X3IG4-82oGoyOd.js → wardleyDiagram-VM6X3IG4-B7Xfv-yN.js} +1 -1
- package/dist/client/assets/wardleyDiagram-VM6X3IG4-B7Xfv-yN.js.br +0 -0
- package/dist/client/assets/wardleyDiagram-VM6X3IG4-B7Xfv-yN.js.gz +0 -0
- package/dist/client/assets/{xychartDiagram-S5SC5T6Z-Ygu0NbUo.js → xychartDiagram-S5SC5T6Z-BJupQX5U.js} +1 -1
- package/dist/client/assets/{xychartDiagram-S5SC5T6Z-Ygu0NbUo.js.br → xychartDiagram-S5SC5T6Z-BJupQX5U.js.br} +0 -0
- package/dist/client/assets/xychartDiagram-S5SC5T6Z-BJupQX5U.js.gz +0 -0
- package/dist/client/index.html +2 -2
- package/dist/client/index.html.br +0 -0
- package/dist/client/index.html.gz +0 -0
- package/dist/client/sw.js +1 -1
- package/dist/client/sw.js.br +0 -0
- package/dist/client/sw.js.gz +0 -0
- package/dist/server/android/adb.js +154 -0
- package/dist/server/android/scrcpy.js +379 -0
- package/dist/server/entry.js +9 -1
- package/dist/server/federation/runtime.js +29 -4
- package/dist/server/routes/android.js +205 -0
- package/dist/server/routes/terminal.js +13 -1
- package/dist/server/utils/apiAccess.js +3 -1
- package/dist/server/utils/settings.js +47 -0
- package/package.json +1 -1
- package/runtime-manifest.json +3 -3
- package/dist/client/assets/App-DA7cQLR7.js.br +0 -0
- package/dist/client/assets/App-DA7cQLR7.js.gz +0 -0
- package/dist/client/assets/ChangeWalkthroughPanel-vDiLbIdv.js.br +0 -0
- package/dist/client/assets/ChangeWalkthroughPanel-vDiLbIdv.js.gz +0 -0
- package/dist/client/assets/DagPlayground-BvY_KblL.js.br +0 -0
- package/dist/client/assets/DagPlayground-BvY_KblL.js.gz +0 -0
- package/dist/client/assets/DiffLab-Dra7cEKx.js.br +0 -0
- package/dist/client/assets/DiffLab-Dra7cEKx.js.gz +0 -0
- package/dist/client/assets/DiffReview-v9_1IS2n.js.br +0 -0
- package/dist/client/assets/DiffReview-v9_1IS2n.js.gz +0 -0
- package/dist/client/assets/DiffReviewLab-BF8Dada6.js.br +0 -0
- package/dist/client/assets/DiffReviewLab-BF8Dada6.js.gz +0 -0
- package/dist/client/assets/DiffViewer-C2s4BXp0.js.br +0 -0
- package/dist/client/assets/DiffViewer-C2s4BXp0.js.gz +0 -0
- package/dist/client/assets/ModelPreview-D9pzW_YD.js.br +0 -0
- package/dist/client/assets/ModelPreview-D9pzW_YD.js.gz +0 -0
- package/dist/client/assets/RightSidebar-DL8z6vvJ.js +0 -1678
- package/dist/client/assets/RightSidebar-DL8z6vvJ.js.br +0 -0
- package/dist/client/assets/RightSidebar-DL8z6vvJ.js.gz +0 -0
- package/dist/client/assets/abnfDiagram-VCTEODGH-DVOxqZDY.js.br +0 -0
- package/dist/client/assets/abnfDiagram-VCTEODGH-DVOxqZDY.js.gz +0 -0
- package/dist/client/assets/app-JK4pM9zv.js.br +0 -0
- package/dist/client/assets/app-JK4pM9zv.js.gz +0 -0
- package/dist/client/assets/arc-DY_WK1Ln.js.br +0 -0
- package/dist/client/assets/arc-DY_WK1Ln.js.gz +0 -0
- package/dist/client/assets/architectureDiagram-5GKGNRK7-oyhVtQ5E.js.br +0 -0
- package/dist/client/assets/blockDiagram-I7D4REHJ-DTMir8MU.js.br +0 -0
- package/dist/client/assets/blockDiagram-I7D4REHJ-DTMir8MU.js.gz +0 -0
- package/dist/client/assets/c4Diagram-7LVT6UL2-DpDHYMUW.js.br +0 -0
- package/dist/client/assets/c4Diagram-7LVT6UL2-DpDHYMUW.js.gz +0 -0
- package/dist/client/assets/channel-cexw6K2f.js +0 -1
- package/dist/client/assets/chunk-F27PBJKO-8wDqj2MV.js.br +0 -0
- package/dist/client/assets/chunk-F27PBJKO-8wDqj2MV.js.gz +0 -0
- package/dist/client/assets/chunk-IMKFNOWR-BnRNosQc.js.br +0 -0
- package/dist/client/assets/chunk-IMKFNOWR-BnRNosQc.js.gz +0 -0
- package/dist/client/assets/chunk-SVP7TREG-qPD0Q74z.js.br +0 -0
- package/dist/client/assets/chunk-SVP7TREG-qPD0Q74z.js.gz +0 -0
- package/dist/client/assets/chunk-TICWLB2K-yzLwt_MH.js.br +0 -0
- package/dist/client/assets/chunk-TICWLB2K-yzLwt_MH.js.gz +0 -0
- package/dist/client/assets/classDiagram-ZZMXUADV-CpjKakYG.js +0 -1
- package/dist/client/assets/classDiagram-v2-VYDZK3BY-CpjKakYG.js +0 -1
- package/dist/client/assets/cose-bilkent-JH36ORCC-BZyAGO6R.js.br +0 -0
- package/dist/client/assets/cose-bilkent-JH36ORCC-BZyAGO6R.js.gz +0 -0
- package/dist/client/assets/cynefin-OW5HDTMX-CyrkJWSU.js.br +0 -0
- package/dist/client/assets/cynefin-OW5HDTMX-CyrkJWSU.js.gz +0 -0
- package/dist/client/assets/cynefinDiagram-5FMLGOSQ-BrqS8ZGX.js.br +0 -0
- package/dist/client/assets/cynefinDiagram-5FMLGOSQ-BrqS8ZGX.js.gz +0 -0
- package/dist/client/assets/dagre-GXQ25YYZ-ysmdkSNr.js.br +0 -0
- package/dist/client/assets/dagre-GXQ25YYZ-ysmdkSNr.js.gz +0 -0
- package/dist/client/assets/diagram-S7CK7UJ4-CWSuzX1d.js.br +0 -0
- package/dist/client/assets/diagram-S7CK7UJ4-CWSuzX1d.js.gz +0 -0
- package/dist/client/assets/diagram-UQ7AKVKN-BMxc5NDd.js.br +0 -0
- package/dist/client/assets/diagram-UQ7AKVKN-BMxc5NDd.js.gz +0 -0
- package/dist/client/assets/diagram-VSXAHHWV-Cz5KpnGX.js.br +0 -0
- package/dist/client/assets/diagram-VSXAHHWV-Cz5KpnGX.js.gz +0 -0
- package/dist/client/assets/diagram-VX7I27RA-CNk1h20b.js.br +0 -0
- package/dist/client/assets/diagram-VX7I27RA-CNk1h20b.js.gz +0 -0
- package/dist/client/assets/diagram-Z3DM3KII-C4lcgl6z.js.br +0 -0
- package/dist/client/assets/diagram-Z3DM3KII-C4lcgl6z.js.gz +0 -0
- package/dist/client/assets/ebnfDiagram-PWID7BFC-Cwo356mF.js.br +0 -0
- package/dist/client/assets/ebnfDiagram-PWID7BFC-Cwo356mF.js.gz +0 -0
- package/dist/client/assets/erDiagram-RLTQ6QDP-B4DEocxu.js.br +0 -0
- package/dist/client/assets/erDiagram-RLTQ6QDP-B4DEocxu.js.gz +0 -0
- package/dist/client/assets/flowDiagram-HODETNUW-CWP3R7jz.js.br +0 -0
- package/dist/client/assets/flowDiagram-HODETNUW-CWP3R7jz.js.gz +0 -0
- package/dist/client/assets/ganttDiagram-EL5Y4UJY-DBdMWa6L.js.br +0 -0
- package/dist/client/assets/ganttDiagram-EL5Y4UJY-DBdMWa6L.js.gz +0 -0
- package/dist/client/assets/gestureArbiter-FNHriuhM.js.br +0 -0
- package/dist/client/assets/gestureArbiter-FNHriuhM.js.gz +0 -0
- package/dist/client/assets/gitGraphDiagram-WWUBYQGX-B6qG6eRT.js.br +0 -0
- package/dist/client/assets/gitGraphDiagram-WWUBYQGX-B6qG6eRT.js.gz +0 -0
- package/dist/client/assets/index-fjWWhVQp.css.br +0 -0
- package/dist/client/assets/ishikawaDiagram-5VMMS53U-Cz8OXUK9.js.br +0 -0
- package/dist/client/assets/ishikawaDiagram-5VMMS53U-Cz8OXUK9.js.gz +0 -0
- package/dist/client/assets/journeyDiagram-3NMN7TZE-CD4v2_eb.js.br +0 -0
- package/dist/client/assets/journeyDiagram-3NMN7TZE-CD4v2_eb.js.gz +0 -0
- package/dist/client/assets/kanban-definition-UXKFOSKX-DwLWqTSY.js.br +0 -0
- package/dist/client/assets/kanban-definition-UXKFOSKX-DwLWqTSY.js.gz +0 -0
- package/dist/client/assets/layout-C1EhlDX0.js.br +0 -0
- package/dist/client/assets/layout-C1EhlDX0.js.gz +0 -0
- package/dist/client/assets/linear-DQLRPRnN.js.br +0 -0
- package/dist/client/assets/linear-DQLRPRnN.js.gz +0 -0
- package/dist/client/assets/mermaid.core-BzEGo4OD.js.br +0 -0
- package/dist/client/assets/mermaid.core-BzEGo4OD.js.gz +0 -0
- package/dist/client/assets/mindmap-definition-YA3MSWOX-Dct8OiDY.js.br +0 -0
- package/dist/client/assets/mindmap-definition-YA3MSWOX-Dct8OiDY.js.gz +0 -0
- package/dist/client/assets/passwordBootstrap-C7Gy7ATG.js.br +0 -0
- package/dist/client/assets/passwordBootstrap-C7Gy7ATG.js.gz +0 -0
- package/dist/client/assets/pegDiagram-XKGWAZYB-fIABDYj-.js.br +0 -0
- package/dist/client/assets/pegDiagram-XKGWAZYB-fIABDYj-.js.gz +0 -0
- package/dist/client/assets/pieDiagram-E7YTZNPT-D2EuLDIj.js.br +0 -0
- package/dist/client/assets/pieDiagram-E7YTZNPT-D2EuLDIj.js.gz +0 -0
- package/dist/client/assets/quadrantDiagram-AXDQQJYC-BjF42rwO.js.br +0 -0
- package/dist/client/assets/quadrantDiagram-AXDQQJYC-BjF42rwO.js.gz +0 -0
- package/dist/client/assets/railroadDiagram-O6MQD6OU-DjfSTDvw.js.br +0 -0
- package/dist/client/assets/railroadDiagram-O6MQD6OU-DjfSTDvw.js.gz +0 -0
- package/dist/client/assets/referenceLongPress-Cz9no5pI.js.br +0 -0
- package/dist/client/assets/referenceLongPress-Cz9no5pI.js.gz +0 -0
- package/dist/client/assets/requirementDiagram-BXWQKSXE-BewZ62Ou.js.br +0 -0
- package/dist/client/assets/requirementDiagram-BXWQKSXE-BewZ62Ou.js.gz +0 -0
- package/dist/client/assets/sankeyDiagram-P5KCCOFB-BhlchAkm.js.br +0 -0
- package/dist/client/assets/sankeyDiagram-P5KCCOFB-BhlchAkm.js.gz +0 -0
- package/dist/client/assets/sequenceDiagram-WJ2MYXX4-CFzratAc.js.br +0 -0
- package/dist/client/assets/sequenceDiagram-WJ2MYXX4-CFzratAc.js.gz +0 -0
- package/dist/client/assets/sizeCapture-INFHLROL-DeLB3S5x.js.br +0 -0
- package/dist/client/assets/sizeCapture-INFHLROL-DeLB3S5x.js.gz +0 -0
- package/dist/client/assets/stateDiagram-D77RDMKH-smIkttxr.js.br +0 -0
- package/dist/client/assets/stateDiagram-D77RDMKH-smIkttxr.js.gz +0 -0
- package/dist/client/assets/stateDiagram-v2-MP3YSRHH-BFDLILuN.js +0 -1
- package/dist/client/assets/swimlanes-42K2YHIH-CVwGuPcK.js.br +0 -0
- package/dist/client/assets/swimlanes-42K2YHIH-CVwGuPcK.js.gz +0 -0
- package/dist/client/assets/swimlanesDiagram-VR7AAH4N-B0Dzvkwg.js +0 -8
- package/dist/client/assets/timeline-definition-24CTP7MA-Dsn_Rxbd.js.br +0 -0
- package/dist/client/assets/timeline-definition-24CTP7MA-Dsn_Rxbd.js.gz +0 -0
- package/dist/client/assets/vennDiagram-4TSXK5OY-Dtdth9UN.js.br +0 -0
- package/dist/client/assets/vennDiagram-4TSXK5OY-Dtdth9UN.js.gz +0 -0
- package/dist/client/assets/wardleyDiagram-VM6X3IG4-82oGoyOd.js.br +0 -0
- package/dist/client/assets/wardleyDiagram-VM6X3IG4-82oGoyOd.js.gz +0 -0
- package/dist/client/assets/xychartDiagram-S5SC5T6Z-Ygu0NbUo.js.gz +0 -0
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { existsSync, realpathSync } from 'node:fs';
|
|
3
|
+
import net from 'node:net';
|
|
4
|
+
import { delimiter, dirname, join } from 'node:path';
|
|
5
|
+
import { randomInt } from 'node:crypto';
|
|
6
|
+
import { adbSearchPath, resolveAdbBinary } from './adb.js';
|
|
7
|
+
const DEVICE_NAME_LENGTH = 64;
|
|
8
|
+
const VIDEO_HEADER_LENGTH = 12;
|
|
9
|
+
const FRAME_HEADER_LENGTH = 12;
|
|
10
|
+
const CONNECT_TIMEOUT_MS = 12_000;
|
|
11
|
+
const CODEC_IDS = {
|
|
12
|
+
0x68323634: 'h264',
|
|
13
|
+
0x68323635: 'h265',
|
|
14
|
+
0x00617631: 'av1',
|
|
15
|
+
};
|
|
16
|
+
function scrcpyExecutableCandidates() {
|
|
17
|
+
const override = process.env.TERMDOCK_SCRCPY_BIN;
|
|
18
|
+
const candidates = [];
|
|
19
|
+
if (override)
|
|
20
|
+
candidates.push(override);
|
|
21
|
+
const names = process.platform === 'win32' ? ['scrcpy.exe', 'scrcpy'] : ['scrcpy'];
|
|
22
|
+
// 用平台路径分隔符切分;Windows 盘符里的 ':' 在正斜杠分隔下会出错。
|
|
23
|
+
for (const dir of adbSearchPath().split(delimiter).filter(Boolean)) {
|
|
24
|
+
for (const name of names)
|
|
25
|
+
candidates.push(join(dir, name));
|
|
26
|
+
}
|
|
27
|
+
return candidates;
|
|
28
|
+
}
|
|
29
|
+
function resolveScrcpyBin() {
|
|
30
|
+
for (const candidate of scrcpyExecutableCandidates())
|
|
31
|
+
if (existsSync(candidate))
|
|
32
|
+
return candidate;
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
function serverJarCandidates(bin) {
|
|
36
|
+
const candidates = [process.env.TERMDOCK_SCRCPY_SERVER || ''];
|
|
37
|
+
if (bin !== 'scrcpy') {
|
|
38
|
+
let resolved = bin;
|
|
39
|
+
try {
|
|
40
|
+
resolved = realpathSync(bin);
|
|
41
|
+
}
|
|
42
|
+
catch { /* keep original path */ }
|
|
43
|
+
const dir = dirname(resolved);
|
|
44
|
+
candidates.push(join(dir, 'scrcpy-server'));
|
|
45
|
+
candidates.push(join(dir, '..', 'share', 'scrcpy', 'scrcpy-server'));
|
|
46
|
+
}
|
|
47
|
+
candidates.push('/usr/local/share/scrcpy/scrcpy-server');
|
|
48
|
+
candidates.push('/usr/share/scrcpy/scrcpy-server');
|
|
49
|
+
candidates.push('/opt/homebrew/share/scrcpy/scrcpy-server');
|
|
50
|
+
return candidates.filter(Boolean);
|
|
51
|
+
}
|
|
52
|
+
let cachedBinaries;
|
|
53
|
+
function runCapture(file, args, timeout) {
|
|
54
|
+
return new Promise((resolve, reject) => {
|
|
55
|
+
const child = spawn(file, args, { stdio: ['ignore', 'pipe', 'pipe'], env: { ...process.env, PATH: adbSearchPath() } });
|
|
56
|
+
let out = '', err = '';
|
|
57
|
+
const timer = setTimeout(() => { child.kill('SIGKILL'); reject(new Error('SCRCPY_PROBE_TIMEOUT')); }, timeout);
|
|
58
|
+
child.stdout.on('data', chunk => { out += chunk; });
|
|
59
|
+
child.stderr.on('data', chunk => { err += chunk; });
|
|
60
|
+
child.on('error', error => { clearTimeout(timer); reject(error); });
|
|
61
|
+
child.on('close', code => {
|
|
62
|
+
clearTimeout(timer);
|
|
63
|
+
if (code === 0 || out.trim())
|
|
64
|
+
resolve(out.trim() || err.trim());
|
|
65
|
+
else
|
|
66
|
+
reject(new Error(err.trim() || `SCRCPY_PROBE_FAILED_${code}`));
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
/** 定位 scrcpy 客户端与随包 scrcpy-server,并从 `scrcpy --version` 取协议版本。 */
|
|
71
|
+
export async function resolveScrcpyBinaries() {
|
|
72
|
+
if (cachedBinaries !== undefined) {
|
|
73
|
+
if (!cachedBinaries)
|
|
74
|
+
throw new Error('SCRCPY_NOT_FOUND');
|
|
75
|
+
return cachedBinaries;
|
|
76
|
+
}
|
|
77
|
+
// 找不到绝对路径时退回 PATH 解析(安装包可能把 scrcpy 放在非常规目录)。
|
|
78
|
+
const bin = resolveScrcpyBin() ?? 'scrcpy';
|
|
79
|
+
const serverJar = serverJarCandidates(bin).find(candidate => existsSync(candidate));
|
|
80
|
+
if (!serverJar) {
|
|
81
|
+
cachedBinaries = null;
|
|
82
|
+
throw new Error('SCRCPY_SERVER_NOT_FOUND');
|
|
83
|
+
}
|
|
84
|
+
let version = '';
|
|
85
|
+
try {
|
|
86
|
+
const output = await runCapture(bin, ['--version'], 6000);
|
|
87
|
+
version = /^scrcpy\s+([0-9][^\s]*)/m.exec(output)?.[1] ?? '';
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
version = '';
|
|
91
|
+
}
|
|
92
|
+
if (!version) {
|
|
93
|
+
cachedBinaries = null;
|
|
94
|
+
throw new Error('SCRCPY_VERSION_UNKNOWN');
|
|
95
|
+
}
|
|
96
|
+
cachedBinaries = { bin, serverJar, version };
|
|
97
|
+
return cachedBinaries;
|
|
98
|
+
}
|
|
99
|
+
function remoteJarPath(version) {
|
|
100
|
+
return `/data/local/tmp/termdock-scrcpy-server-${version}.jar`;
|
|
101
|
+
}
|
|
102
|
+
function adbBinary() {
|
|
103
|
+
const binary = resolveAdbBinary();
|
|
104
|
+
if (!binary)
|
|
105
|
+
throw new Error('ADB_NOT_FOUND');
|
|
106
|
+
return binary;
|
|
107
|
+
}
|
|
108
|
+
function runAdbCapture(args, timeout = 30_000) {
|
|
109
|
+
return new Promise((resolve, reject) => {
|
|
110
|
+
const child = spawn(adbBinary(), args, { stdio: ['ignore', 'pipe', 'pipe'], env: { ...process.env, PATH: adbSearchPath() } });
|
|
111
|
+
let out = '', err = '';
|
|
112
|
+
const timer = setTimeout(() => { child.kill('SIGKILL'); reject(new Error('ADB_TIMEOUT')); }, timeout);
|
|
113
|
+
child.on('error', error => { clearTimeout(timer); reject(error); });
|
|
114
|
+
child.stdout.on('data', chunk => { out += chunk; });
|
|
115
|
+
child.stderr.on('data', chunk => { err += chunk; });
|
|
116
|
+
child.on('close', code => {
|
|
117
|
+
clearTimeout(timer);
|
|
118
|
+
if (code === 0)
|
|
119
|
+
resolve(out.trim());
|
|
120
|
+
else
|
|
121
|
+
reject(new Error((err || out).trim() || `ADB_FAILED_${code}`));
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
function scidHex(scid) {
|
|
126
|
+
return scid.toString(16).padStart(8, '0');
|
|
127
|
+
}
|
|
128
|
+
/** 单设备的一条 scrcpy 会话:推送 server → adb reverse → 启动 server → 解析 H.264/H.265 流。 */
|
|
129
|
+
export class ScrcpySession {
|
|
130
|
+
serial;
|
|
131
|
+
events;
|
|
132
|
+
options;
|
|
133
|
+
scid = randomInt(1, 0x7fffffff);
|
|
134
|
+
child = null;
|
|
135
|
+
server = null;
|
|
136
|
+
videoSocket = null;
|
|
137
|
+
controlSocket = null;
|
|
138
|
+
videoBuffer = Buffer.alloc(0);
|
|
139
|
+
controlBuffer = Buffer.alloc(0);
|
|
140
|
+
videoStage = 'meta';
|
|
141
|
+
port = 0;
|
|
142
|
+
stopped = false;
|
|
143
|
+
started = false;
|
|
144
|
+
deviceName = '';
|
|
145
|
+
resolveHeader = null;
|
|
146
|
+
logLines = [];
|
|
147
|
+
constructor(serial, events, options = {}) {
|
|
148
|
+
this.serial = serial;
|
|
149
|
+
this.events = events;
|
|
150
|
+
this.options = options;
|
|
151
|
+
}
|
|
152
|
+
get running() { return this.started && !this.stopped; }
|
|
153
|
+
log(line) {
|
|
154
|
+
const text = line.trim();
|
|
155
|
+
if (!text)
|
|
156
|
+
return;
|
|
157
|
+
this.logLines.push(text);
|
|
158
|
+
if (this.logLines.length > 40)
|
|
159
|
+
this.logLines.shift();
|
|
160
|
+
}
|
|
161
|
+
fail(error) {
|
|
162
|
+
if (this.stopped)
|
|
163
|
+
return;
|
|
164
|
+
this.events.onError(error);
|
|
165
|
+
void this.stop();
|
|
166
|
+
}
|
|
167
|
+
async start() {
|
|
168
|
+
if (this.started)
|
|
169
|
+
return;
|
|
170
|
+
this.started = true;
|
|
171
|
+
try {
|
|
172
|
+
const { serverJar, version } = await resolveScrcpyBinaries();
|
|
173
|
+
const remoteJar = remoteJarPath(version);
|
|
174
|
+
await runAdbCapture(['-s', this.serial, 'push', serverJar, remoteJar], 60_000);
|
|
175
|
+
const server = net.createServer();
|
|
176
|
+
this.server = server;
|
|
177
|
+
server.on('connection', socket => this.acceptSocket(socket));
|
|
178
|
+
await new Promise((resolve, reject) => {
|
|
179
|
+
server.once('error', reject);
|
|
180
|
+
server.listen(0, '127.0.0.1', () => { this.port = server.address().port; resolve(); });
|
|
181
|
+
});
|
|
182
|
+
await runAdbCapture(['-s', this.serial, 'reverse', `localabstract:scrcpy_${scidHex(this.scid)}`, `tcp:${this.port}`], 15_000);
|
|
183
|
+
const args = [
|
|
184
|
+
'-s', this.serial, 'shell',
|
|
185
|
+
`CLASSPATH=${remoteJar}`,
|
|
186
|
+
'app_process', '/',
|
|
187
|
+
'com.genymobile.scrcpy.Server', version,
|
|
188
|
+
`scid=${this.scid.toString(16)}`,
|
|
189
|
+
'log_level=info',
|
|
190
|
+
'audio=false',
|
|
191
|
+
'control=true',
|
|
192
|
+
'tunnel_forward=false',
|
|
193
|
+
'power_on=true',
|
|
194
|
+
'stay_awake=true',
|
|
195
|
+
'clipboard_autosync=false',
|
|
196
|
+
`max_size=${this.options.maxSize ?? 1600}`,
|
|
197
|
+
`video_bit_rate=${this.options.videoBitRate ?? 8_000_000}`,
|
|
198
|
+
];
|
|
199
|
+
if (this.options.maxFps && this.options.maxFps > 0)
|
|
200
|
+
args.push(`max_fps=${this.options.maxFps}`);
|
|
201
|
+
const child = spawn(adbBinary(), args, { stdio: ['ignore', 'pipe', 'pipe'], env: { ...process.env, PATH: adbSearchPath() } });
|
|
202
|
+
this.child = child;
|
|
203
|
+
child.stdout?.on('data', chunk => this.log(String(chunk).replace(/^\[server\]\s*/, '')));
|
|
204
|
+
child.stderr?.on('data', chunk => this.log(String(chunk)));
|
|
205
|
+
child.on('error', error => this.fail(error instanceof Error ? error : new Error('SCRCPY_SPAWN_FAILED')));
|
|
206
|
+
child.on('close', () => { if (!this.stopped)
|
|
207
|
+
this.fail(new Error(this.logLines.at(-1) || 'SCRCPY_SERVER_EXITED')); });
|
|
208
|
+
await this.waitForHeader();
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
const normalized = error instanceof Error ? error : new Error('SCRCPY_START_FAILED');
|
|
212
|
+
if (!this.stopped)
|
|
213
|
+
this.fail(normalized);
|
|
214
|
+
throw normalized;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
waitForHeader() {
|
|
218
|
+
return new Promise((resolve, reject) => {
|
|
219
|
+
const timer = setTimeout(() => { this.resolveHeader = null; reject(new Error('SCRCPY_CONNECT_TIMEOUT')); }, CONNECT_TIMEOUT_MS);
|
|
220
|
+
this.resolveHeader = () => { clearTimeout(timer); this.resolveHeader = null; resolve(); };
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
acceptSocket(socket) {
|
|
224
|
+
socket.setNoDelay(true);
|
|
225
|
+
if (!this.videoSocket) {
|
|
226
|
+
this.videoSocket = socket;
|
|
227
|
+
socket.on('data', chunk => this.readVideo(chunk));
|
|
228
|
+
socket.on('error', error => this.fail(error instanceof Error ? error : new Error('VIDEO_SOCKET_ERROR')));
|
|
229
|
+
socket.on('close', () => { if (!this.stopped)
|
|
230
|
+
this.fail(new Error(this.logLines.at(-1) || 'VIDEO_SOCKET_CLOSED')); });
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
if (!this.controlSocket) {
|
|
234
|
+
this.controlSocket = socket;
|
|
235
|
+
// 控制通道会回传剪贴板/ACK 等设备消息;v1 只消费,避免读缓冲堆积。
|
|
236
|
+
socket.on('data', chunk => this.readControl(chunk));
|
|
237
|
+
socket.on('error', () => { });
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
socket.destroy();
|
|
241
|
+
}
|
|
242
|
+
readVideo(chunk) {
|
|
243
|
+
if (this.stopped)
|
|
244
|
+
return;
|
|
245
|
+
this.videoBuffer = Buffer.concat([this.videoBuffer, chunk]);
|
|
246
|
+
if (this.videoStage === 'meta') {
|
|
247
|
+
if (this.videoBuffer.length < DEVICE_NAME_LENGTH)
|
|
248
|
+
return;
|
|
249
|
+
this.deviceName = this.videoBuffer.subarray(0, DEVICE_NAME_LENGTH).toString('utf8').replace(/\0+$/, '');
|
|
250
|
+
this.videoBuffer = this.videoBuffer.subarray(DEVICE_NAME_LENGTH);
|
|
251
|
+
this.videoStage = 'codec';
|
|
252
|
+
}
|
|
253
|
+
if (this.videoStage === 'codec') {
|
|
254
|
+
if (this.videoBuffer.length < VIDEO_HEADER_LENGTH)
|
|
255
|
+
return;
|
|
256
|
+
const codec = CODEC_IDS[this.videoBuffer.readUInt32BE(0)];
|
|
257
|
+
const width = this.videoBuffer.readUInt32BE(4);
|
|
258
|
+
const height = this.videoBuffer.readUInt32BE(8);
|
|
259
|
+
this.videoBuffer = this.videoBuffer.subarray(VIDEO_HEADER_LENGTH);
|
|
260
|
+
this.videoStage = 'frames';
|
|
261
|
+
if (!codec) {
|
|
262
|
+
this.fail(new Error('SCRCPY_UNSUPPORTED_CODEC'));
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
this.events.onHeader({ codec, width, height, deviceName: this.deviceName || this.serial });
|
|
266
|
+
this.resolveHeader?.();
|
|
267
|
+
}
|
|
268
|
+
while (this.videoStage === 'frames' && this.videoBuffer.length >= FRAME_HEADER_LENGTH) {
|
|
269
|
+
const ptsFlags = this.videoBuffer.readBigUInt64BE(0);
|
|
270
|
+
const size = this.videoBuffer.readUInt32BE(8);
|
|
271
|
+
if (this.videoBuffer.length < FRAME_HEADER_LENGTH + size)
|
|
272
|
+
break;
|
|
273
|
+
const payload = Buffer.from(this.videoBuffer.subarray(FRAME_HEADER_LENGTH, FRAME_HEADER_LENGTH + size));
|
|
274
|
+
this.videoBuffer = this.videoBuffer.subarray(FRAME_HEADER_LENGTH + size);
|
|
275
|
+
const config = ((ptsFlags >> 63n) & 1n) === 1n;
|
|
276
|
+
const keyFrame = ((ptsFlags >> 62n) & 1n) === 1n;
|
|
277
|
+
this.events.onFrame({ config, keyFrame, pts: ptsFlags & ((1n << 62n) - 1n), data: payload });
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
readControl(chunk) {
|
|
281
|
+
this.controlBuffer = Buffer.concat([this.controlBuffer, chunk]);
|
|
282
|
+
while (this.controlBuffer.length > 0) {
|
|
283
|
+
const type = this.controlBuffer[0];
|
|
284
|
+
if (type === 0) {
|
|
285
|
+
if (this.controlBuffer.length < 5)
|
|
286
|
+
return;
|
|
287
|
+
const length = this.controlBuffer.readUInt32BE(1);
|
|
288
|
+
if (this.controlBuffer.length < 5 + length)
|
|
289
|
+
return;
|
|
290
|
+
this.controlBuffer = this.controlBuffer.subarray(5 + length);
|
|
291
|
+
}
|
|
292
|
+
else if (type === 1) {
|
|
293
|
+
if (this.controlBuffer.length < 9)
|
|
294
|
+
return;
|
|
295
|
+
this.controlBuffer = this.controlBuffer.subarray(9);
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
this.controlBuffer = Buffer.alloc(0);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
/** 客户端消费不过来时暂停视频 socket:让内核缓冲填满,把反压传到设备端,
|
|
304
|
+
* 从而在不丢帧的前提下限制延迟(丢 delta 会让参考帧链断裂、画面花屏)。 */
|
|
305
|
+
pauseVideo() {
|
|
306
|
+
if (this.videoPaused)
|
|
307
|
+
return;
|
|
308
|
+
this.videoPaused = true;
|
|
309
|
+
try {
|
|
310
|
+
this.videoSocket?.pause();
|
|
311
|
+
}
|
|
312
|
+
catch { /* ignore */ }
|
|
313
|
+
}
|
|
314
|
+
resumeVideo() {
|
|
315
|
+
if (!this.videoPaused)
|
|
316
|
+
return;
|
|
317
|
+
this.videoPaused = false;
|
|
318
|
+
try {
|
|
319
|
+
this.videoSocket?.resume();
|
|
320
|
+
}
|
|
321
|
+
catch { /* ignore */ }
|
|
322
|
+
}
|
|
323
|
+
videoPaused = false;
|
|
324
|
+
sendControl(data) {
|
|
325
|
+
const socket = this.controlSocket;
|
|
326
|
+
if (!socket || socket.destroyed)
|
|
327
|
+
return false;
|
|
328
|
+
try {
|
|
329
|
+
socket.write(data);
|
|
330
|
+
return true;
|
|
331
|
+
}
|
|
332
|
+
catch {
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
async stop() {
|
|
337
|
+
if (this.stopped)
|
|
338
|
+
return;
|
|
339
|
+
this.stopped = true;
|
|
340
|
+
activeSessions.delete(this);
|
|
341
|
+
const child = this.child;
|
|
342
|
+
this.child = null;
|
|
343
|
+
try {
|
|
344
|
+
this.videoSocket?.destroy();
|
|
345
|
+
}
|
|
346
|
+
catch { /* ignore */ }
|
|
347
|
+
try {
|
|
348
|
+
this.controlSocket?.destroy();
|
|
349
|
+
}
|
|
350
|
+
catch { /* ignore */ }
|
|
351
|
+
this.videoSocket = null;
|
|
352
|
+
this.controlSocket = null;
|
|
353
|
+
try {
|
|
354
|
+
this.server?.close();
|
|
355
|
+
}
|
|
356
|
+
catch { /* ignore */ }
|
|
357
|
+
this.server = null;
|
|
358
|
+
if (child) {
|
|
359
|
+
try {
|
|
360
|
+
child.kill('SIGKILL');
|
|
361
|
+
}
|
|
362
|
+
catch { /* ignore */ }
|
|
363
|
+
}
|
|
364
|
+
try {
|
|
365
|
+
await runAdbCapture(['-s', this.serial, 'reverse', '--remove', `localabstract:scrcpy_${scidHex(this.scid)}`], 8000);
|
|
366
|
+
}
|
|
367
|
+
catch { /* reverse 可能已随进程退出被清理 */ }
|
|
368
|
+
this.events.onClose();
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
const activeSessions = new Set();
|
|
372
|
+
export function createScrcpySession(serial, events, options) {
|
|
373
|
+
const session = new ScrcpySession(serial, events, options);
|
|
374
|
+
activeSessions.add(session);
|
|
375
|
+
return session;
|
|
376
|
+
}
|
|
377
|
+
export async function stopAllScrcpySessions() {
|
|
378
|
+
await Promise.all([...activeSessions].map(session => session.stop()));
|
|
379
|
+
}
|
package/dist/server/entry.js
CHANGED
|
@@ -30,6 +30,8 @@ import cookieParser from 'cookie-parser';
|
|
|
30
30
|
import { fileURLToPath } from 'url';
|
|
31
31
|
import { WebSocketServer } from 'ws';
|
|
32
32
|
import terminalRoutes, { handleTerminalWebSocket, handleControlWebSocket, resolveStableFederationSessionId } from './routes/terminal.js';
|
|
33
|
+
import androidRoutes, { handleAndroidWebSocket } from './routes/android.js';
|
|
34
|
+
import { stopAllScrcpySessions } from './android/scrcpy.js';
|
|
33
35
|
import filesystemRoutes from './routes/filesystem.js';
|
|
34
36
|
import authRoutes from './routes/auth.js';
|
|
35
37
|
import notificationRoutes from './routes/notifications.js';
|
|
@@ -336,6 +338,11 @@ export function createApp(options = {}) {
|
|
|
336
338
|
app.use('/api/terminal', terminalRoutes);
|
|
337
339
|
// 文件系统路由(继承 /api/terminal 上的 auth + CSRF 保护)
|
|
338
340
|
app.use('/api/terminal/fs', filesystemRoutes);
|
|
341
|
+
// 本机 Android 设备桥接(adb/scrcpy)。经加密隧道时按业务 API 处理,
|
|
342
|
+
// 仅全权服务授权可达;投屏 WS 在 federation runtime 中按 full-service 校验。
|
|
343
|
+
app.use('/api/android', requireAuth({ bypass: isTrustedLocalCliRequest }));
|
|
344
|
+
app.use('/api/android', csrfProtection.verifyMiddleware({ bypass: isTrustedLocalCliRequest }));
|
|
345
|
+
app.use('/api/android', androidRoutes);
|
|
339
346
|
if (fs.existsSync(bundledClientIndexPath)) {
|
|
340
347
|
const selectedClientPath = resolveRuntimeClientDist(bundledClientDistPath);
|
|
341
348
|
const clientPath = selectedClientPath === bundledClientDistPath
|
|
@@ -449,8 +456,9 @@ export function startServer(options = {}) {
|
|
|
449
456
|
server.once('close', () => { clearInterval(desktopRoutesTimer); for (const target of dynamicDirectTargets.values())
|
|
450
457
|
target.close(); });
|
|
451
458
|
server.once('close', () => relayRouter.close());
|
|
459
|
+
server.once('close', () => { void stopAllScrcpySessions(); });
|
|
452
460
|
const federation = createFederationRuntime(app, path.join(homedir(), '.termdock', 'federation'), {
|
|
453
|
-
terminal: handleTerminalWebSocket, control: handleControlWebSocket,
|
|
461
|
+
terminal: handleTerminalWebSocket, control: handleControlWebSocket, android: handleAndroidWebSocket,
|
|
454
462
|
}, {
|
|
455
463
|
resolveSessionId: resolveStableFederationSessionId,
|
|
456
464
|
collaborationConnected: (subjectId, rpc) => {
|
|
@@ -252,7 +252,12 @@ export async function createFederationRuntime(app, directory, handlers, options
|
|
|
252
252
|
}
|
|
253
253
|
}
|
|
254
254
|
revokeTimer = setInterval(() => {
|
|
255
|
-
for (const { socket: logical, sessionId } of sockets.values()) {
|
|
255
|
+
for (const { socket: logical, sessionId, android } of sockets.values()) {
|
|
256
|
+
if (android) {
|
|
257
|
+
if (!full(subjectId))
|
|
258
|
+
logical.close(4003, 'Authorization revoked');
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
256
261
|
if (!allowed(subjectId, sessionId ? 'session.view' : 'service.view', sessionId))
|
|
257
262
|
logical.close(4003, 'Authorization revoked');
|
|
258
263
|
}
|
|
@@ -558,14 +563,19 @@ export async function createFederationRuntime(app, directory, handlers, options
|
|
|
558
563
|
throw new Error('API_NOT_ALLOWED');
|
|
559
564
|
const url = new URL(packet.path, 'http://inner');
|
|
560
565
|
const match = /^\/api\/terminal\/([^/%]+)\/ws$/.exec(url.pathname);
|
|
566
|
+
const androidMatch = /^\/api\/android\/([^/%]+)\/ws$/.exec(url.pathname);
|
|
561
567
|
if (match)
|
|
562
568
|
check('session.view', match[1]);
|
|
563
569
|
else if (url.pathname === '/api/control/ws')
|
|
564
570
|
check('service.view');
|
|
571
|
+
else if (androidMatch) {
|
|
572
|
+
if (!full(subjectId))
|
|
573
|
+
throw new Error('API_NOT_ALLOWED');
|
|
574
|
+
}
|
|
565
575
|
else
|
|
566
576
|
throw new Error('API_NOT_ALLOWED');
|
|
567
|
-
const logical = new LogicalSocket(channel, packet.id, () => allowed(subjectId, match ? 'session.view' : 'service.view', match?.[1]));
|
|
568
|
-
sockets.set(packet.id, { socket: logical, sessionId: match?.[1] });
|
|
577
|
+
const logical = new LogicalSocket(channel, packet.id, () => androidMatch ? full(subjectId) : allowed(subjectId, match ? 'session.view' : 'service.view', match?.[1]));
|
|
578
|
+
sockets.set(packet.id, { socket: logical, sessionId: match?.[1], android: Boolean(androidMatch) });
|
|
569
579
|
logical.once('close', () => sockets.delete(packet.id));
|
|
570
580
|
send({ type: 'ws-ready', id: packet.id });
|
|
571
581
|
if (match)
|
|
@@ -574,6 +584,15 @@ export async function createFederationRuntime(app, directory, handlers, options
|
|
|
574
584
|
flowControl: url.searchParams.get('flow') === '2', outputActive: url.searchParams.get('active') !== '0',
|
|
575
585
|
independentTmux: full(subjectId) && url.searchParams.get('transport') === 'tmux-client',
|
|
576
586
|
}, allowed(subjectId, 'session.resize', match[1]) ? readTerminalHandshakeDimensions(url.searchParams) : undefined);
|
|
587
|
+
else if (androidMatch) {
|
|
588
|
+
if (!handlers.android)
|
|
589
|
+
throw new Error('API_NOT_ALLOWED');
|
|
590
|
+
handlers.android(logical, androidMatch[1], randomUUID(), {
|
|
591
|
+
maxSize: Number(url.searchParams.get('max_size')) || undefined,
|
|
592
|
+
bitRate: Number(url.searchParams.get('bit_rate')) || undefined,
|
|
593
|
+
maxFps: Number(url.searchParams.get('max_fps')) || undefined,
|
|
594
|
+
});
|
|
595
|
+
}
|
|
577
596
|
else
|
|
578
597
|
handlers.control(logical, randomUUID());
|
|
579
598
|
}
|
|
@@ -582,7 +601,13 @@ export async function createFederationRuntime(app, directory, handlers, options
|
|
|
582
601
|
if (!item || typeof packet.data !== 'string')
|
|
583
602
|
throw new Error('INVALID_STREAM');
|
|
584
603
|
const data = JSON.parse(packet.data);
|
|
585
|
-
if (item.
|
|
604
|
+
if (item.android) {
|
|
605
|
+
if (!full(subjectId))
|
|
606
|
+
throw new Error('AUTHORIZATION_DENIED');
|
|
607
|
+
if (!['control', 'ack', 'ping'].includes(data?.type))
|
|
608
|
+
throw new Error('ACTION_NOT_ALLOWED');
|
|
609
|
+
}
|
|
610
|
+
else if (item.sessionId) {
|
|
586
611
|
const action = terminalMessageAction(data.type);
|
|
587
612
|
if (!action)
|
|
588
613
|
throw new Error('ACTION_NOT_ALLOWED');
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import { connectDevice, disconnectDevice, listDevices, resolveAdbBinary } from '../android/adb.js';
|
|
3
|
+
import { createScrcpySession, resolveScrcpyBinaries } from '../android/scrcpy.js';
|
|
4
|
+
const router = Router();
|
|
5
|
+
const SERIAL_PATTERN = /^[0-9a-zA-Z_.:\-]{1,128}$/;
|
|
6
|
+
/** 逐帧/逐控件的诊断日志默认关闭,需要时 TERMDOCK_ANDROID_DEBUG=1 打开。 */
|
|
7
|
+
const DEBUG_ANDROID = process.env.TERMDOCK_ANDROID_DEBUG === '1';
|
|
8
|
+
export function isValidAndroidSerial(serial) {
|
|
9
|
+
return SERIAL_PATTERN.test(serial);
|
|
10
|
+
}
|
|
11
|
+
router.get('/devices', async (_req, res) => {
|
|
12
|
+
try {
|
|
13
|
+
const devices = await listDevices();
|
|
14
|
+
let scrcpyVersion = null;
|
|
15
|
+
const adbAvailable = resolveAdbBinary() !== null;
|
|
16
|
+
try {
|
|
17
|
+
scrcpyVersion = (await resolveScrcpyBinaries()).version;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
scrcpyVersion = null;
|
|
21
|
+
}
|
|
22
|
+
res.json({ adbAvailable, scrcpyVersion, devices });
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
res.status(503).json({ error: error instanceof Error ? error.message : 'ADB_UNAVAILABLE', code: 'ADB_UNAVAILABLE' });
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
router.post('/connect', async (req, res) => {
|
|
29
|
+
const address = typeof req.body?.address === 'string' ? req.body.address : '';
|
|
30
|
+
try {
|
|
31
|
+
const result = await connectDevice(address);
|
|
32
|
+
res.json({ ok: true, serial: result.serial, output: result.output });
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
const message = error instanceof Error ? error.message : 'ADB_CONNECT_FAILED';
|
|
36
|
+
res.status(400).json({ error: message, code: message });
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
router.post('/disconnect', async (req, res) => {
|
|
40
|
+
const address = typeof req.body?.address === 'string' ? req.body.address : '';
|
|
41
|
+
try {
|
|
42
|
+
res.json({ ok: true, output: await disconnectDevice(address) });
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
const message = error instanceof Error ? error.message : 'ADB_DISCONNECT_FAILED';
|
|
46
|
+
res.status(400).json({ error: message, code: message });
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
export default router;
|
|
50
|
+
function clampInt(value, min, max, fallback) {
|
|
51
|
+
if (typeof value !== 'number' || !Number.isFinite(value))
|
|
52
|
+
return fallback;
|
|
53
|
+
return Math.max(min, Math.min(max, Math.round(value)));
|
|
54
|
+
}
|
|
55
|
+
/** 加密隧道打开 `/api/android/:serial/ws` 后的桥接:视频帧下发、控制消息回写。 */
|
|
56
|
+
export function handleAndroidWebSocket(socket, serial, _clientId, requested = {}) {
|
|
57
|
+
if (!isValidAndroidSerial(serial)) {
|
|
58
|
+
socket.close(4400, 'INVALID_SERIAL');
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
// 网络差时客户端可主动降码率/分辨率;这里对入参做边界收敛。
|
|
62
|
+
const quality = {
|
|
63
|
+
maxSize: clampInt(requested.maxSize, 360, 2160, 1600),
|
|
64
|
+
videoBitRate: clampInt(requested.bitRate, 300_000, 30_000_000, 8_000_000),
|
|
65
|
+
maxFps: clampInt(requested.maxFps, 0, 60, 0),
|
|
66
|
+
};
|
|
67
|
+
let closed = false;
|
|
68
|
+
let inflight = 0;
|
|
69
|
+
let seq = 0;
|
|
70
|
+
let sent = 0;
|
|
71
|
+
let acked = 0;
|
|
72
|
+
let controlCount = 0;
|
|
73
|
+
let lastAckAt = 0;
|
|
74
|
+
let lastFrameAt = Date.now();
|
|
75
|
+
let videoPaused = false;
|
|
76
|
+
let androidSendSkipLogged = false;
|
|
77
|
+
let session = null;
|
|
78
|
+
// 客户端 ack 丢失时不能让视频永久暂停:超时视为丢包并恢复。
|
|
79
|
+
const flowTimer = setInterval(() => {
|
|
80
|
+
if (videoPaused && Date.now() - Math.max(lastAckAt, lastFrameAt) > 3000) {
|
|
81
|
+
if (DEBUG_ANDROID)
|
|
82
|
+
console.log(`[android] flow resume after ack timeout serial=${serial} inflight=${inflight} sent=${sent} acked=${acked}`);
|
|
83
|
+
videoPaused = false;
|
|
84
|
+
session?.resumeVideo();
|
|
85
|
+
}
|
|
86
|
+
}, 1000);
|
|
87
|
+
const statsTimer = DEBUG_ANDROID ? setInterval(() => {
|
|
88
|
+
if (sent > 0)
|
|
89
|
+
console.log(`[android] stats serial=${serial} sent=${sent} acked=${acked} inflight=${inflight} controls=${controlCount} paused=${videoPaused}`);
|
|
90
|
+
}, 10_000) : null;
|
|
91
|
+
const FRAME_WINDOW = 3;
|
|
92
|
+
const send = (payload) => {
|
|
93
|
+
if (closed || socket.readyState !== 1) {
|
|
94
|
+
if (!androidSendSkipLogged) {
|
|
95
|
+
androidSendSkipLogged = true;
|
|
96
|
+
console.log(`[android] send skipped serial=${serial} closed=${closed} readyState=${socket.readyState} type=${String(payload.type)}`);
|
|
97
|
+
}
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
socket.send(JSON.stringify(payload));
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
console.log(`[android] send failed serial=${serial} type=${String(payload.type)} ${error instanceof Error ? error.message : 'unknown'}`);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
const shutdown = (reason) => {
|
|
108
|
+
if (closed)
|
|
109
|
+
return;
|
|
110
|
+
closed = true;
|
|
111
|
+
clearInterval(flowTimer);
|
|
112
|
+
if (statsTimer)
|
|
113
|
+
clearInterval(statsTimer);
|
|
114
|
+
if (DEBUG_ANDROID)
|
|
115
|
+
console.log(`[android] session end serial=${serial} sent=${sent} acked=${acked} reason=${reason ?? 'ok'}`);
|
|
116
|
+
if (reason)
|
|
117
|
+
send({ type: 'error', message: reason });
|
|
118
|
+
try {
|
|
119
|
+
session?.stop();
|
|
120
|
+
}
|
|
121
|
+
catch { /* ignore */ }
|
|
122
|
+
session = null;
|
|
123
|
+
try {
|
|
124
|
+
socket.close(1000, reason ? 'error' : 'closed');
|
|
125
|
+
}
|
|
126
|
+
catch { /* ignore */ }
|
|
127
|
+
};
|
|
128
|
+
socket.on('close', () => { closed = true; clearInterval(flowTimer); if (statsTimer)
|
|
129
|
+
clearInterval(statsTimer); void session?.stop(); session = null; });
|
|
130
|
+
socket.on('error', () => { closed = true; clearInterval(flowTimer); if (statsTimer)
|
|
131
|
+
clearInterval(statsTimer); void session?.stop(); session = null; });
|
|
132
|
+
session = createScrcpySession(serial, {
|
|
133
|
+
onHeader: header => { send({ type: 'header', ...header }); },
|
|
134
|
+
onFrame: frame => {
|
|
135
|
+
if (closed)
|
|
136
|
+
return;
|
|
137
|
+
// 不丢任何帧:客户端慢时暂停视频 socket,用反压换低延迟。
|
|
138
|
+
inflight++;
|
|
139
|
+
if (DEBUG_ANDROID && (sent < 12 || frame.keyFrame))
|
|
140
|
+
console.log(`[android] send frame #${sent} serial=${serial} config=${frame.config} key=${frame.keyFrame} bytes=${frame.data.length}`);
|
|
141
|
+
sent++;
|
|
142
|
+
lastFrameAt = Date.now();
|
|
143
|
+
send({
|
|
144
|
+
type: 'frame', seq: ++seq, config: frame.config, key: frame.keyFrame,
|
|
145
|
+
pts: frame.pts.toString(), data: frame.data.toString('base64'),
|
|
146
|
+
});
|
|
147
|
+
if (!videoPaused && inflight >= FRAME_WINDOW) {
|
|
148
|
+
videoPaused = true;
|
|
149
|
+
session?.pauseVideo();
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
onError: error => shutdown(error.message || 'SCRCPY_ERROR'),
|
|
153
|
+
onClose: () => { if (!closed) {
|
|
154
|
+
send({ type: 'closed' });
|
|
155
|
+
closed = true;
|
|
156
|
+
try {
|
|
157
|
+
socket.close(1000, 'closed');
|
|
158
|
+
}
|
|
159
|
+
catch { /* ignore */ }
|
|
160
|
+
} },
|
|
161
|
+
}, quality);
|
|
162
|
+
void session.start().catch(error => shutdown(error instanceof Error ? error.message : 'SCRCPY_START_FAILED'));
|
|
163
|
+
socket.on('message', raw => {
|
|
164
|
+
if (closed || !session)
|
|
165
|
+
return;
|
|
166
|
+
let message;
|
|
167
|
+
try {
|
|
168
|
+
message = JSON.parse(typeof raw === 'string' ? raw : raw.toString('utf8'));
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
switch (message.type) {
|
|
174
|
+
case 'control': {
|
|
175
|
+
if (typeof message.data !== 'string' || message.data.length > 64 * 1024)
|
|
176
|
+
return;
|
|
177
|
+
const bytes = Buffer.from(message.data, 'base64');
|
|
178
|
+
const ok = session.sendControl(bytes);
|
|
179
|
+
if (DEBUG_ANDROID && controlCount++ < 8) {
|
|
180
|
+
const detail = bytes[0] === 2 && bytes.length >= 32
|
|
181
|
+
? ` touch action=${bytes[1]} x=${bytes.readUInt32BE(10)} y=${bytes.readUInt32BE(14)} size=${bytes.readUInt16BE(18)}x${bytes.readUInt16BE(20)}`
|
|
182
|
+
: ` type=${bytes[0]}`;
|
|
183
|
+
console.log(`[android] control #${controlCount} serial=${serial} bytes=${bytes.length} written=${ok}${detail}`);
|
|
184
|
+
}
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
case 'ack': {
|
|
188
|
+
if (typeof message.seq === 'number') {
|
|
189
|
+
inflight = Math.max(0, inflight - 1);
|
|
190
|
+
acked++;
|
|
191
|
+
lastAckAt = Date.now();
|
|
192
|
+
if (videoPaused && inflight < FRAME_WINDOW) {
|
|
193
|
+
videoPaused = false;
|
|
194
|
+
session?.resumeVideo();
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
case 'ping':
|
|
200
|
+
send({ type: 'pong' });
|
|
201
|
+
break;
|
|
202
|
+
default: break;
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
}
|