├── docs ├── _static │ └── .keep ├── _templates │ └── .keep ├── source │ ├── modules.rst │ ├── xbox.nano.enum.rst │ ├── xbox.nano.channel.rst │ ├── xbox.nano.manager.rst │ ├── xbox.nano.packer.rst │ ├── xbox.nano.xpacker.rst │ ├── xbox.nano.protocol.rst │ ├── xbox.nano.packet.fec.rst │ ├── xbox.nano.packet.audio.rst │ ├── xbox.nano.packet.input.rst │ ├── xbox.nano.packet.json.rst │ ├── xbox.nano.packet.video.rst │ ├── xbox.nano.render.codec.rst │ ├── xbox.nano.render.sink.rst │ ├── xbox.nano.scripts.pcap.rst │ ├── xbox.nano.factory.audio.rst │ ├── xbox.nano.factory.input.rst │ ├── xbox.nano.factory.video.rst │ ├── xbox.nano.packet.control.rst │ ├── xbox.nano.packet.message.rst │ ├── xbox.nano.scripts.client.rst │ ├── xbox.nano.scripts.replay.rst │ ├── xbox.nano.factory.channel.rst │ ├── xbox.nano.factory.control.rst │ ├── xbox.nano.factory.message.rst │ ├── xbox.nano.render.audio.aac.rst │ ├── xbox.nano.render.audio.sdl.rst │ ├── xbox.nano.render.client.gst.rst │ ├── xbox.nano.render.client.sdl.rst │ ├── xbox.nano.render.input.base.rst │ ├── xbox.nano.render.input.sdl.rst │ ├── xbox.nano.render.video.sdl.rst │ ├── xbox.nano.render.client.base.rst │ ├── xbox.nano.render.client.file.rst │ ├── xbox.nano.scripts.client_mp.rst │ ├── xbox.nano.packet.rst │ ├── xbox.nano.render.video.rst │ ├── xbox.nano.render.audio.rst │ ├── xbox.nano.render.input.rst │ ├── xbox.nano.rst │ ├── xbox.nano.scripts.rst │ ├── xbox.nano.render.client.rst │ ├── xbox.nano.factory.rst │ └── xbox.nano.render.rst ├── index.rst ├── Makefile ├── make.bat └── conf.py ├── xbox └── nano │ ├── packet │ ├── __init__.py │ ├── fec.py │ ├── audio.py │ ├── video.py │ ├── input.py │ ├── control.py │ ├── message.py │ └── json.py │ ├── render │ ├── __init__.py │ ├── audio │ │ ├── __init__.py │ │ ├── aac.py │ │ └── sdl.py │ ├── input │ │ ├── __init__.py │ │ ├── sdl_keyboard.py │ │ ├── sdl.py │ │ └── base.py │ ├── video │ │ ├── __init__.py │ │ └── sdl.py │ ├── client │ │ ├── __init__.py │ │ ├── sdl.py │ │ ├── base.py │ │ ├── file.py │ │ └── gst.py │ ├── sink.py │ └── codec.py │ ├── factory │ ├── __init__.py │ ├── input.py │ ├── audio.py │ ├── control.py │ ├── channel.py │ ├── video.py │ └── message.py │ ├── __init__.py │ ├── scripts │ ├── __init__.py │ ├── client.py │ ├── replay.py │ ├── client_mp.py │ └── pcap.py │ ├── adapters.py │ ├── enum.py │ ├── packer.py │ ├── manager.py │ ├── protocol.py │ ├── channel.py │ └── xpacker.py ├── readthedocs.yml ├── tests ├── data │ ├── json_msg │ │ ├── broadcast_state_unknown │ │ ├── broadcast_previewstatus │ │ ├── broadcast_state_stopped │ │ ├── broadcast_stream_enabled │ │ ├── broadcast_state_init │ │ ├── broadcast_state_started │ │ └── broadcast_start_stream │ └── packets │ │ ├── udp_audio_data │ │ ├── udp_handshake │ │ ├── udp_video_data │ │ ├── udp_input_frame │ │ ├── tcp_audio_control │ │ ├── tcp_channel_close │ │ ├── tcp_channel_create │ │ ├── tcp_video_control │ │ ├── udp_input_frame_ack │ │ ├── tcp_control_handshake │ │ ├── tcp_audio_client_handshake │ │ ├── tcp_audio_server_handshake │ │ ├── tcp_channel_open_no_flags │ │ ├── tcp_input_client_handshake │ │ ├── tcp_input_server_handshake │ │ ├── tcp_video_client_handshake │ │ ├── tcp_video_server_handshake │ │ ├── tcp_channel_open_with_flags │ │ ├── tcp_control_msg_with_header │ │ └── tcp_control_msg_with_header_change_video_quality ├── conftest.py ├── test_json.py └── test_packer.py ├── MANIFEST.in ├── requirements.txt ├── setup.cfg ├── CHANGELOG.md ├── .gitignore ├── LICENSE ├── .github └── workflows │ └── build.yml ├── setup.py ├── Makefile └── README.md /docs/_static/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/_templates/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /xbox/nano/packet/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /xbox/nano/render/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /xbox/nano/render/audio/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /xbox/nano/render/input/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /xbox/nano/render/video/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/source/modules.rst: -------------------------------------------------------------------------------- 1 | xbox 2 | ==== 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | xbox.nano 8 | -------------------------------------------------------------------------------- /readthedocs.yml: -------------------------------------------------------------------------------- 1 | build: 2 | image: latest 3 | 4 | python: 5 | version: 3.8 6 | pip_install: true 7 | -------------------------------------------------------------------------------- /tests/data/json_msg/broadcast_state_unknown: -------------------------------------------------------------------------------- 1 | { 2 | "type": 3, 3 | "state": 0, 4 | "sessionId": "" 5 | } -------------------------------------------------------------------------------- /tests/data/json_msg/broadcast_previewstatus: -------------------------------------------------------------------------------- 1 | { 2 | "type": 7, 3 | "isPublicPreview": false, 4 | "isInternalPreview": false 5 | } -------------------------------------------------------------------------------- /tests/data/packets/udp_audio_data: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenXbox/xbox-smartglass-nano-python/HEAD/tests/data/packets/udp_audio_data -------------------------------------------------------------------------------- /tests/data/packets/udp_handshake: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenXbox/xbox-smartglass-nano-python/HEAD/tests/data/packets/udp_handshake -------------------------------------------------------------------------------- /tests/data/packets/udp_video_data: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenXbox/xbox-smartglass-nano-python/HEAD/tests/data/packets/udp_video_data -------------------------------------------------------------------------------- /tests/data/packets/udp_input_frame: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenXbox/xbox-smartglass-nano-python/HEAD/tests/data/packets/udp_input_frame -------------------------------------------------------------------------------- /tests/data/packets/tcp_audio_control: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenXbox/xbox-smartglass-nano-python/HEAD/tests/data/packets/tcp_audio_control -------------------------------------------------------------------------------- /tests/data/packets/tcp_channel_close: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenXbox/xbox-smartglass-nano-python/HEAD/tests/data/packets/tcp_channel_close -------------------------------------------------------------------------------- /tests/data/packets/tcp_channel_create: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenXbox/xbox-smartglass-nano-python/HEAD/tests/data/packets/tcp_channel_create -------------------------------------------------------------------------------- /tests/data/packets/tcp_video_control: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenXbox/xbox-smartglass-nano-python/HEAD/tests/data/packets/tcp_video_control -------------------------------------------------------------------------------- /tests/data/packets/udp_input_frame_ack: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenXbox/xbox-smartglass-nano-python/HEAD/tests/data/packets/udp_input_frame_ack -------------------------------------------------------------------------------- /tests/data/json_msg/broadcast_state_stopped: -------------------------------------------------------------------------------- 1 | { 2 | "type": 3, 3 | "state": 3, 4 | "sessionId": "{14608F3C-1C4A-4F32-9DA6-179CE1001E4A}" 5 | } -------------------------------------------------------------------------------- /tests/data/packets/tcp_control_handshake: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenXbox/xbox-smartglass-nano-python/HEAD/tests/data/packets/tcp_control_handshake -------------------------------------------------------------------------------- /tests/data/packets/tcp_audio_client_handshake: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenXbox/xbox-smartglass-nano-python/HEAD/tests/data/packets/tcp_audio_client_handshake -------------------------------------------------------------------------------- /tests/data/packets/tcp_audio_server_handshake: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenXbox/xbox-smartglass-nano-python/HEAD/tests/data/packets/tcp_audio_server_handshake -------------------------------------------------------------------------------- /tests/data/packets/tcp_channel_open_no_flags: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenXbox/xbox-smartglass-nano-python/HEAD/tests/data/packets/tcp_channel_open_no_flags -------------------------------------------------------------------------------- /tests/data/packets/tcp_input_client_handshake: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenXbox/xbox-smartglass-nano-python/HEAD/tests/data/packets/tcp_input_client_handshake -------------------------------------------------------------------------------- /tests/data/packets/tcp_input_server_handshake: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenXbox/xbox-smartglass-nano-python/HEAD/tests/data/packets/tcp_input_server_handshake -------------------------------------------------------------------------------- /tests/data/packets/tcp_video_client_handshake: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenXbox/xbox-smartglass-nano-python/HEAD/tests/data/packets/tcp_video_client_handshake -------------------------------------------------------------------------------- /tests/data/packets/tcp_video_server_handshake: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenXbox/xbox-smartglass-nano-python/HEAD/tests/data/packets/tcp_video_server_handshake -------------------------------------------------------------------------------- /tests/data/packets/tcp_channel_open_with_flags: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenXbox/xbox-smartglass-nano-python/HEAD/tests/data/packets/tcp_channel_open_with_flags -------------------------------------------------------------------------------- /tests/data/packets/tcp_control_msg_with_header: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenXbox/xbox-smartglass-nano-python/HEAD/tests/data/packets/tcp_control_msg_with_header -------------------------------------------------------------------------------- /xbox/nano/factory/__init__.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | from xbox.nano.factory import message, channel, video, audio, input, control 3 | from xbox.nano.factory.message import * 4 | -------------------------------------------------------------------------------- /xbox/nano/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Top-level package for xbox-smartglass-nano-python.""" 4 | 5 | __author__ = """OpenXbox""" 6 | __version__ = '0.10.1' 7 | -------------------------------------------------------------------------------- /docs/source/xbox.nano.enum.rst: -------------------------------------------------------------------------------- 1 | xbox.nano.enum module 2 | ===================== 3 | 4 | .. automodule:: xbox.nano.enum 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /tests/data/json_msg/broadcast_stream_enabled: -------------------------------------------------------------------------------- 1 | { 2 | "type": 4, 3 | "enabled": true, 4 | "canBeEnabled": true, 5 | "majorProtocolVersion": 6, 6 | "minorProtocolVersion": 0 7 | } -------------------------------------------------------------------------------- /tests/data/json_msg/broadcast_state_init: -------------------------------------------------------------------------------- 1 | { 2 | "type": 3, 3 | "state": 1, 4 | "sessionId": "{14608F3C-1C4A-4F32-9DA6-179CE1001E4A}", 5 | "udpPort": 49665, 6 | "tcpPort": 53394 7 | } -------------------------------------------------------------------------------- /docs/source/xbox.nano.channel.rst: -------------------------------------------------------------------------------- 1 | xbox.nano.channel module 2 | ======================== 3 | 4 | .. automodule:: xbox.nano.channel 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.nano.manager.rst: -------------------------------------------------------------------------------- 1 | xbox.nano.manager module 2 | ======================== 3 | 4 | .. automodule:: xbox.nano.manager 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.nano.packer.rst: -------------------------------------------------------------------------------- 1 | xbox.nano.packer module 2 | ======================= 3 | 4 | .. automodule:: xbox.nano.packer 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.nano.xpacker.rst: -------------------------------------------------------------------------------- 1 | xbox.nano.xpacker module 2 | ======================== 3 | 4 | .. automodule:: xbox.nano.xpacker 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.nano.protocol.rst: -------------------------------------------------------------------------------- 1 | xbox.nano.protocol module 2 | ========================= 3 | 4 | .. automodule:: xbox.nano.protocol 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /tests/data/packets/tcp_control_msg_with_header_change_video_quality: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenXbox/xbox-smartglass-nano-python/HEAD/tests/data/packets/tcp_control_msg_with_header_change_video_quality -------------------------------------------------------------------------------- /docs/source/xbox.nano.packet.fec.rst: -------------------------------------------------------------------------------- 1 | xbox.nano.packet.fec module 2 | =========================== 3 | 4 | .. automodule:: xbox.nano.packet.fec 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.nano.packet.audio.rst: -------------------------------------------------------------------------------- 1 | xbox.nano.packet.audio module 2 | ============================= 3 | 4 | .. automodule:: xbox.nano.packet.audio 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.nano.packet.input.rst: -------------------------------------------------------------------------------- 1 | xbox.nano.packet.input module 2 | ============================= 3 | 4 | .. automodule:: xbox.nano.packet.input 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.nano.packet.json.rst: -------------------------------------------------------------------------------- 1 | xbox.nano.packet.json module 2 | ============================ 3 | 4 | .. automodule:: xbox.nano.packet.json 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.nano.packet.video.rst: -------------------------------------------------------------------------------- 1 | xbox.nano.packet.video module 2 | ============================= 3 | 4 | .. automodule:: xbox.nano.packet.video 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.nano.render.codec.rst: -------------------------------------------------------------------------------- 1 | xbox.nano.render.codec module 2 | ============================= 3 | 4 | .. automodule:: xbox.nano.render.codec 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.nano.render.sink.rst: -------------------------------------------------------------------------------- 1 | xbox.nano.render.sink module 2 | ============================ 3 | 4 | .. automodule:: xbox.nano.render.sink 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.nano.scripts.pcap.rst: -------------------------------------------------------------------------------- 1 | xbox.nano.scripts.pcap module 2 | ============================= 3 | 4 | .. automodule:: xbox.nano.scripts.pcap 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.nano.factory.audio.rst: -------------------------------------------------------------------------------- 1 | xbox.nano.factory.audio module 2 | ============================== 3 | 4 | .. automodule:: xbox.nano.factory.audio 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.nano.factory.input.rst: -------------------------------------------------------------------------------- 1 | xbox.nano.factory.input module 2 | ============================== 3 | 4 | .. automodule:: xbox.nano.factory.input 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.nano.factory.video.rst: -------------------------------------------------------------------------------- 1 | xbox.nano.factory.video module 2 | ============================== 3 | 4 | .. automodule:: xbox.nano.factory.video 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.nano.packet.control.rst: -------------------------------------------------------------------------------- 1 | xbox.nano.packet.control module 2 | =============================== 3 | 4 | .. automodule:: xbox.nano.packet.control 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.nano.packet.message.rst: -------------------------------------------------------------------------------- 1 | xbox.nano.packet.message module 2 | =============================== 3 | 4 | .. automodule:: xbox.nano.packet.message 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.nano.scripts.client.rst: -------------------------------------------------------------------------------- 1 | xbox.nano.scripts.client module 2 | =============================== 3 | 4 | .. automodule:: xbox.nano.scripts.client 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.nano.scripts.replay.rst: -------------------------------------------------------------------------------- 1 | xbox.nano.scripts.replay module 2 | =============================== 3 | 4 | .. automodule:: xbox.nano.scripts.replay 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.nano.factory.channel.rst: -------------------------------------------------------------------------------- 1 | xbox.nano.factory.channel module 2 | ================================ 3 | 4 | .. automodule:: xbox.nano.factory.channel 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.nano.factory.control.rst: -------------------------------------------------------------------------------- 1 | xbox.nano.factory.control module 2 | ================================ 3 | 4 | .. automodule:: xbox.nano.factory.control 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.nano.factory.message.rst: -------------------------------------------------------------------------------- 1 | xbox.nano.factory.message module 2 | ================================ 3 | 4 | .. automodule:: xbox.nano.factory.message 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.nano.render.audio.aac.rst: -------------------------------------------------------------------------------- 1 | xbox.nano.render.audio.aac module 2 | ================================= 3 | 4 | .. automodule:: xbox.nano.render.audio.aac 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.nano.render.audio.sdl.rst: -------------------------------------------------------------------------------- 1 | xbox.nano.render.audio.sdl module 2 | ================================= 3 | 4 | .. automodule:: xbox.nano.render.audio.sdl 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.nano.render.client.gst.rst: -------------------------------------------------------------------------------- 1 | xbox.nano.render.client.gst module 2 | ================================== 3 | 4 | .. automodule:: xbox.nano.render.client.gst 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.nano.render.client.sdl.rst: -------------------------------------------------------------------------------- 1 | xbox.nano.render.client.sdl module 2 | ================================== 3 | 4 | .. automodule:: xbox.nano.render.client.sdl 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.nano.render.input.base.rst: -------------------------------------------------------------------------------- 1 | xbox.nano.render.input.base module 2 | ================================== 3 | 4 | .. automodule:: xbox.nano.render.input.base 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.nano.render.input.sdl.rst: -------------------------------------------------------------------------------- 1 | xbox.nano.render.input.sdl module 2 | ================================= 3 | 4 | .. automodule:: xbox.nano.render.input.sdl 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.nano.render.video.sdl.rst: -------------------------------------------------------------------------------- 1 | xbox.nano.render.video.sdl module 2 | ================================= 3 | 4 | .. automodule:: xbox.nano.render.video.sdl 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.nano.render.client.base.rst: -------------------------------------------------------------------------------- 1 | xbox.nano.render.client.base module 2 | =================================== 3 | 4 | .. automodule:: xbox.nano.render.client.base 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.nano.render.client.file.rst: -------------------------------------------------------------------------------- 1 | xbox.nano.render.client.file module 2 | =================================== 3 | 4 | .. automodule:: xbox.nano.render.client.file 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/xbox.nano.scripts.client_mp.rst: -------------------------------------------------------------------------------- 1 | xbox.nano.scripts.client\_mp module 2 | =================================== 3 | 4 | .. automodule:: xbox.nano.scripts.client_mp 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /tests/data/json_msg/broadcast_state_started: -------------------------------------------------------------------------------- 1 | { 2 | "type": 3, 3 | "state": 2, 4 | "sessionId": "{14608F3C-1C4A-4F32-9DA6-179CE1001E4A}", 5 | "isWirelessConnection": false, 6 | "wirelessChannel": 0, 7 | "transmitLinkSpeed": 1000000000 8 | } -------------------------------------------------------------------------------- /xbox/nano/render/client/__init__.py: -------------------------------------------------------------------------------- 1 | from xbox.nano.render.client.base import Client 2 | from xbox.nano.render.client.sdl import SDLClient 3 | from xbox.nano.render.client.file import FileClient 4 | 5 | 6 | __all__ = ['Client', 'SDLClient', 'FileClient'] 7 | -------------------------------------------------------------------------------- /xbox/nano/scripts/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | from appdirs import user_data_dir 3 | 4 | 5 | DATA_DIR = user_data_dir('xbox', 'OpenXbox') 6 | TOKENS_FILE = os.path.join(DATA_DIR, 'tokens.json') 7 | if not os.path.exists(DATA_DIR): 8 | os.makedirs(DATA_DIR) 9 | -------------------------------------------------------------------------------- /xbox/nano/render/sink.py: -------------------------------------------------------------------------------- 1 | class Sink(object): 2 | def open(self, client): 3 | pass 4 | 5 | def close(self): 6 | pass 7 | 8 | def setup(self, fmt): 9 | pass 10 | 11 | def render(self, data): 12 | pass 13 | 14 | def pump(self): 15 | pass 16 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include CHANGELOG.md 2 | include LICENSE 3 | include README.md 4 | 5 | include xbox/nano/render/input/controller_db.txt 6 | 7 | recursive-include tests * 8 | recursive-exclude * __pycache__ 9 | recursive-exclude * *.py[co] 10 | 11 | recursive-include docs *.rst conf.py Makefile make.bat *.jpg *.png *.gif 12 | -------------------------------------------------------------------------------- /docs/source/xbox.nano.packet.rst: -------------------------------------------------------------------------------- 1 | xbox.nano.packet package 2 | ======================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | .. toctree:: 8 | 9 | xbox.nano.packet.json 10 | 11 | Module contents 12 | --------------- 13 | 14 | .. automodule:: xbox.nano.packet 15 | :members: 16 | :undoc-members: 17 | :show-inheritance: 18 | -------------------------------------------------------------------------------- /docs/source/xbox.nano.render.video.rst: -------------------------------------------------------------------------------- 1 | xbox.nano.render.video package 2 | ============================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | .. toctree:: 8 | 9 | xbox.nano.render.video.sdl 10 | 11 | Module contents 12 | --------------- 13 | 14 | .. automodule:: xbox.nano.render.video 15 | :members: 16 | :undoc-members: 17 | :show-inheritance: 18 | -------------------------------------------------------------------------------- /docs/source/xbox.nano.render.audio.rst: -------------------------------------------------------------------------------- 1 | xbox.nano.render.audio package 2 | ============================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | .. toctree:: 8 | 9 | xbox.nano.render.audio.aac 10 | xbox.nano.render.audio.sdl 11 | 12 | Module contents 13 | --------------- 14 | 15 | .. automodule:: xbox.nano.render.audio 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | -------------------------------------------------------------------------------- /docs/source/xbox.nano.render.input.rst: -------------------------------------------------------------------------------- 1 | xbox.nano.render.input package 2 | ============================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | .. toctree:: 8 | 9 | xbox.nano.render.input.base 10 | xbox.nano.render.input.sdl 11 | 12 | Module contents 13 | --------------- 14 | 15 | .. automodule:: xbox.nano.render.input 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | -------------------------------------------------------------------------------- /docs/source/xbox.nano.rst: -------------------------------------------------------------------------------- 1 | xbox.nano package 2 | ================= 3 | 4 | Subpackages 5 | ----------- 6 | 7 | .. toctree:: 8 | 9 | xbox.nano.packet 10 | 11 | Submodules 12 | ---------- 13 | 14 | .. toctree:: 15 | 16 | xbox.nano.enum 17 | xbox.nano.manager 18 | 19 | Module contents 20 | --------------- 21 | 22 | .. automodule:: xbox.nano 23 | :members: 24 | :undoc-members: 25 | :show-inheritance: 26 | -------------------------------------------------------------------------------- /docs/source/xbox.nano.scripts.rst: -------------------------------------------------------------------------------- 1 | xbox.nano.scripts package 2 | ========================= 3 | 4 | Submodules 5 | ---------- 6 | 7 | .. toctree:: 8 | 9 | xbox.nano.scripts.client 10 | xbox.nano.scripts.client_mp 11 | xbox.nano.scripts.pcap 12 | xbox.nano.scripts.replay 13 | 14 | Module contents 15 | --------------- 16 | 17 | .. automodule:: xbox.nano.scripts 18 | :members: 19 | :undoc-members: 20 | :show-inheritance: 21 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # Run 2 | xbox-smartglass-core==1.3.0 3 | av==8.0.3 4 | PySDL2==0.9.7 5 | 6 | # Dev 7 | pip==20.2.3 8 | setuptools==50.3.0 9 | bump2version==1.0.1 10 | wheel==0.35.1 11 | watchdog==0.10.3 12 | flake8==3.8.4 13 | coverage==5.3 14 | Sphinx==3.2.1 15 | sphinx_rtd_theme==0.5.0 16 | recommonmark==0.6.0 17 | twine==3.2.0 18 | 19 | pytest==6.1.1 20 | pytest-runner==5.2 21 | pytest-asyncio==0.14.0 22 | pytest-console-scripts==1.0.0 23 | -------------------------------------------------------------------------------- /docs/source/xbox.nano.render.client.rst: -------------------------------------------------------------------------------- 1 | xbox.nano.render.client package 2 | =============================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | .. toctree:: 8 | 9 | xbox.nano.render.client.base 10 | xbox.nano.render.client.file 11 | xbox.nano.render.client.gst 12 | xbox.nano.render.client.sdl 13 | 14 | Module contents 15 | --------------- 16 | 17 | .. automodule:: xbox.nano.render.client 18 | :members: 19 | :undoc-members: 20 | :show-inheritance: 21 | -------------------------------------------------------------------------------- /docs/source/xbox.nano.factory.rst: -------------------------------------------------------------------------------- 1 | xbox.nano.factory package 2 | ========================= 3 | 4 | Submodules 5 | ---------- 6 | 7 | .. toctree:: 8 | 9 | xbox.nano.factory.audio 10 | xbox.nano.factory.channel 11 | xbox.nano.factory.control 12 | xbox.nano.factory.input 13 | xbox.nano.factory.message 14 | xbox.nano.factory.video 15 | 16 | Module contents 17 | --------------- 18 | 19 | .. automodule:: xbox.nano.factory 20 | :members: 21 | :undoc-members: 22 | :show-inheritance: 23 | -------------------------------------------------------------------------------- /docs/source/xbox.nano.render.rst: -------------------------------------------------------------------------------- 1 | xbox.nano.render package 2 | ======================== 3 | 4 | Subpackages 5 | ----------- 6 | 7 | .. toctree:: 8 | 9 | xbox.nano.render.audio 10 | xbox.nano.render.client 11 | xbox.nano.render.input 12 | xbox.nano.render.video 13 | 14 | Submodules 15 | ---------- 16 | 17 | .. toctree:: 18 | 19 | xbox.nano.render.codec 20 | xbox.nano.render.sink 21 | 22 | Module contents 23 | --------------- 24 | 25 | .. automodule:: xbox.nano.render 26 | :members: 27 | :undoc-members: 28 | :show-inheritance: 29 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.10.1 3 | commit = True 4 | tag = True 5 | 6 | [bumpversion:file:setup.py] 7 | search = version="{current_version}" 8 | replace = version="{new_version}" 9 | 10 | [bumpversion:file:xbox/nano/__init__.py] 11 | search = __version__ = '{current_version}' 12 | replace = __version__ = '{new_version}' 13 | 14 | [bumpversion:file:docs/conf.py] 15 | search = release = '{current_version}' 16 | replace = release = '{new_version}' 17 | 18 | [bdist_wheel] 19 | universal = 1 20 | 21 | [flake8] 22 | exclude = docs 23 | 24 | [aliases] 25 | test = pytest 26 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## 0.10.0 (2020-12-12) 4 | 5 | * Change of license -> MIT license 6 | * Deprecated Python 3.6, Added Python 3.9 7 | * Migration from gevent to asyncio (in sync with smartglass-core) 8 | * Migration from marshmallow objects to pydantic 9 | * Fixed controller and closing issues (#15) 10 | 11 | ## 0.9.4 (2020-02-29) 12 | 13 | * Fix KeyError for debug prints 14 | * PyAV 0.4.1 -> 6.1.0 compatibility 15 | 16 | ## 0.9.3 (2018-11-14) 17 | 18 | * Python 3.7 compatibility 19 | 20 | ## 0.9.2 (2018-09-30) 21 | 22 | * Fix TravisCI dependency on ffmpeg 23 | 24 | ## 0.9.1 (2018-09-29) 25 | 26 | * First release on PyPI. 27 | -------------------------------------------------------------------------------- /xbox/nano/render/client/sdl.py: -------------------------------------------------------------------------------- 1 | from xbox.nano.render.client.base import Client 2 | from xbox.nano.render.video.sdl import SDLVideoRenderer 3 | from xbox.nano.render.audio.sdl import SDLAudioRenderer 4 | from xbox.nano.render.input.sdl import SDLInputHandler 5 | from xbox.nano.render.input.sdl_keyboard import SDLKeyboardInputHandler 6 | 7 | 8 | class SDLClient(Client): 9 | def __init__(self, width, height, use_keyboard=False): 10 | super(SDLClient, self).__init__( 11 | SDLVideoRenderer(width, height), 12 | SDLAudioRenderer(), 13 | SDLInputHandler() if not use_keyboard else SDLKeyboardInputHandler(), 14 | ) 15 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. Xbox-Smartglass-Nano documentation master file, created by 2 | sphinx-quickstart on Fri Mar 9 07:54:03 2018. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to Xbox-Smartglass-Nano's documentation! 7 | ================================================ 8 | 9 | .. mdinclude:: ../README.md 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | :caption: Contents: 14 | 15 | source/xbox.nano.manager 16 | source/xbox.nano.protocol 17 | 18 | 19 | 20 | Indices and tables 21 | ================== 22 | 23 | * :ref:`genindex` 24 | * :ref:`modindex` 25 | * :ref:`search` 26 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = Xbox-Smartglass-Nano 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /xbox/nano/factory/input.py: -------------------------------------------------------------------------------- 1 | import construct 2 | from xbox.nano.packet import input 3 | 4 | 5 | def server_handshake(protocol_version, desktop_width, desktop_height, 6 | max_touches, initial_frame_id): 7 | return input.server_handshake( 8 | protocol_version=protocol_version, 9 | desktop_width=desktop_width, 10 | desktop_height=desktop_height, 11 | max_touches=max_touches, 12 | initial_frame_id=initial_frame_id 13 | ) 14 | 15 | 16 | def client_handshake(max_touches, reference_timestamp): 17 | return input.client_handshake( 18 | max_touches=max_touches, 19 | reference_timestamp=reference_timestamp 20 | ) 21 | 22 | 23 | def frame_ack(acked_frame): 24 | return input.frame_ack(acked_frame=acked_frame) 25 | -------------------------------------------------------------------------------- /xbox/nano/adapters.py: -------------------------------------------------------------------------------- 1 | import struct 2 | import construct 3 | from datetime import datetime 4 | 5 | 6 | class ReferenceTimestampAdapter(construct.Adapter): 7 | """ 8 | Construct-Adapter for JSON field. 9 | Parses and dumps JSON. 10 | """ 11 | def __init__(self): 12 | super(self.__class__, self).__init__(construct.Int64ul) 13 | 14 | def _encode(self, obj, context, path): 15 | if not isinstance(obj, datetime): 16 | raise TypeError('Object not of type datetime') 17 | # Timestamp in milliseconds since epoch (uint64 LE) 18 | return int((obj - datetime.utcfromtimestamp(0)).total_seconds() * 1000.0) 19 | 20 | def _decode(self, obj, context, path): 21 | # Convert from millisecond since epoch to datetime (uint64 LE) 22 | return datetime.utcfromtimestamp(obj / 1000) 23 | -------------------------------------------------------------------------------- /xbox/nano/packet/fec.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | from construct import * 3 | from xbox.sg.utils.struct import XStruct 4 | 5 | # Microsoft::Rdp::Dct::MuxDCTChannelFECLayer::AddOutgoingPacket 6 | fec_packet = XStruct( 7 | 'unk1' / Int8ul, 8 | 'unk2' / Int16ul 9 | ) 10 | 11 | 12 | # Microsoft::Rdp::Dct::MuxDCTChannelFECLayerVariableBlockLength::FECCommonHeader::Serialize 13 | fec_common_header = XStruct( 14 | 'unk1' / Int8ul, 15 | # if v2 & 2 16 | 'unk2' / Int8ul, 17 | 'unk3' / Int32ul, 18 | 'unk4' / Int16ul 19 | ) 20 | 21 | # Microsoft::Rdp::Dct::MuxDCTChannelFECLayerVariableBlockLength::FECLayerStatistics::Deserialize 22 | fec_layer_statistics = XStruct( 23 | 'unk1' / Int8ul, 24 | 'unk2' / Int64ul, 25 | 'unk3' / Float32l, 26 | 'unk4' / Int16ul, 27 | 'unk5' / Int16ul, 28 | 'unk6' / Int16ul, 29 | 'unk7' / Int16ul, 30 | 'unk8' / Int16ul, 31 | 'unk9' / Float32l 32 | ) 33 | 34 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | set SPHINXPROJ=Xbox-Smartglass-Nano 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 20 | echo.installed, then set the SPHINXBUILD environment variable to point 21 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 22 | echo.may add the Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /xbox/nano/factory/audio.py: -------------------------------------------------------------------------------- 1 | from construct import Container 2 | from xbox.nano.packet import audio 3 | 4 | 5 | def server_handshake(protocol_version, reference_timestamp, formats): 6 | return audio.server_handshake( 7 | protocol_version=protocol_version, 8 | reference_timestamp=reference_timestamp, 9 | formats=formats 10 | ) 11 | 12 | 13 | def client_handshake(initial_frame_id, requested_format): 14 | return audio.client_handshake( 15 | initial_frame_id=initial_frame_id, 16 | requested_format=requested_format 17 | ) 18 | 19 | 20 | def control(reinitialize=False, start_stream=False, stop_stream=False): 21 | return audio.control( 22 | flags=Container( 23 | reinitialize=reinitialize, 24 | start_stream=start_stream, 25 | stop_stream=stop_stream 26 | ) 27 | ) 28 | 29 | 30 | def data(flags, frame_id, timestamp, data): 31 | return audio.data( 32 | flags=flags, frame_id=frame_id, timestamp=timestamp, data=data 33 | ) 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | .pytest_cache 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | .env/ 13 | venv/ 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *,cover 48 | .hypothesis/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | 57 | # Sphinx documentation 58 | docs/_build/ 59 | 60 | # PyBuilder 61 | target/ 62 | 63 | # Intellij 64 | .idea/ 65 | 66 | # Node 67 | node_modules 68 | 69 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 OpenXbox 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /xbox/nano/render/codec.py: -------------------------------------------------------------------------------- 1 | import av 2 | from xbox.nano.enum import VideoCodec, AudioCodec 3 | 4 | 5 | class FrameDecoder(object): 6 | def __init__(self, codec_name): 7 | self._decoder = av.Codec(codec_name, 'r').create() 8 | 9 | @classmethod 10 | def video(cls, codec_id): 11 | if VideoCodec.H264 == codec_id: 12 | return cls('h264') 13 | elif VideoCodec.YUV == codec_id: 14 | return cls('yuv420p') 15 | elif VideoCodec.RGB == codec_id: 16 | return cls('rgb') 17 | else: 18 | raise Exception('FrameDecoder was supplied invalid VideoCodec') 19 | 20 | @classmethod 21 | def audio(cls, codec_id): 22 | if AudioCodec.AAC == codec_id: 23 | return cls('aac') 24 | elif AudioCodec.Opus == codec_id: 25 | return cls('opus') 26 | elif AudioCodec.PCM == codec_id: 27 | return cls('pcm') 28 | else: 29 | raise Exception('FrameDecoder was supplied invalid AudioCodec') 30 | 31 | def decode(self, data): 32 | packet = av.packet.Packet(data) 33 | return self._decoder.decode(packet) 34 | -------------------------------------------------------------------------------- /xbox/nano/packet/audio.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | from construct import * 3 | from xbox.sg.utils.struct import XStruct 4 | from xbox.sg.utils.adapters import XEnum, PrefixedBytes 5 | from xbox.nano.enum import AudioCodec 6 | from xbox.nano.adapters import ReferenceTimestampAdapter 7 | 8 | fmt = XStruct( 9 | 'channels' / Int32ul, 10 | 'sample_rate' / Int32ul, 11 | 'codec' / XEnum(Int32ul, AudioCodec), 12 | 'pcm' / If(this.codec == AudioCodec.PCM, Struct( 13 | 'bit_depth' / Int32ul, 14 | 'type' / Int32ul # float or integer 15 | )) 16 | ) 17 | 18 | 19 | server_handshake = XStruct( 20 | 'protocol_version' / Int32ul, 21 | 'reference_timestamp' / ReferenceTimestampAdapter(), 22 | 'formats' / PrefixedArray(Int32ul, fmt) 23 | ) 24 | 25 | 26 | client_handshake = XStruct( 27 | 'initial_frame_id' / Int32ul, 28 | 'requested_format' / fmt 29 | ) 30 | 31 | 32 | control = XStruct( 33 | 'flags' / BitStruct( 34 | Padding(1), 35 | 'reinitialize' / Default(Flag, False), 36 | Padding(1), 37 | 'start_stream' / Default(Flag, False), 38 | 'stop_stream' / Default(Flag, False), 39 | Padding(27) 40 | ) 41 | ) 42 | 43 | 44 | data = XStruct( 45 | 'flags' / Int32ul, 46 | 'frame_id' / Int32ul, 47 | 'timestamp' / Int64ul, 48 | 'data' / PrefixedBytes(Int32ul) 49 | ) 50 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pytest 3 | import json 4 | 5 | from xbox.nano.channel import Channel 6 | from xbox.nano.enum import ChannelClass 7 | 8 | 9 | @pytest.fixture(scope='session') 10 | def packets(): 11 | # Who cares about RAM anyway? 12 | data = {} 13 | data_path = os.path.join(os.path.dirname(__file__), 'data', 'packets') 14 | for f in os.listdir(data_path): 15 | with open(os.path.join(data_path, f), 'rb') as fh: 16 | data[f] = fh.read() 17 | 18 | return data 19 | 20 | 21 | @pytest.fixture(scope='session') 22 | def json_messages(): 23 | # Who cares about RAM anyway? 24 | data = {} 25 | data_path = os.path.join(os.path.dirname(__file__), 'data', 'json_msg') 26 | for f in os.listdir(data_path): 27 | with open(os.path.join(data_path, f), 'rt') as fh: 28 | data[f] = json.load(fh) 29 | 30 | return data 31 | 32 | 33 | @pytest.fixture(scope='session') 34 | def channels(): 35 | return { 36 | 0: None, 37 | 1024: Channel(None, None, 1024, ChannelClass.Video, 0), 38 | 1025: Channel(None, None, 1025, ChannelClass.Audio, 0), 39 | 1026: Channel(None, None, 1026, ChannelClass.ChatAudio, 0), 40 | 1027: Channel(None, None, 1027, ChannelClass.Control, 0), 41 | 1028: Channel(None, None, 1028, ChannelClass.Input, 0), 42 | 1029: Channel(None, None, 1029, ChannelClass.InputFeedback, 0) 43 | } 44 | -------------------------------------------------------------------------------- /tests/data/json_msg/broadcast_start_stream: -------------------------------------------------------------------------------- 1 | { 2 | "type": 1, 3 | "configuration": { 4 | "urcpType": "0", 5 | "urcpFixedRate": "-1", 6 | "urcpMaximumWindow": "1310720", 7 | "urcpMinimumRate": "256000", 8 | "urcpMaximumRate": "10000000", 9 | "urcpKeepAliveTimeoutMs": "0", 10 | "audioFecType": "0", 11 | "videoFecType": "0", 12 | "videoFecLevel": "3", 13 | "videoPacketUtilization": "0", 14 | "enableDynamicBitrate": "false", 15 | "dynamicBitrateScaleFactor": "1", 16 | "dynamicBitrateUpdateMs": "5000", 17 | "sendKeyframesOverTCP": "false", 18 | "videoMaximumWidth": "1280", 19 | "videoMaximumHeight": "720", 20 | "videoMaximumFrameRate": "60", 21 | "videoPacketDefragTimeoutMs": "16", 22 | "enableVideoFrameAcks": "false", 23 | "enableAudioChat": "true", 24 | "audioBufferLengthHns": "10000000", 25 | "audioSyncPolicy": "1", 26 | "audioSyncMinLatency": "10", 27 | "audioSyncDesiredLatency": "40", 28 | "audioSyncMaxLatency": "170", 29 | "audioSyncCompressLatency": "100", 30 | "audioSyncCompressFactor": "0.99", 31 | "audioSyncLengthenFactor": "1.01", 32 | "enableOpusAudio": "false", 33 | "enableOpusChatAudio": "true", 34 | "inputReadsPerSecond": "120", 35 | "udpMaxSendPacketsInWinsock": "250", 36 | "udpSubBurstGroups": "5", 37 | "udpBurstDurationMs": "11" 38 | }, 39 | "reQueryPreviewStatus": true 40 | } -------------------------------------------------------------------------------- /xbox/nano/factory/control.py: -------------------------------------------------------------------------------- 1 | from xbox.nano.packet import control 2 | 3 | 4 | def session_create(**kwargs): 5 | return control.session_create(**kwargs) 6 | 7 | 8 | def session_create_response(**kwargs): 9 | return control.session_create_response(**kwargs) 10 | 11 | 12 | def session_destroy(**kwargs): 13 | return control.session_destroy(**kwargs) 14 | 15 | 16 | def video_statistics(**kwargs): 17 | return control.video_statistics(**kwargs) 18 | 19 | 20 | def realtime_telemetry(**kwargs): 21 | return control.realtime_telemetry(**kwargs) 22 | 23 | 24 | def change_video_quality(unk3, unk4, unk5, unk6, unk7, unk8): 25 | return control.change_video_quality( 26 | unk3=unk3, 27 | unk4=unk4, 28 | unk5=unk5, 29 | unk6=unk6, 30 | unk7=unk7, 31 | unk8=unk8 32 | ) 33 | 34 | 35 | def initiate_network_test(**kwargs): 36 | return control.initiate_network_test(**kwargs) 37 | 38 | 39 | def network_information(**kwargs): 40 | return control.network_information(**kwargs) 41 | 42 | 43 | def network_test_response(**kwargs): 44 | return control.network_test_response(**kwargs) 45 | 46 | 47 | def controller_event(event, controller_num): 48 | return control.controller_event( 49 | event=event, 50 | controller_num=controller_num 51 | ) 52 | 53 | 54 | def control_header(prev_seq_dup, unk1, unk2, opcode, payload): 55 | return control.control_packet( 56 | prev_seq_dup=prev_seq_dup, 57 | unk1=unk1, 58 | unk2=unk2, 59 | opcode=opcode, 60 | payload=payload 61 | ) 62 | -------------------------------------------------------------------------------- /xbox/nano/factory/channel.py: -------------------------------------------------------------------------------- 1 | from xbox.nano.packet import message 2 | from xbox.nano.factory.message import header 3 | from xbox.nano.enum import RtpPayloadType, ChannelControlPayloadType 4 | 5 | 6 | def control_handshake(connection_id, **kwargs): 7 | return message.struct( 8 | header=header( 9 | payload_type=RtpPayloadType.Control, **kwargs 10 | ), 11 | payload=message.channel_control_handshake( 12 | type=ChannelControlPayloadType.ClientHandshake, 13 | connection_id=connection_id 14 | ) 15 | ) 16 | 17 | 18 | def create(name, flags, channel_id, **kwargs): 19 | return message.struct( 20 | header=header( 21 | payload_type=RtpPayloadType.ChannelControl, 22 | channel_id=channel_id, **kwargs 23 | ), 24 | payload=message.channel_control( 25 | type=ChannelControlPayloadType.ChannelCreate, 26 | name=name, 27 | flags=flags 28 | ) 29 | ) 30 | 31 | 32 | def open(flags, channel_id, **kwargs): 33 | return message.struct( 34 | header=header( 35 | payload_type=RtpPayloadType.ChannelControl, 36 | channel_id=channel_id, **kwargs 37 | ), 38 | payload=message.channel_control( 39 | type=ChannelControlPayloadType.ChannelOpen, 40 | name=None, 41 | flags=flags 42 | ) 43 | ) 44 | 45 | 46 | def close(flags, channel_id, **kwargs): 47 | return message.struct( 48 | header=header( 49 | payload_type=RtpPayloadType.ChannelControl, 50 | channel_id=channel_id, **kwargs 51 | ), 52 | payload=message.channel_control( 53 | type=ChannelControlPayloadType.ChannelClose, 54 | name=None, 55 | flags=flags 56 | ) 57 | ) 58 | -------------------------------------------------------------------------------- /xbox/nano/scripts/client.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import logging 3 | import argparse 4 | import asyncio 5 | 6 | from xbox.webapi.authentication.manager import AuthenticationManager 7 | 8 | from xbox.sg.console import Console 9 | from xbox.sg.enum import ConnectionState 10 | 11 | from xbox.nano.manager import NanoManager 12 | from xbox.nano.render.client import SDLClient 13 | 14 | 15 | def on_gamestream_error(error_msg) -> None: 16 | print(f'!!! Gamestream error occured: {error_msg}') 17 | sys.exit(1) 18 | 19 | 20 | async def async_main(): 21 | parser = argparse.ArgumentParser(description="Basic smartglass NANO client") 22 | parser.add_argument('--address', '-a', 23 | help="IP address of console") 24 | args = parser.parse_args() 25 | 26 | logging.basicConfig(level=logging.DEBUG) 27 | 28 | discovered = await Console.discover(timeout=1, addr=args.address) 29 | if len(discovered): 30 | console = discovered[0] 31 | 32 | console.add_manager(NanoManager) 33 | console.nano.on_gamestream_error += on_gamestream_error 34 | 35 | await console.connect("", "") 36 | if console.connection_state != ConnectionState.Connected: 37 | print("Connection failed") 38 | sys.exit(1) 39 | 40 | await console.wait(1) 41 | await console.nano.start_stream() 42 | await console.wait(2) 43 | 44 | client = SDLClient(1280, 720) 45 | await console.nano.start_gamestream(client) 46 | 47 | try: 48 | while True: 49 | await console.wait(5.0) 50 | except KeyboardInterrupt: 51 | pass 52 | else: 53 | print("No consoles discovered") 54 | sys.exit(1) 55 | 56 | 57 | def main(): 58 | loop = asyncio.get_event_loop() 59 | loop.run_until_complete(async_main()) 60 | 61 | 62 | if __name__ == "__main__": 63 | main() 64 | -------------------------------------------------------------------------------- /xbox/nano/factory/video.py: -------------------------------------------------------------------------------- 1 | from construct import Container 2 | from xbox.nano.packet import video 3 | 4 | 5 | def server_handshake(protocol_version, width, height, fps, 6 | reference_timestamp, formats): 7 | return video.server_handshake( 8 | protocol_version=protocol_version, 9 | width=width, height=height, fps=fps, 10 | reference_timestamp=reference_timestamp, 11 | formats=formats 12 | ) 13 | 14 | 15 | def client_handshake(initial_frame_id, requested_format): 16 | return video.client_handshake( 17 | initial_frame_id=initial_frame_id, 18 | requested_format=requested_format 19 | ) 20 | 21 | 22 | # TODO: split these up in often used combinations? 23 | # Need to figure out the different control messages used... 24 | def control(request_keyframe=False, start_stream=False, 25 | stop_stream=False, queue_depth=False, lost_frames=False, 26 | last_displayed_frame=False, last_displayed_frame_id=0, 27 | timestamp=0, queue_depth_field=0, first_lost_frame=0, 28 | last_lost_frame=0): 29 | return video.control( 30 | flags=Container( 31 | request_keyframe=request_keyframe, 32 | start_stream=start_stream, 33 | stop_stream=stop_stream, 34 | queue_depth=queue_depth, 35 | lost_frames=lost_frames, 36 | last_displayed_frame=last_displayed_frame 37 | ), 38 | last_displayed_frame=Container( 39 | frame_id=last_displayed_frame_id, 40 | timestamp=timestamp 41 | ), 42 | queue_depth=queue_depth_field, 43 | lost_frames=Container( 44 | first=first_lost_frame, 45 | last=last_lost_frame 46 | ) 47 | ) 48 | 49 | 50 | def data(flags, frame_id, timestamp, total_size, 51 | packet_count, offset, data): 52 | return video.data( 53 | flags=flags, frame_id=frame_id, timestamp=timestamp, 54 | total_size=total_size, packet_count=packet_count, offset=offset, 55 | data=data 56 | ) 57 | -------------------------------------------------------------------------------- /xbox/nano/factory/message.py: -------------------------------------------------------------------------------- 1 | from construct import Container 2 | from xbox.nano.packet import message 3 | from xbox.nano.enum import RtpPayloadType 4 | 5 | STREAMER_VERSION = 3 6 | 7 | 8 | def header(payload_type, connection_id=0, channel_id=0, timestamp=0, 9 | streamer=None, padding=False, sequence_num=0, **kwargs): 10 | """ 11 | Helper method for creating a RTP header. 12 | """ 13 | return message.header( 14 | flags=Container( 15 | padding=padding, 16 | payload_type=payload_type 17 | ), 18 | sequence_num=sequence_num, 19 | timestamp=timestamp, 20 | ssrc=Container( 21 | connection_id=connection_id, 22 | channel_id=channel_id 23 | ), 24 | streamer=streamer, 25 | **kwargs 26 | ) 27 | 28 | 29 | def streamer_tcp(sequence_num, prev_sequence_num, 30 | payload_type, payload, **kwargs): 31 | return message.struct( 32 | header=header( 33 | payload_type=RtpPayloadType.Streamer, 34 | streamer=message.streamer( 35 | streamer_version=STREAMER_VERSION, 36 | sequence_num=sequence_num, 37 | prev_sequence_num=prev_sequence_num, 38 | type=payload_type 39 | ), **kwargs 40 | ), 41 | payload=payload 42 | ) 43 | 44 | 45 | def streamer_udp(payload_type, payload, **kwargs): 46 | return message.struct( 47 | header=header( 48 | payload_type=RtpPayloadType.Streamer, 49 | streamer=message.streamer( 50 | streamer_version=0, 51 | type=payload_type 52 | ), **kwargs 53 | ), 54 | payload=payload 55 | ) 56 | 57 | 58 | def udp_handshake(connection_id, unknown=1, **kwargs): 59 | return message.struct( 60 | header=header( 61 | payload_type=RtpPayloadType.UDPHandshake, 62 | connection_id=connection_id, **kwargs 63 | ), 64 | payload=message.udp_handshake( 65 | unk=unknown 66 | ) 67 | ) 68 | -------------------------------------------------------------------------------- /xbox/nano/packet/video.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | from construct import * 3 | from xbox.nano.enum import VideoCodec 4 | from xbox.sg.utils.struct import XStruct 5 | from xbox.sg.utils.adapters import XEnum, PrefixedBytes 6 | from xbox.nano.adapters import ReferenceTimestampAdapter 7 | 8 | 9 | fmt = XStruct( 10 | 'fps' / Int32ul, 11 | 'width' / Int32ul, 12 | 'height' / Int32ul, 13 | 'codec' / XEnum(Int32ul, VideoCodec), 14 | 'rgb' / If(this.codec == VideoCodec.RGB, Struct( 15 | 'bpp' / Int32ul, 16 | 'bytes' / Int32ul, 17 | 'red_mask' / Int64ul, 18 | 'green_mask' / Int64ul, 19 | 'blue_mask' / Int64ul 20 | )) 21 | ) 22 | 23 | 24 | server_handshake = XStruct( 25 | 'protocol_version' / Int32ul, 26 | 'width' / Int32ul, 27 | 'height' / Int32ul, 28 | 'fps' / Int32ul, 29 | 'reference_timestamp' / ReferenceTimestampAdapter(), 30 | 'formats' / PrefixedArray(Int32ul, fmt) 31 | ) 32 | 33 | 34 | client_handshake = XStruct( 35 | 'initial_frame_id' / Int32ul, 36 | 'requested_format' / fmt 37 | ) 38 | 39 | 40 | control = XStruct( 41 | 'flags' / BitStruct( 42 | Padding(2), 43 | 'request_keyframe' / Default(Flag, False), 44 | 'start_stream' / Default(Flag, False), 45 | 'stop_stream' / Default(Flag, False), 46 | 'queue_depth' / Default(Flag, False), 47 | 'lost_frames' / Default(Flag, False), 48 | 'last_displayed_frame' / Default(Flag, False), 49 | Padding(24), 50 | ), 51 | 'last_displayed_frame' / If(this.flags.last_displayed_frame, Struct( 52 | 'frame_id' / Int32ul, 53 | 'timestamp' / Int64sl 54 | )), 55 | 'queue_depth' / If(this.flags.queue_depth, Int32ul), 56 | 'lost_frames' / If(this.flags.lost_frames, Struct( 57 | 'first' / Int32ul, 58 | 'last' / Int32ul 59 | )) 60 | ) 61 | 62 | 63 | data = XStruct( 64 | 'flags' / Int32ul, 65 | 'frame_id' / Int32ul, 66 | 'timestamp' / Int64ul, 67 | 'total_size' / Int32ul, 68 | 'packet_count' / Int32ul, 69 | 'offset' / Int32ul, 70 | 'data' / PrefixedBytes(Int32ul) 71 | ) 72 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: ['push', 'pull_request'] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | python-version: [3.7, 3.8, 3.9] 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Set up Python ${{ matrix.python-version }} 16 | uses: actions/setup-python@v2 17 | with: 18 | python-version: ${{ matrix.python-version }} 19 | - name: Install native dependencies 20 | run: | 21 | sudo apt update 22 | sudo apt install pkg-config libavcodec-dev libavformat-dev libavutil-dev libavdevice-dev libavfilter-dev libswscale-dev libswresample-dev ffmpeg libsdl2-dev 23 | - name: Install dependencies 24 | run: | 25 | python -m pip install --upgrade pip 26 | pip install -e .[dev] 27 | python setup.py develop 28 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 29 | - name: Lint with flake8 30 | run: | 31 | # stop the build if there are Python syntax errors or undefined names 32 | flake8 xbox --count --select=E9,F63,F7,F82 --show-source --statistics 33 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 34 | flake8 xbox --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 35 | - name: Test with pytest 36 | run: | 37 | pytest 38 | 39 | deploy: 40 | runs-on: ubuntu-latest 41 | needs: build 42 | steps: 43 | - uses: actions/checkout@v2 44 | - name: Set up Python 45 | uses: actions/setup-python@v2 46 | with: 47 | python-version: '3.8' 48 | - name: Install dependencies 49 | run: | 50 | python -m pip install --upgrade pip 51 | pip install setuptools wheel twine 52 | - name: Build and publish 53 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') 54 | env: 55 | TWINE_USERNAME: __token__ 56 | TWINE_PASSWORD: ${{ secrets.PYPI_API_KEY }} 57 | run: | 58 | python setup.py sdist bdist_wheel 59 | twine upload dist/* 60 | -------------------------------------------------------------------------------- /xbox/nano/render/audio/aac.py: -------------------------------------------------------------------------------- 1 | from av import audio 2 | 3 | 4 | class AACProfile(object): 5 | Main = 0 6 | Lc = 1 7 | Ssr = 2 8 | Ltp = 3 9 | 10 | 11 | class AACFrame(object): 12 | """ 13 | Use like this, on each audio frame: 14 | frame_size = len(msg.payload.payload.data) 15 | frame = AACFrame.gen_adts_header(frame_size, AACProfile.Main, 48000, 2) 16 | frame += msg.payload.payload.data 17 | ... deliver to audio sink 18 | """ 19 | sampling_freq_index = { 20 | 96000: 0, 21 | 88200: 1, 22 | 64000: 2, 23 | 48000: 3, 24 | 44100: 4, 25 | 32000: 5, 26 | 24000: 6, 27 | 22050: 7, 28 | 16000: 8, 29 | 12000: 9, 30 | 11025: 10, 31 | 8000: 11, 32 | 7350: 12 33 | } 34 | ADTS_HEADER_LEN = 7 35 | 36 | @staticmethod 37 | def generate_header(frame_size, aac_profile, sampling_freq, channels): 38 | header_id = 0 # MPEG4 39 | adts_headers = bytearray(AACFrame.ADTS_HEADER_LEN) 40 | frame_size += AACFrame.ADTS_HEADER_LEN 41 | sampling_index = AACFrame.sampling_freq_index[sampling_freq] 42 | 43 | adts_headers[0] = 0xFF 44 | adts_headers[1] = 0xF0 | (header_id << 3) | 0x1 45 | adts_headers[2] = (aac_profile << 6) | (sampling_index << 2) | 0x2 | \ 46 | (channels & 0x4) 47 | adts_headers[3] = ((channels & 0x3) << 6) | 0x30 | (frame_size >> 11) 48 | adts_headers[4] = ((frame_size >> 3) & 0x00FF) 49 | adts_headers[5] = (((frame_size & 0x0007) << 5) + 0x1F) 50 | adts_headers[6] = 0xFC 51 | 52 | return adts_headers 53 | 54 | 55 | class AACResampler(object): 56 | """ 57 | Resampler should be used to convert AAC data from planar->packet format 58 | """ 59 | def __init__(self, sample_format, samplerate, channels): 60 | layout = audio.layout.AudioLayout(channels) 61 | self.resampler = audio.resampler.AudioResampler(format=sample_format, 62 | layout=layout, 63 | rate=samplerate) 64 | 65 | def resample(self, frame): 66 | return self.resampler.resample(frame) 67 | -------------------------------------------------------------------------------- /xbox/nano/render/client/base.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from typing import Optional 3 | 4 | from xbox.nano.enum import ChannelClass 5 | 6 | 7 | class ClientError(Exception): 8 | pass 9 | 10 | 11 | class Client(object): 12 | def __init__(self, video, audio, input): 13 | self.video = video 14 | self.audio = audio 15 | self.input = input 16 | self.protocol = None 17 | 18 | self._running = False 19 | self._loop_task: Optional[asyncio.Task] = None 20 | 21 | def open(self, protocol): 22 | self.protocol = protocol 23 | self.video.open(self) 24 | self.audio.open(self) 25 | self.input.open(self) 26 | 27 | self.start_loop() 28 | 29 | def close(self): 30 | self.video.close() 31 | self.audio.close() 32 | self.input.close() 33 | 34 | def start_loop(self): 35 | self._loop_task = asyncio.create_task(self.loop()) 36 | 37 | async def loop(self): 38 | self._running = True 39 | while self._running: 40 | self.pump() 41 | await asyncio.sleep(0.1) 42 | 43 | def pump(self): 44 | self.video.pump() 45 | self.audio.pump() 46 | self.input.pump() 47 | 48 | def set_video_format(self, video_fmt): 49 | self.video.setup(video_fmt) 50 | 51 | def set_audio_format(self, audio_fmt): 52 | self.audio.setup(audio_fmt) 53 | 54 | def render_video(self, data): 55 | self.video.render(data) 56 | 57 | def render_audio(self, data): 58 | self.audio.render(data) 59 | 60 | def send_input(self, frame, timestamp_dt): 61 | input_channel = self.protocol.get_channel(ChannelClass.Input) 62 | if input_channel and input_channel.reference_timestamp: 63 | input_channel.send_frame(frame, timestamp_dt) 64 | 65 | def controller_added(self, controller_index): 66 | control_channel = self.protocol.get_channel(ChannelClass.Control) 67 | if control_channel: 68 | control_channel.controller_added(controller_index) 69 | 70 | def controller_removed(self, controller_index): 71 | control_channel = self.protocol.get_channel(ChannelClass.Control) 72 | if control_channel: 73 | control_channel.controller_removed(controller_index) 74 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from setuptools import setup, find_namespace_packages 5 | 6 | setup( 7 | name="xbox-smartglass-nano", 8 | version="0.10.1", 9 | author="OpenXbox", 10 | author_email="noreply@openxbox.org", 11 | description="The NANO (v2) part of the xbox smartglass library", 12 | long_description=open('README.md').read() + '\n\n' + open('CHANGELOG.md').read(), 13 | long_description_content_type="text/markdown", 14 | license="MIT", 15 | keywords="xbox one smartglass nano gamestreaming xcloud", 16 | url="https://github.com/OpenXbox/xbox-smartglass-nano-python", 17 | python_requires=">=3.7", 18 | packages=find_namespace_packages(include=['xbox.*']), 19 | zip_safe=False, 20 | include_package_data=True, 21 | classifiers=[ 22 | "Development Status :: 4 - Beta", 23 | "Intended Audience :: Developers", 24 | "License :: OSI Approved :: MIT License", 25 | "Topic :: Software Development :: Libraries :: Python Modules", 26 | "Programming Language :: Python :: 3", 27 | "Programming Language :: Python :: 3.7", 28 | "Programming Language :: Python :: 3.8", 29 | "Programming Language :: Python :: 3.9" 30 | ], 31 | test_suite="tests", 32 | install_requires=[ 33 | 'xbox-smartglass-core==1.3.0', 34 | 'av==8.0.3', 35 | 'PySDL2==0.9.7' 36 | ], 37 | setup_requires=['pytest-runner'], 38 | tests_require=['pytest', 'pytest-console-scripts', 'pytest-asyncio'], 39 | extras_require={ 40 | "dev": [ 41 | "pip", 42 | "bump2version", 43 | "wheel", 44 | "watchdog", 45 | "flake8", 46 | "coverage", 47 | "Sphinx", 48 | "sphinx_rtd_theme", 49 | "recommonmark", 50 | "twine", 51 | "pytest", 52 | "pytest-asyncio", 53 | "pytest-console-scripts", 54 | "pytest-runner", 55 | ], 56 | }, 57 | entry_points={ 58 | 'console_scripts': [ 59 | 'xbox-nano-client=xbox.nano.scripts.client:main', 60 | 'xbox-nano-pcap=xbox.nano.scripts.pcap:main', 61 | 'xbox-nano-replay=xbox.nano.scripts.replay:main' 62 | ] 63 | } 64 | ) 65 | -------------------------------------------------------------------------------- /xbox/nano/render/client/file.py: -------------------------------------------------------------------------------- 1 | from xbox.nano.render.client.base import Client 2 | from xbox.nano.render.audio.aac import AACFrame, AACProfile 3 | 4 | 5 | class FileClient(Client): 6 | def __init__(self, filename, save_frames=False): 7 | self.filename = filename 8 | self.save_frames = save_frames 9 | 10 | self._audio_fmt = None 11 | self._video_file = None 12 | self._audio_file = None 13 | self._video_frame_index = 0 14 | self._audio_frame_index = 0 15 | super(FileClient, self).__init__(None, None, None) 16 | 17 | def open(self, protocol): 18 | if not self.save_frames: 19 | self._video_file = open('%s.video.raw' % self.filename, 'wb') 20 | self._audio_file = open('%s.audio.raw' % self.filename, 'wb') 21 | 22 | def close(self): 23 | if not self.save_frames: 24 | self._video_file.close() 25 | self._audio_file.close() 26 | 27 | def loop(self): 28 | pass 29 | 30 | def pump(self): 31 | pass 32 | 33 | def set_video_format(self, video_fmt): 34 | pass 35 | 36 | def set_audio_format(self, audio_fmt): 37 | self._audio_fmt = audio_fmt 38 | 39 | def render_video(self, data): 40 | # Video frames can be written as-is 41 | if not self.save_frames: 42 | self._video_file.write(data) 43 | else: 44 | with open('%s.video.%08d.frame' % (self.filename, self._video_frame_index), 'wb') as f: 45 | f.write(data) 46 | self._video_frame_index += 1 47 | 48 | def render_audio(self, data): 49 | if not self._audio_fmt: 50 | raise Exception( 51 | "No audio format set, cannot create frame header" 52 | ) 53 | # Audio frames need a header prepended 54 | data = AACFrame.generate_header( 55 | len(data), AACProfile.Main, 56 | self._audio_fmt.sample_rate, 57 | self._audio_fmt.channels 58 | ) + data 59 | 60 | if not self.save_frames: 61 | self._audio_file.write(data) 62 | else: 63 | with open('%s.audio.%08d.frame' % (self.filename, self._audio_frame_index), 'wb') as f: 64 | f.write(data) 65 | self._audio_frame_index += 1 66 | 67 | def send_input(self, frame, timestamp): 68 | pass 69 | 70 | def controller_added(self, controller_index): 71 | pass 72 | 73 | def controller_removed(self, controller_index): 74 | pass 75 | -------------------------------------------------------------------------------- /xbox/nano/packet/input.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | from construct import * 3 | from xbox.sg.utils.struct import XStruct 4 | from xbox.nano.adapters import ReferenceTimestampAdapter 5 | 6 | server_handshake = XStruct( 7 | 'protocol_version' / Int32ul, 8 | 'desktop_width' / Int32ul, 9 | 'desktop_height' / Int32ul, 10 | 'max_touches' / Int32ul, 11 | 'initial_frame_id' / Int32ul 12 | ) 13 | 14 | 15 | client_handshake = XStruct( 16 | 'max_touches' / Int32ul, 17 | 'reference_timestamp' / ReferenceTimestampAdapter() 18 | ) 19 | 20 | 21 | frame_ack = XStruct( 22 | 'acked_frame' / Int32ul 23 | ) 24 | 25 | 26 | input_frame_buttons = XStruct( 27 | 'dpad_up' / Default(Byte, 0), 28 | 'dpad_down' / Default(Byte, 0), 29 | 'dpad_left' / Default(Byte, 0), 30 | 'dpad_right' / Default(Byte, 0), 31 | 'start' / Default(Byte, 0), 32 | 'back' / Default(Byte, 0), 33 | 'left_thumbstick' / Default(Byte, 0), 34 | 'right_thumbstick' / Default(Byte, 0), 35 | 'left_shoulder' / Default(Byte, 0), 36 | 'right_shoulder' / Default(Byte, 0), 37 | 'guide' / Default(Byte, 0), 38 | 'unknown' / Default(Byte, 0), 39 | 'a' / Default(Byte, 0), 40 | 'b' / Default(Byte, 0), 41 | 'x' / Default(Byte, 0), 42 | 'y' / Default(Byte, 0) 43 | ) 44 | 45 | 46 | input_frame_analog = XStruct( 47 | 'left_trigger' / Default(Byte, 0), 48 | 'right_trigger' / Default(Byte, 0), 49 | 'left_thumb_x' / Default(Int16sl, 0), 50 | 'left_thumb_y' / Default(Int16sl, 0), 51 | 'right_thumb_x' / Default(Int16sl, 0), 52 | 'right_thumb_y' / Default(Int16sl, 0), 53 | 'rumble_trigger_l' / Default(Byte, 0), 54 | 'rumble_trigger_r' / Default(Byte, 0), 55 | 'rumble_handle_l' / Default(Byte, 0), 56 | 'rumble_handle_r' / Default(Byte, 0) 57 | ) 58 | 59 | 60 | input_frame_extension = XStruct( 61 | 'byte_6' / Default(Byte, 0), # always 1 for gamepad stuff? 62 | 'byte_7' / Default(Byte, 0), 63 | 'rumble_trigger_l2' / Default(Byte, 0), 64 | 'rumble_trigger_r2' / Default(Byte, 0), 65 | 'rumble_handle_l2' / Default(Byte, 0), 66 | 'rumble_handle_r2' / Default(Byte, 0), 67 | 'byte_12' / Default(Byte, 0), 68 | 'byte_13' / Default(Byte, 0), 69 | 'byte_14' / Default(Byte, 0) 70 | ) 71 | 72 | 73 | frame = XStruct( 74 | 'frame_id' / Int32ul, 75 | 'timestamp' / Int64ul, 76 | 'created_ts' / Int64ul, 77 | 'buttons' / input_frame_buttons, 78 | 'analog' / input_frame_analog, 79 | # Check if remaining length is at least 9 80 | 'extension' / input_frame_extension 81 | ) 82 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean clean-test clean-pyc clean-build docs help 2 | .DEFAULT_GOAL := help 3 | 4 | define BROWSER_PYSCRIPT 5 | import os, webbrowser, sys 6 | 7 | try: 8 | from urllib import pathname2url 9 | except: 10 | from urllib.request import pathname2url 11 | 12 | webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1]))) 13 | endef 14 | export BROWSER_PYSCRIPT 15 | 16 | define PRINT_HELP_PYSCRIPT 17 | import re, sys 18 | 19 | for line in sys.stdin: 20 | match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line) 21 | if match: 22 | target, help = match.groups() 23 | print("%-20s %s" % (target, help)) 24 | endef 25 | export PRINT_HELP_PYSCRIPT 26 | 27 | BROWSER := python -c "$$BROWSER_PYSCRIPT" 28 | 29 | help: 30 | @python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST) 31 | 32 | clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts 33 | 34 | clean-build: ## remove build artifacts 35 | rm -fr build/ 36 | rm -fr dist/ 37 | rm -fr .eggs/ 38 | find . -name '*.egg-info' -exec rm -fr {} + 39 | find . -name '*.egg' -exec rm -f {} + 40 | 41 | clean-pyc: ## remove Python file artifacts 42 | find . -name '*.pyc' -exec rm -f {} + 43 | find . -name '*.pyo' -exec rm -f {} + 44 | find . -name '*~' -exec rm -f {} + 45 | find . -name '__pycache__' -exec rm -fr {} + 46 | 47 | clean-test: ## remove test and coverage artifacts 48 | rm -fr .tox/ 49 | rm -f .coverage 50 | rm -fr htmlcov/ 51 | 52 | lint: ## check style with flake8 53 | flake8 xbox tests 54 | 55 | test: ## run tests quickly with the default Python 56 | py.test 57 | 58 | test-all: ## run tests on every Python version with tox 59 | tox 60 | 61 | coverage: ## check code coverage quickly with the default Python 62 | coverage run --source xbox -m pytest 63 | coverage report -m 64 | coverage html 65 | $(BROWSER) htmlcov/index.html 66 | 67 | docs: ## generate Sphinx HTML documentation, including API docs 68 | rm -f docs/xbox.rst 69 | rm -f docs/modules.rst 70 | sphinx-apidoc --implicit-namespaces -a -e -o docs/source xbox 71 | $(MAKE) -C docs clean 72 | $(MAKE) -C docs html 73 | $(BROWSER) docs/_build/html/index.html 74 | 75 | servedocs: docs ## compile the docs watching for changes 76 | watchmedo shell-command -p '*.rst' -c '$(MAKE) -C docs html' -R -D . 77 | 78 | release: clean ## package and upload a release 79 | twine upload dist/* 80 | 81 | dist: clean ## builds source and wheel package 82 | python setup.py sdist 83 | python setup.py bdist_wheel 84 | ls -l dist 85 | 86 | install: clean ## install the package to the active Python's site-packages 87 | python setup.py install 88 | -------------------------------------------------------------------------------- /xbox/nano/scripts/replay.py: -------------------------------------------------------------------------------- 1 | import dpkt 2 | import logging 3 | import argparse 4 | from xbox.nano.render.client.sdl import SDLClient 5 | from xbox.nano.render.client.file import FileClient 6 | from xbox.nano.protocol import NanoProtocol, StreamerProtocol, ControlProtocol 7 | 8 | 9 | class DummyStreamerProtocol(StreamerProtocol): 10 | def send_message(self, msg): 11 | pass 12 | 13 | 14 | class DummyControlProtocol(ControlProtocol): 15 | def send_message(self, msg): 16 | pass 17 | 18 | 19 | def replay(client, pcap_file, tcp_port, udp_port): 20 | with open(pcap_file, 'rb') as fh: 21 | proto = NanoProtocol(client, None, None, None, None) 22 | proto.streamer_protocol = DummyStreamerProtocol('', 0, proto) 23 | proto.control_protocol = DummyControlProtocol('', 0, proto) 24 | 25 | proto.control_protocol.on_message += proto._on_control_message 26 | proto.streamer_protocol.on_message += proto._on_streamer_message 27 | 28 | client.open(proto) 29 | 30 | for ts, buf in dpkt.pcap.Reader(fh): 31 | eth = dpkt.ethernet.Ethernet(buf) 32 | 33 | # Make sure the Ethernet data contains an IP packet 34 | if not isinstance(eth.data, dpkt.ip.IP): 35 | continue 36 | 37 | ip = eth.data 38 | 39 | if isinstance(ip.data, dpkt.tcp.TCP): 40 | if ip.data.sport != tcp_port and ip.data.dport != tcp_port: 41 | continue 42 | 43 | from_client = ip.data.dport == tcp_port 44 | 45 | if not from_client: 46 | proto.control_protocol.handle(ip.data.data) 47 | 48 | elif isinstance(ip.data, dpkt.udp.UDP): 49 | if ip.data.sport != udp_port and ip.data.dport != udp_port: 50 | continue 51 | 52 | from_client = ip.data.dport == udp_port 53 | if not from_client: 54 | proto.streamer_protocol.handle(ip.data.data, '') 55 | 56 | else: 57 | continue 58 | 59 | client.pump() 60 | 61 | 62 | def main(): 63 | logging.basicConfig(level=logging.DEBUG) 64 | 65 | parser = argparse.ArgumentParser( 66 | description='Parse PCAP files and replay SG sessions' 67 | ) 68 | parser.add_argument('file', help='Path to PCAP') 69 | parser.add_argument('tcp_port', type=int, help='Server TCP Port (console)') 70 | parser.add_argument('udp_port', type=int, help='Server UDP Port (console)') 71 | parser.add_argument('--output', '-o', help='Write stream to file') 72 | parser.add_argument('--frames', '-f', action='store_true', 73 | help='Save single frames') 74 | args = parser.parse_args() 75 | 76 | if args.output: 77 | client = FileClient(args.output, save_frames=args.frames) 78 | else: 79 | client = SDLClient(1280, 720) 80 | 81 | replay(client, args.file, args.tcp_port, args.udp_port) 82 | 83 | 84 | if __name__ == '__main__': 85 | main() 86 | -------------------------------------------------------------------------------- /xbox/nano/packet/control.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | from construct import * 3 | from xbox.sg.utils.struct import XStruct 4 | from xbox.sg.utils.adapters import XSwitch, XEnum, PrefixedBytes 5 | from xbox.nano.enum import ControlPayloadType, ControllerEvent 6 | 7 | """ 8 | ControlProtocol Streamer Messages 9 | """ 10 | 11 | 12 | session_init = XStruct( 13 | 'unk3' / GreedyBytes 14 | ) 15 | 16 | 17 | session_create = XStruct( 18 | 'guid' / Bytes(16), 19 | 'unk3' / PrefixedBytes(Int32ul) 20 | ) 21 | 22 | 23 | session_create_response = XStruct( 24 | 'guid' / Bytes(16) 25 | ) 26 | 27 | 28 | session_destroy = XStruct( 29 | 'unk3' / Float32l, 30 | 'unk5' / PrefixedBytes(Int32ul) 31 | ) 32 | 33 | 34 | video_statistics = XStruct( 35 | 'unk3' / Float32l, 36 | 'unk4' / Float32l, 37 | 'unk5' / Float32l, 38 | 'unk6' / Float32l, 39 | 'unk7' / Float32l, 40 | 'unk8' / Float32l 41 | ) 42 | 43 | 44 | realtime_telemetry = XStruct( 45 | 'data' / PrefixedArray(Int16ul, Struct( 46 | 'key' / Int16ul, 47 | 'value' / Int64ul 48 | )) 49 | ) 50 | 51 | 52 | change_video_quality = XStruct( 53 | 'unk3' / Int32ul, 54 | 'unk4' / Int32ul, 55 | 'unk5' / Int32ul, 56 | 'unk6' / Int32ul, 57 | 'unk7' / Int32ul, 58 | 'unk8' / Int32ul 59 | ) 60 | 61 | 62 | initiate_network_test = XStruct( 63 | 'guid' / Bytes(16) 64 | ) 65 | 66 | 67 | network_information = XStruct( 68 | 'guid' / Bytes(16), 69 | 'unk4' / Int64ul, 70 | 'unk5' / Int8ul, 71 | 'unk6' / Float32l 72 | ) 73 | 74 | 75 | network_test_response = XStruct( 76 | 'guid' / Bytes(16), 77 | 'unk3' / Float32l, 78 | 'unk4' / Float32l, 79 | 'unk5' / Float32l, 80 | 'unk6' / Float32l, 81 | 'unk7' / Float32l, 82 | 'unk8' / Int64ul, 83 | 'unk9' / Int64ul, 84 | 'unk10' / Float32l 85 | ) 86 | 87 | 88 | controller_event = XStruct( 89 | 'event' / XEnum(Int8ul, ControllerEvent), 90 | 'controller_num' / Int8ul 91 | ) 92 | 93 | 94 | control_packet = XStruct( 95 | 'prev_seq_dup' / Int32ul, 96 | 'unk1' / Int16ul, 97 | 'unk2' / Int16ul, 98 | 'opcode' / XEnum(Int16ul, ControlPayloadType), 99 | 'payload' / XSwitch( 100 | this.opcode, { 101 | ControlPayloadType.SessionInit: session_init, 102 | ControlPayloadType.SessionCreate: session_create, 103 | ControlPayloadType.SessionCreateResponse: session_create_response, 104 | ControlPayloadType.SessionDestroy: session_destroy, 105 | ControlPayloadType.VideoStatistics: video_statistics, 106 | ControlPayloadType.RealtimeTelemetry: realtime_telemetry, 107 | ControlPayloadType.ChangeVideoQuality: change_video_quality, 108 | ControlPayloadType.InitiateNetworkTest: initiate_network_test, 109 | ControlPayloadType.NetworkInformation: network_information, 110 | ControlPayloadType.NetworkTestResponse: network_test_response, 111 | ControlPayloadType.ControllerEvent: controller_event 112 | } 113 | ) 114 | ) 115 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Xbox-Smartglass-Nano 2 | 3 | 4 | [![PyPi version](https://pypip.in/version/xbox-smartglass-nano/badge.svg)](https://pypi.python.org/pypi/xbox-smartglass-nano) 5 | [![Docs](https://readthedocs.org/projects/xbox-smartglass-nano-python/badge/?version=latest)](http://xbox-smartglass-nano-python.readthedocs.io/en/latest/?badge=latest) 6 | [![Build status](https://img.shields.io/github/workflow/status/OpenXbox/xbox-smartglass-nano-python/build?label=build)](https://github.com/OpenXbox/xbox-smartglass-nano-python/actions?query=workflow%3Abuild) 7 | [![Discord chat](https://img.shields.io/discord/338946086775554048)](https://openxbox.org/discord) 8 | 9 | The gamestreaming part of the smartglass library, codename NANO. 10 | 11 | Currently supported version: 12 | 13 | * NANO v1 (Xbox One family) 14 | 15 | XCloud and new XHome streaming are Nano v3, required for Xbox Series S/X. 16 | 17 | For in-depth information, check out the documentation: (https://openxbox.github.io) 18 | 19 | 20 | ## Features 21 | 22 | * Stream from your local Xbox One (OG/S/X) console 23 | 24 | 25 | ## Dependencies 26 | 27 | * Python >= 3.7 28 | * xbox-smartglass-core 29 | * pyav (https://pyav.org/docs/develop/) 30 | * pysdl2 (https://pysdl2.readthedocs.io/en/latest/install.html) 31 | 32 | 33 | ## Install 34 | 35 | pip install xbox-smartglass-nano 36 | 37 | ## How to use 38 | 39 | ```text 40 | xbox-nano-client 41 | ``` 42 | 43 | ## Known issues 44 | 45 | * Video / Audio / Input is not smooth yet 46 | * ChatAudio and Input Feedback not implemented 47 | 48 | ## Development workflow 49 | 50 | Ready to contribute? Here's how to set up `xbox-smartglass-nano-python` for local development. 51 | 52 | 1. Fork the `xbox-smartglass-nano-python` repo on GitHub. 53 | 2. Clone your fork locally 54 | 55 | ```text 56 | git clone git@github.com:your_name_here/xbox-smartglass-nano-python.git 57 | ``` 58 | 59 | 3. Install your local copy into a virtual environment. This is how you set up your fork for local development 60 | 61 | ```text 62 | python -m venv ~/pyvenv/xbox-smartglass 63 | source ~/pyvenv/xbox-smartglass/bin/activate 64 | cd xbox-smartglass-nano-python 65 | pip install -e .[dev] 66 | ``` 67 | 68 | 5. Create a branch for local development:: 69 | 70 | ```text 71 | git checkout -b name-of-your-bugfix-or-feature 72 | ``` 73 | 74 | 6. Make your changes. 75 | 76 | 7. Before pushing the changes to git, please verify they actually work 77 | 78 | ```text 79 | pytest 80 | ``` 81 | 82 | 8. Commit your changes and push your branch to GitHub:: 83 | 84 | ```text 85 | git commit -m "Your detailed description of your changes." 86 | git push origin name-of-your-bugfix-or-feature 87 | ``` 88 | 89 | 9. Submit a pull request through the GitHub website. 90 | 91 | ### Pull Request Guidelines 92 | 93 | Before you submit a pull request, check that it meets these guidelines: 94 | 95 | 1. Code includes unit-tests. 96 | 2. Added code is properly named and documented. 97 | 3. On major changes the README is updated. 98 | 4. Run tests / linting locally before pushing to remote. 99 | 100 | 101 | ## Credits 102 | 103 | This package uses parts of [Cookiecutter](https://github.com/audreyr/cookiecutter) and the 104 | [audreyr/cookiecutter-pypackage project template](https://github.com/audreyr/cookiecutter-pypackage) 105 | -------------------------------------------------------------------------------- /xbox/nano/render/audio/sdl.py: -------------------------------------------------------------------------------- 1 | import sdl2 2 | import logging 3 | from xbox.nano.enum import AudioCodec 4 | from xbox.nano.render.sink import Sink 5 | from xbox.nano.render.codec import FrameDecoder 6 | from xbox.nano.render.audio.aac import AACFrame, AACProfile, AACResampler 7 | 8 | log = logging.getLogger(__name__) 9 | 10 | 11 | class AudioRenderError(Exception): 12 | pass 13 | 14 | 15 | class SDLAudioRenderer(Sink): 16 | def __init__(self, sample_size=4096): 17 | self._sample_size = sample_size 18 | self._sample_rate = None 19 | self._channels = None 20 | self._audio_spec = None 21 | self._decoder = None 22 | self._resampler = None 23 | self._dev = None 24 | self._fmt = None 25 | 26 | def open(self, client): 27 | sdl2.SDL_InitSubSystem(sdl2.SDL_INIT_AUDIO) 28 | 29 | def close(self): 30 | sdl2.SDL_PauseAudioDevice(self._dev, 1) 31 | sdl2.SDL_CloseAudioDevice(self._dev) 32 | 33 | def setup(self, fmt): 34 | if fmt.codec == AudioCodec.AAC: 35 | sdl_audio_fmt = sdl2.AUDIO_F32LSB 36 | self._resampler = AACResampler( 37 | 'flt', fmt.sample_rate, fmt.channels 38 | ) 39 | elif fmt.codec == AudioCodec.PCM: 40 | raise TypeError("PCM format not implemented") 41 | elif fmt.codec == AudioCodec.Opus: 42 | raise TypeError("Opus format not implemented") 43 | else: 44 | raise TypeError("Unknown audio codec: %d" % fmt.codec) 45 | 46 | self._channels = fmt.channels 47 | self._sample_rate = fmt.sample_rate 48 | self._decoder = FrameDecoder.audio(fmt.codec) 49 | 50 | dummy_callback = sdl2.SDL_AudioCallback() 51 | 52 | target_spec = sdl2.SDL_AudioSpec( 53 | self._sample_rate, sdl_audio_fmt, self._channels, 54 | self._sample_size, dummy_callback 55 | ) 56 | 57 | self._audio_spec = sdl2.SDL_AudioSpec( 58 | self._sample_rate, sdl_audio_fmt, self._channels, 59 | self._sample_size, dummy_callback 60 | ) 61 | 62 | self._dev = sdl2.SDL_OpenAudioDevice( 63 | None, 0, target_spec, self._audio_spec, 64 | sdl2.SDL_AUDIO_ALLOW_FORMAT_CHANGE 65 | ) 66 | 67 | if not self._dev: 68 | raise AudioRenderError( 69 | "SDL: Could not open audio device: %s - exiting", 70 | sdl2.SDL_GetError() 71 | ) 72 | 73 | # TODO: Is this even possible? 74 | if target_spec.format != self._audio_spec.format: 75 | log.error("SDL: We didn't get requested audio format") 76 | 77 | # Start playback 78 | sdl2.SDL_PauseAudioDevice(self._dev, 0) 79 | 80 | def render(self, data): 81 | # TODO: Make decoder recognize data 82 | # without adding a header manually 83 | data = AACFrame.generate_header( 84 | len(data), AACProfile.Main, 85 | self._sample_rate, self._channels 86 | ) + data 87 | 88 | for frame in self._decoder.decode(data): 89 | if self._resampler: 90 | frame = self._resampler.resample(frame) 91 | audio_data = frame.planes[0].to_bytes() 92 | sdl2.SDL_QueueAudio( 93 | self._dev, audio_data, len(audio_data) 94 | ) 95 | -------------------------------------------------------------------------------- /xbox/nano/render/video/sdl.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import threading 3 | from ctypes import cast, c_ubyte, POINTER 4 | 5 | import sdl2 6 | import sdl2.ext 7 | 8 | from xbox.nano.enum import VideoCodec 9 | from xbox.nano.render.sink import Sink 10 | from xbox.nano.render.codec import FrameDecoder 11 | 12 | log = logging.getLogger(__name__) 13 | 14 | 15 | class VideoRenderError(Exception): 16 | pass 17 | 18 | 19 | class SDLVideoRenderer(Sink): 20 | TITLE = 'Nano SDL' 21 | 22 | def __init__(self, width, height, fullscreen=False): 23 | self._window = None 24 | self._window_dimensions = (width, height) 25 | self._window_flags = sdl2.SDL_WINDOW_FULLSCREEN if fullscreen else 0 26 | self._renderer = None 27 | self._texture = None 28 | self._decoder = None 29 | self._fmt = None 30 | 31 | self._lock = threading.Lock() 32 | 33 | def open(self, client): 34 | sdl2.ext.init() 35 | self._window = sdl2.ext.Window( 36 | self.TITLE, self._window_dimensions, 37 | (sdl2.SDL_WINDOWPOS_UNDEFINED, sdl2.SDL_WINDOWPOS_UNDEFINED), 38 | sdl2.SDL_WINDOW_OPENGL | sdl2.SDL_WINDOW_RESIZABLE | 39 | self._window_flags 40 | ) 41 | 42 | self._renderer = sdl2.ext.Renderer( 43 | self._window, -1, None, 44 | sdl2.SDL_RENDERER_ACCELERATED | sdl2.SDL_RENDERER_PRESENTVSYNC 45 | ) 46 | 47 | self._window.show() 48 | 49 | def close(self): 50 | sdl2.SDL_DestroyTexture(self._texture) 51 | del self._renderer 52 | del self._window 53 | sdl2.ext.quit() 54 | 55 | def setup(self, fmt): 56 | if fmt.codec == VideoCodec.H264: 57 | pixel_fmt = sdl2.SDL_PIXELFORMAT_YV12 58 | elif fmt.codec == VideoCodec.YUV: 59 | raise TypeError("YUV format not implemented") 60 | elif fmt.codec == VideoCodec.RGB: 61 | raise TypeError("RGB format not implemented") 62 | else: 63 | raise TypeError("Unknown video codec: %d" % fmt.codec) 64 | 65 | self._decoder = FrameDecoder.video(fmt.codec) 66 | self._texture = sdl2.SDL_CreateTexture( 67 | self._renderer.sdlrenderer, pixel_fmt, 68 | sdl2.SDL_TEXTUREACCESS_STREAMING, 69 | fmt.width, fmt.height 70 | ) 71 | 72 | def render(self, data): 73 | renderer = self._renderer.sdlrenderer 74 | try: 75 | for frame in self._decoder.decode(data): 76 | self._lock.acquire() 77 | intp = POINTER(c_ubyte) 78 | sdl2.SDL_UpdateYUVTexture( 79 | self._texture, None, 80 | cast(frame.planes[0].buffer_ptr, intp), frame.planes[0].line_size, 81 | cast(frame.planes[1].buffer_ptr, intp), frame.planes[1].line_size, 82 | cast(frame.planes[2].buffer_ptr, intp), frame.planes[2].line_size, 83 | ) 84 | self._lock.release() 85 | sdl2.SDL_RenderClear(renderer) 86 | sdl2.SDL_RenderCopy(renderer, self._texture, None, None) 87 | sdl2.SDL_RenderPresent(renderer) 88 | except Exception as e: 89 | log.debug('SDLVideoRenderer.render: {0}'.format(e)) 90 | 91 | def pump(self): 92 | sdl2.SDL_PumpEvents() 93 | self._window.refresh() 94 | -------------------------------------------------------------------------------- /xbox/nano/render/input/sdl_keyboard.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | 4 | import sdl2 5 | import sdl2.ext 6 | 7 | from xbox.nano.render.input.base import InputHandler, InputError, \ 8 | GamepadButton, GamepadButtonState, GamepadAxis 9 | 10 | LOGGER = logging.getLogger(__name__) 11 | 12 | 13 | SDL_BUTTON_MAP = { 14 | sdl2.SDLK_UP: GamepadButton.DPadUp, 15 | sdl2.SDLK_DOWN: GamepadButton.DPadDown, 16 | sdl2.SDLK_LEFT: GamepadButton.DPadLeft, 17 | sdl2.SDLK_RIGHT: GamepadButton.DPadRight, 18 | sdl2.SDLK_e: GamepadButton.Start, 19 | sdl2.SDLK_q: GamepadButton.Back, 20 | sdl2.SDLK_1: GamepadButton.LeftThumbstick, 21 | sdl2.SDLK_2: GamepadButton.RightThumbstick, 22 | sdl2.SDLK_3: GamepadButton.LeftShoulder, 23 | sdl2.SDLK_4: GamepadButton.RightShoulder, 24 | sdl2.SDLK_ESCAPE: GamepadButton.Guide, 25 | sdl2.SDLK_s: GamepadButton.A, 26 | sdl2.SDLK_d: GamepadButton.B, 27 | sdl2.SDLK_a: GamepadButton.X, 28 | sdl2.SDLK_w: GamepadButton.Y 29 | } 30 | 31 | SDL_STATE_MAP = { 32 | sdl2.SDL_KEYDOWN: GamepadButtonState.Pressed, 33 | sdl2.SDL_KEYUP: GamepadButtonState.Released 34 | } 35 | 36 | """ 37 | SDL_AXIS_MAP = { 38 | sdl2.SDL_CONTROLLER_AXIS_TRIGGERLEFT: GamepadAxis.LeftTrigger, 39 | sdl2.SDL_CONTROLLER_AXIS_TRIGGERRIGHT: GamepadAxis.RightTrigger, 40 | sdl2.SDL_CONTROLLER_AXIS_LEFTX: GamepadAxis.LeftThumbstick_X, 41 | sdl2.SDL_CONTROLLER_AXIS_LEFTY: GamepadAxis.LeftThumbstick_Y, 42 | sdl2.SDL_CONTROLLER_AXIS_RIGHTX: GamepadAxis.RightThumbstick_X, 43 | sdl2.SDL_CONTROLLER_AXIS_RIGHTY: GamepadAxis.LeftThumbstick_Y 44 | } 45 | """ 46 | 47 | 48 | class SDLKeyboardInputHandler(InputHandler): 49 | def __init__(self): 50 | self._open = False 51 | super(SDLKeyboardInputHandler, self).__init__() 52 | 53 | def open(self, client): 54 | super(SDLKeyboardInputHandler, self).open(client) 55 | 56 | def _open_controller(self): 57 | self._open = True 58 | controller_index = 0 59 | LOGGER.debug('Opening keyboard as controller {}'.format(controller_index)) 60 | self.client.controller_added(controller_index) 61 | 62 | def pump(self): 63 | for event in sdl2.ext.get_events(): 64 | if event.type == sdl2.SDL_KEYDOWN: 65 | if not self._open: 66 | self._open_controller() 67 | 68 | button = event.key.keysym.sym 69 | gamepad_button = SDL_BUTTON_MAP.get(button, None) 70 | if not gamepad_button: 71 | LOGGER.debug('Input key {} not supported'.format(button)) 72 | break 73 | 74 | self.set_button( 75 | gamepad_button, GamepadButtonState.Pressed 76 | ) 77 | 78 | elif event.type == sdl2.SDL_KEYUP: 79 | button = event.key.keysym.sym 80 | gamepad_button = SDL_BUTTON_MAP.get(button, None) 81 | if not gamepad_button: 82 | LOGGER.debug('Input key {} not supported'.format(button)) 83 | break 84 | 85 | self.set_button( 86 | gamepad_button, GamepadButtonState.Released 87 | ) 88 | 89 | """ 90 | elif event.type == sdl2.SDL_CONTROLLERAXISMOTION: 91 | axis = event.caxis.axis 92 | value = event.caxis.value 93 | self.set_axis(SDL_AXIS_MAP[axis], value) 94 | """ 95 | -------------------------------------------------------------------------------- /xbox/nano/scripts/client_mp.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | import threading 4 | import multiprocessing 5 | from construct import Container 6 | 7 | from xbox.nano.render.client import Client 8 | from xbox.nano.render.sink import Sink 9 | from xbox.nano.render.video.sdl import SDLVideoRenderer 10 | from xbox.nano.render.audio.sdl import SDLAudioRenderer 11 | 12 | 13 | class BlockingClient(Client): 14 | def start_loop(self): 15 | self.loop() 16 | 17 | def loop(self): 18 | self._running = True 19 | while self._running: 20 | self.pump() 21 | 22 | 23 | class PipedSink(Sink): 24 | def __init__(self, pipe): 25 | self.pipe = pipe 26 | 27 | def open(self, client): 28 | # Ignore client for now, only needed for input 29 | self.pipe.send('open') 30 | 31 | def close(self): 32 | self.pipe.send('close') 33 | 34 | def setup(self, fmt): 35 | self.pipe.send(fmt) 36 | 37 | def render(self, data): 38 | self.pipe.send(data) 39 | 40 | 41 | async def protocol_runner(video_pipe, audio_pipe): 42 | from xbox.sg.console import Console 43 | from xbox.nano.manager import NanoManager 44 | from xbox.nano.render.client import Client 45 | 46 | logging.basicConfig(level=logging.DEBUG) 47 | 48 | consoles = await Console.discover(timeout=1) 49 | if len(consoles): 50 | console = consoles[0] 51 | 52 | console.add_manager(NanoManager) 53 | await console.connect() 54 | await console.wait(1) 55 | print('connected') 56 | await console.nano.start_stream() 57 | await console.wait(2) 58 | 59 | client = Client(PipedSink(video_pipe), PipedSink(audio_pipe), Sink()) 60 | await console.nano.start_gamestream(client) 61 | print('stream started') 62 | try: 63 | while True: 64 | await asyncio.sleep(5.0) 65 | except KeyboardInterrupt: 66 | pass 67 | 68 | print('protocol_runner exit') 69 | 70 | 71 | def sink_pump(pipe, sink): 72 | while True: 73 | data = pipe.recv() 74 | if isinstance(data, Container): 75 | sink.setup(data) 76 | elif data == 'open': 77 | # sink.open(None) 78 | pass 79 | elif data == 'close': 80 | sink.close() 81 | else: 82 | sink.render(data) 83 | 84 | 85 | async def async_main(): 86 | logging.basicConfig(level=logging.DEBUG) 87 | 88 | vparent, vchild = multiprocessing.Pipe() 89 | aparent, achild = multiprocessing.Pipe() 90 | protocol_proc = multiprocessing.Process( 91 | target=protocol_runner, args=(vchild, achild) 92 | ) 93 | protocol_proc.start() 94 | 95 | client = BlockingClient( 96 | SDLVideoRenderer(1280, 720), 97 | SDLAudioRenderer(), 98 | Sink() 99 | ) 100 | 101 | video_pumper = threading.Thread( 102 | target=sink_pump, args=(vparent, client.video) 103 | ) 104 | video_pumper.start() 105 | 106 | audio_pumper = threading.Thread( 107 | target=sink_pump, args=(aparent, client.audio) 108 | ) 109 | audio_pumper.start() 110 | 111 | client.open(None) 112 | 113 | 114 | def main(): 115 | loop = asyncio.get_event_loop() 116 | loop.run_until_complete(async_main()) 117 | 118 | 119 | if __name__ == '__main__': 120 | main() 121 | -------------------------------------------------------------------------------- /xbox/nano/enum.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class ChannelClass(Enum): 5 | Video = 'Microsoft::Rdp::Dct::Channel::Class::Video' 6 | Audio = 'Microsoft::Rdp::Dct::Channel::Class::Audio' 7 | ChatAudio = 'Microsoft::Rdp::Dct::Channel::Class::ChatAudio' 8 | Control = 'Microsoft::Rdp::Dct::Channel::Class::Control' 9 | Input = 'Microsoft::Rdp::Dct::Channel::Class::Input' 10 | InputFeedback = 'Microsoft::Rdp::Dct::Channel::Class::Input Feedback' 11 | TcpBase = 'Microsoft::Rdp::Dct::Channel::Class::TcpBase' 12 | 13 | 14 | class RtpPayloadType(Enum): 15 | Streamer = 0x23 16 | Control = 0x60 17 | ChannelControl = 0x61 18 | UDPHandshake = 0x64 19 | 20 | 21 | class ChannelControlPayloadType(Enum): 22 | ClientHandshake = 0x0 # SYN 23 | ServerHandshake = 0x1 # ACK 24 | # Channel requests 25 | ChannelCreate = 0x2 26 | ChannelOpen = 0x3 27 | ChannelClose = 0x4 28 | 29 | 30 | class AudioPayloadType(Enum): 31 | ServerHandshake = 0x1 32 | ClientHandshake = 0x2 33 | Control = 0x3 34 | Data = 0x4 35 | 36 | 37 | class VideoPayloadType(Enum): 38 | ServerHandshake = 0x1 39 | ClientHandshake = 0x2 40 | Control = 0x3 41 | Data = 0x4 42 | 43 | 44 | class InputPayloadType(Enum): 45 | ServerHandshake = 0x1 46 | ClientHandshake = 0x2 47 | FrameAck = 0x3 48 | Frame = 0x4 49 | 50 | 51 | class ControlPayloadType(Enum): 52 | Unknown = 0x0 53 | SessionInit = 0x1 # Unused 54 | SessionCreate = 0x2 55 | SessionCreateResponse = 0x3 56 | SessionDestroy = 0x4 57 | VideoStatistics = 0x5 58 | RealtimeTelemetry = 0x6 59 | ChangeVideoQuality = 0x7 60 | InitiateNetworkTest = 0x8 61 | NetworkInformation = 0x9 62 | NetworkTestResponse = 0xA 63 | ControllerEvent = 0xB 64 | 65 | 66 | class ControllerEvent(Enum): 67 | Removed = 0x0 68 | Added = 0x1 69 | 70 | 71 | class AudioCodec(Enum): 72 | Opus = 0x0 73 | AAC = 0x1 74 | PCM = 0x2 75 | 76 | 77 | class AudioBitDepthType(Enum): 78 | Integer = 0x0 79 | Float = 0x1 80 | 81 | 82 | class VideoCodec(Enum): 83 | H264 = 0x0 84 | YUV = 0x1 # Actually can be IYUV or NV12 based on another field 85 | RGB = 0x2 86 | 87 | 88 | class BroadcastMessageType(Enum): 89 | Unknown = 0x0 90 | StartGameStream = 0x1 91 | StopGameStream = 0x2 92 | GameStreamState = 0x3 93 | GameStreamEnabled = 0x4 94 | GameStreamError = 0x5 95 | Telemetry = 0x6 96 | PreviewStatus = 0x7 97 | 98 | 99 | class VideoQuality(object): 100 | VeryHigh = [12000000, 3, 60000, 1001, 59, 0] 101 | High = [8000000, 2, 60000, 1001, 59, 0] 102 | Middle = [6000002, 2, 60000, 1001, 3600, 0] 103 | Low = [3000001, 1, 30000, 1001, 3600, 0] 104 | 105 | 106 | class GameStreamError(Enum): 107 | Unknown = 0x0 108 | General = 0x1 109 | FailedToInstantiate = 0x2 110 | FailedToInitialize = 0x3 111 | FailedToStart = 0x4 112 | FailedToStop = 0x5 113 | NoController = 0x6 114 | DifferentMsaActive = 0x7 115 | DrmVideo = 0x8 116 | HdcpVideo = 0x9 117 | KinectTitle = 0xA 118 | ProhibitedGame = 0xB 119 | PoorNetworkConnection = 0xC 120 | StreamingDisabled = 0xD 121 | CannotReachConsole = 0xE 122 | GenericError = 0xF 123 | VersionMismatch = 0x10 124 | NoProfile = 0x11 125 | BroadcastInProgress = 0x12 126 | 127 | 128 | class GameStreamState(Enum): 129 | Unknown = 0x0 130 | Initializing = 0x1 131 | Started = 0x2 132 | Stopped = 0x3 133 | Paused = 0x4 134 | 135 | 136 | # class StreamerChannel(Enum): 137 | # Control = 0x0 138 | # Video = 0x1 139 | # Audio = 0x2 140 | # Input = 0x3 141 | -------------------------------------------------------------------------------- /xbox/nano/scripts/pcap.py: -------------------------------------------------------------------------------- 1 | import dpkt 2 | import shutil 3 | import textwrap 4 | import argparse 5 | from xbox.nano import packer 6 | from xbox.nano.channel import Channel 7 | from xbox.nano.enum import RtpPayloadType, ChannelClass, \ 8 | ChannelControlPayloadType 9 | 10 | 11 | channels = { 12 | 0: "None", 13 | 1024: Channel(None, None, 1024, ChannelClass.Video, 0), 14 | 1025: Channel(None, None, 1025, ChannelClass.Audio, 0), 15 | 1026: Channel(None, None, 1026, ChannelClass.ChatAudio, 0), 16 | 1027: Channel(None, None, 1027, ChannelClass.Control, 0), 17 | 1028: Channel(None, None, 1028, ChannelClass.Input, 0), 18 | 1029: Channel(None, None, 1029, ChannelClass.InputFeedback, 0) 19 | } 20 | 21 | 22 | def parse(pcap_file, tcp_port, udp_port): 23 | width = shutil.get_terminal_size().columns 24 | col_width = width // 2 - 3 25 | wrapper = textwrap.TextWrapper(col_width, replace_whitespace=False) 26 | 27 | with open(pcap_file, 'rb') as fh: 28 | for ts, buf in dpkt.pcap.Reader(fh): 29 | eth = dpkt.ethernet.Ethernet(buf) 30 | 31 | # Make sure the Ethernet data contains an IP packet 32 | if not isinstance(eth.data, dpkt.ip.IP): 33 | continue 34 | 35 | ip = eth.data 36 | 37 | if isinstance(ip.data, dpkt.tcp.TCP): 38 | if ip.data.sport != tcp_port and ip.data.dport != tcp_port: 39 | continue 40 | 41 | try: 42 | msgs = packer.unpack_tcp(ip.data.data, channels) 43 | msgs = list(msgs) 44 | except Exception as e: 45 | print("Error: {}".format(e)) 46 | continue 47 | is_client = ip.data.dport == tcp_port 48 | elif isinstance(ip.data, dpkt.udp.UDP): 49 | if ip.data.sport != udp_port and ip.data.dport != udp_port: 50 | continue 51 | 52 | try: 53 | msgs = [packer.unpack(ip.data.data, channels)] 54 | except Exception as e: 55 | print("Error: {}".format(e)) 56 | continue 57 | is_client = ip.data.dport == udp_port 58 | else: 59 | continue 60 | 61 | for msg in msgs: 62 | payload_type = msg.header.flags.payload_type 63 | channel_id = msg.header.ssrc.channel_id 64 | 65 | type_str = '%s Seq %i ' % \ 66 | (RtpPayloadType(payload_type), msg.header.sequence_num) 67 | 68 | if payload_type == RtpPayloadType.ChannelControl: 69 | type_str += ' (%s)' % \ 70 | ChannelControlPayloadType(msg.payload.type) 71 | # elif payload_type == PayloadType.Streamer: 72 | # type_str += ' (Type: %i)' \ 73 | # % msg.payload.streamer_header.packet_type 74 | 75 | if channel_id != 0: 76 | type_str += ' | %s' % channels[channel_id] 77 | 78 | direction = '>' if is_client else '<' 79 | print(' {} '.format(type_str).center(width, direction)) 80 | 81 | lines = str(msg).split('\n') 82 | for line in lines: 83 | line = wrapper.wrap(line) 84 | for i in line: 85 | if is_client: 86 | print('{0: <{1}}'.format(i, col_width), '│') 87 | else: 88 | print( 89 | ' ' * col_width, '│', 90 | '{0}'.format(i, col_width) 91 | ) 92 | 93 | 94 | def main(): 95 | parser = argparse.ArgumentParser( 96 | description='Parse PCAP files and show SG sessions' 97 | ) 98 | parser.add_argument('file', help='Path to PCAP') 99 | parser.add_argument('tcp_port', type=int, help='Server TCP Port (console)') 100 | parser.add_argument('udp_port', type=int, help='Server UDP Port (console)') 101 | args = parser.parse_args() 102 | 103 | parse(args.file, args.tcp_port, args.udp_port) 104 | 105 | 106 | if __name__ == '__main__': 107 | main() 108 | -------------------------------------------------------------------------------- /xbox/nano/packet/message.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | from construct import * 3 | from xbox.nano.enum import RtpPayloadType, ChannelControlPayloadType, ChannelClass 4 | from xbox.sg.utils.struct import XStruct 5 | from xbox.sg.utils.adapters import XSwitch, XEnum, XInject, PrefixedBytes 6 | 7 | 8 | channel_control_handshake = XStruct( 9 | 'type' / XEnum(Int8ul, ChannelControlPayloadType), 10 | 'connection_id' / Int16ul 11 | ) 12 | 13 | 14 | channel_control = XStruct( 15 | 'type' / XEnum(Int32ul, ChannelControlPayloadType), 16 | 'name' / If( 17 | this.type == ChannelControlPayloadType.ChannelCreate, 18 | XEnum(PascalString(Int16ul, 'utf8'), ChannelClass) 19 | ), 20 | 'flags' / XSwitch( 21 | this.type, { 22 | # Flags: For control channel, second Int16ul is protocol version. First doesnt seem to do anything 23 | ChannelControlPayloadType.ChannelCreate: Int32ul, 24 | ChannelControlPayloadType.ChannelOpen: PrefixedBytes(Int32ul), 25 | # Might be reason? 26 | ChannelControlPayloadType.ChannelClose: Int32ul 27 | } 28 | ) 29 | ) 30 | 31 | 32 | udp_handshake = XStruct( 33 | 'unk' / Byte 34 | ) 35 | 36 | 37 | streamer = XStruct( 38 | 'streamer_version' / Int32ul, # Probably 39 | 'sequence_num' / If(this.streamer_version & 1, Int32ul), 40 | 'prev_sequence_num' / If(this.streamer_version & 1, Int32ul), 41 | 'type' / XEnum(Int32ul) 42 | ) 43 | 44 | 45 | # Based on RTP header 46 | header = XStruct( 47 | XInject('from xbox.nano.enum import RtpPayloadType'), 48 | 'flags' / BitStruct( 49 | 'version' / Default(BitsInteger(2), 2), 50 | 'padding' / Default(Flag, False), 51 | 'extension' / Default(Flag, False), 52 | 'csrc_count' / Default(BitsInteger(4), 0), 53 | 'marker' / Default(Flag, False), 54 | 'payload_type' / XEnum(BitsInteger(7), RtpPayloadType) 55 | ), 56 | 'sequence_num' / Default(Int16ub, 0), 57 | 'timestamp' / Default(Int32ub, 0), 58 | 'ssrc' / Struct( 59 | 'connection_id' / Int16ub, 60 | 'channel_id' / Int16ub 61 | ), 62 | 'csrc_list' / Default(Array(this.flags.csrc_count, Int32ub), []), 63 | 'streamer' / If( 64 | this.flags.payload_type == RtpPayloadType.Streamer, 65 | streamer 66 | ) 67 | ) 68 | 69 | 70 | struct = XStruct( 71 | 'header' / header, 72 | 'payload' / XSwitch(this.header.flags.payload_type, { 73 | RtpPayloadType.Control: channel_control_handshake, 74 | RtpPayloadType.ChannelControl: channel_control, 75 | RtpPayloadType.UDPHandshake: udp_handshake, 76 | RtpPayloadType.Streamer: IfThenElse( 77 | this.header.ssrc.connection_id == 0 and this.header.streamer.type == 0, 78 | # ControlStreamer (type: 0) -> No payload length prefixed 79 | GreedyBytes, 80 | PrefixedBytes(Int32ul) 81 | ) 82 | }) 83 | ) 84 | 85 | 86 | # # StreamerMessage Body - W/o Header 87 | # session_init = Struct( 88 | # 'streamer_version' / Int16ub, 89 | # 'control_channel_protocol_version' / Int16ub 90 | # ) 91 | 92 | 93 | # session_create = Struct( 94 | # 'guid' / UUIDAdapter(), 95 | # 'uint1' / Int32ub, 96 | # 'rest' / GreedyBytes # TODO 97 | # ) 98 | 99 | 100 | # session_create_response = Struct( 101 | # 'guid' / UUIDAdapter() 102 | # ) 103 | 104 | 105 | # session_destroy = Struct( 106 | # 'uint1' / Int32ub, 107 | # 'uint2' / Int32ub, 108 | # 'rest' / GreedyBytes # TODO 109 | # ) 110 | 111 | # initiate_network_test = Struct( 112 | # 'guid' / UUIDAdapter() 113 | # ) 114 | 115 | 116 | # network_information = Struct( 117 | # 'guid' / UUIDAdapter(), 118 | # 'ull' / Int64ub, 119 | # 'bool' / Byte, 120 | # 'uint' / Int32ub 121 | # ) 122 | 123 | 124 | # network_test_response = Struct( 125 | # 'guid' / UUIDAdapter(), 126 | # 'uint1' / Int32ub, 127 | # 'float1' / Float32b, 128 | # 'uint2' / Int32ub, 129 | # 'uint3' / Int32ub, 130 | # 'float2' / Float32b, 131 | # 'ull1' / Int64ub, 132 | # 'ull2' / Int64ub, 133 | # 'uint4' / Int32ub 134 | # ) 135 | 136 | 137 | # video_statistics = Struct( 138 | # 'ssrc' / Int32ub, 139 | # 'lost_packets' / Int32ub, 140 | # 'highest_seq_received' / Int32ub, 141 | # 'interarrival_jitter' / Int32ub, 142 | # 'last_sr' / Int32ub, 143 | # 'delay_since_last_sr' / Int32ub, 144 | # ) 145 | -------------------------------------------------------------------------------- /xbox/nano/packer.py: -------------------------------------------------------------------------------- 1 | from construct import Int32ul 2 | from xbox.sg.utils.struct import flatten, XStructObj 3 | from xbox.sg.crypto import ANSIX923Padding 4 | from xbox.nano.packet import message, video, audio, input, control 5 | from xbox.nano.enum import RtpPayloadType, ChannelClass, VideoPayloadType, \ 6 | AudioPayloadType, InputPayloadType 7 | 8 | 9 | STREAMER_TYPE_MAP = { 10 | ChannelClass.Video: VideoPayloadType, 11 | ChannelClass.Audio: AudioPayloadType, 12 | ChannelClass.ChatAudio: AudioPayloadType, 13 | ChannelClass.Input: InputPayloadType, 14 | ChannelClass.InputFeedback: InputPayloadType, 15 | ChannelClass.Control: lambda _: 0 16 | } 17 | 18 | 19 | PAYLOAD_TYPE_MAP = { 20 | ChannelClass.Video: { 21 | VideoPayloadType.ServerHandshake: video.server_handshake, 22 | VideoPayloadType.ClientHandshake: video.client_handshake, 23 | VideoPayloadType.Control: video.control, 24 | VideoPayloadType.Data: video.data 25 | }, 26 | ChannelClass.Audio: { 27 | AudioPayloadType.ServerHandshake: audio.server_handshake, 28 | AudioPayloadType.ClientHandshake: audio.client_handshake, 29 | AudioPayloadType.Control: audio.control, 30 | AudioPayloadType.Data: audio.data 31 | }, 32 | ChannelClass.ChatAudio: { 33 | AudioPayloadType.ServerHandshake: audio.server_handshake, 34 | AudioPayloadType.ClientHandshake: audio.client_handshake, 35 | AudioPayloadType.Control: audio.control, 36 | AudioPayloadType.Data: audio.data 37 | }, 38 | ChannelClass.Input: { 39 | InputPayloadType.ServerHandshake: input.server_handshake, 40 | InputPayloadType.ClientHandshake: input.client_handshake, 41 | InputPayloadType.FrameAck: input.frame_ack, 42 | InputPayloadType.Frame: input.frame 43 | }, 44 | ChannelClass.InputFeedback: { 45 | InputPayloadType.ServerHandshake: input.server_handshake, 46 | InputPayloadType.ClientHandshake: input.client_handshake, 47 | InputPayloadType.FrameAck: input.frame_ack, 48 | InputPayloadType.Frame: input.frame 49 | }, 50 | ChannelClass.Control: { 51 | 0: control.control_packet 52 | } 53 | } 54 | 55 | 56 | def unpack_tcp(buf, channels=None): 57 | while len(buf): 58 | size = Int32ul.parse(buf) 59 | msg, buf = buf[4:size + 4], buf[size + 4:] 60 | yield unpack(msg, channels) 61 | 62 | 63 | def pack_tcp(msgs, channels=None): 64 | buf = b'' 65 | 66 | for msg in msgs: 67 | msg = pack(msg, channels) 68 | buf += Int32ul.build(len(msg)) + msg 69 | 70 | return buf 71 | 72 | 73 | def unpack(buf, channels=None): 74 | # Padding is ignored 75 | msg = message.struct.parse(buf) 76 | 77 | if msg.header.flags.payload_type == RtpPayloadType.Streamer: 78 | payload_struct = _find_channel_payload(msg, channels) 79 | payload = payload_struct.parse(msg.payload) 80 | 81 | return msg(payload=payload) 82 | return msg 83 | 84 | 85 | def pack(msg, channels=None): 86 | container = flatten(msg.container) 87 | 88 | # streamer = b'' 89 | payload = b'' 90 | if msg.header.flags.payload_type == RtpPayloadType.Streamer: 91 | # streamer = msg.subcon.streamer.build(container.streamer, **container) 92 | 93 | if isinstance(msg.payload, XStructObj): 94 | payload = msg.payload.build(**container) 95 | else: 96 | payload_struct = _find_channel_payload(msg, channels) 97 | payload = payload_struct.build(container.payload, **container) 98 | container.payload = payload 99 | 100 | payload = msg.subcon.payload.build(container.payload, **container) 101 | if ANSIX923Padding.size(len(payload), 4) > 0: 102 | msg.container.header.flags.padding = True 103 | payload = ANSIX923Padding.pad(payload, 4) 104 | 105 | buf = msg.subcon.header.build(container.header, **container.header) 106 | # buf += streamer 107 | buf += payload 108 | 109 | return buf 110 | 111 | 112 | def _find_channel_payload(msg, channels): 113 | if not channels: 114 | raise ValueError('No channels passed') 115 | 116 | channel_id = msg.header.ssrc.channel_id 117 | if channel_id not in channels: 118 | raise ValueError('Unknown channel ID %d' % channel_id) 119 | 120 | channel = channels[channel_id] 121 | 122 | if channel.name not in PAYLOAD_TYPE_MAP: 123 | raise ValueError('Unknown channel class %s' % channel.name) 124 | 125 | streamer_type = msg.header.streamer.type 126 | if isinstance(streamer_type, int): 127 | streamer_type = msg.header.streamer.type = STREAMER_TYPE_MAP[channel.name](streamer_type) 128 | 129 | if streamer_type not in PAYLOAD_TYPE_MAP[channel.name]: 130 | raise ValueError('Unknown streamer type %r' % streamer_type) 131 | 132 | return PAYLOAD_TYPE_MAP[channel.name][streamer_type] 133 | -------------------------------------------------------------------------------- /xbox/nano/packet/json.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | from pydantic import BaseModel 3 | from xbox.nano.enum import BroadcastMessageType, GameStreamState 4 | 5 | 6 | class BroadcastJsonError(Exception): 7 | pass 8 | 9 | 10 | class BaseBroadcastMessage(BaseModel): 11 | type: BroadcastMessageType 12 | 13 | class Config: 14 | use_enum_values = True 15 | 16 | 17 | class BroadcastStreamEnabled(BaseBroadcastMessage): 18 | enabled: bool 19 | canBeEnabled: bool 20 | majorProtocolVersion: int 21 | minorProtocolVersion: int 22 | 23 | 24 | class BroadcastPreviewStatus(BaseBroadcastMessage): 25 | isPublicPreview: bool 26 | isInternalPreview: bool 27 | 28 | 29 | class GamestreamConfiguration(BaseModel): 30 | audioFecType: str 31 | audioSyncPolicy: str 32 | audioSyncMaxLatency: str 33 | audioSyncDesiredLatency: str 34 | audioSyncMinLatency: str 35 | audioSyncCompressLatency: str 36 | audioSyncCompressFactor: str 37 | audioSyncLengthenFactor: str 38 | audioBufferLengthHns: str 39 | 40 | enableOpusChatAudio: str 41 | enableDynamicBitrate: str 42 | enableAudioChat: str 43 | enableVideoFrameAcks: str 44 | enableOpusAudio: str 45 | 46 | dynamicBitrateUpdateMs: str 47 | dynamicBitrateScaleFactor: str 48 | 49 | inputReadsPerSecond: str 50 | 51 | videoFecType: str 52 | videoFecLevel: str 53 | videoMaximumWidth: str 54 | videoMaximumHeight: str 55 | videoMaximumFrameRate: str 56 | videoPacketUtilization: str 57 | videoPacketDefragTimeoutMs: str 58 | sendKeyframesOverTCP: str 59 | 60 | udpSubBurstGroups: str 61 | udpBurstDurationMs: str 62 | udpMaxSendPacketsInWinsock: str 63 | 64 | urcpType: str 65 | urcpFixedRate: str 66 | urcpMaximumRate: str 67 | urcpMinimumRate: str 68 | urcpMaximumWindow: str 69 | urcpKeepAliveTimeoutMs: str 70 | 71 | 72 | class BroadcastStartStream(BaseBroadcastMessage): 73 | configuration: GamestreamConfiguration 74 | reQueryPreviewStatus: bool = False 75 | 76 | 77 | class BroadcastStopStream(BaseBroadcastMessage): 78 | pass 79 | 80 | 81 | class BroadcastError(BaseBroadcastMessage): 82 | errorType: int 83 | errorValue: int 84 | 85 | 86 | class BroadcastTelemetry(BaseBroadcastMessage): 87 | pass 88 | 89 | 90 | class BaseBroadcastStateMessage(BaseBroadcastMessage): 91 | state: GameStreamState 92 | sessionId: str 93 | 94 | 95 | class BroadcastStateUnknown(BaseBroadcastStateMessage): 96 | pass 97 | 98 | 99 | class BroadcastStateInitializing(BaseBroadcastStateMessage): 100 | udpPort: int 101 | tcpPort: int 102 | 103 | 104 | class BroadcastStateStarted(BaseBroadcastStateMessage): 105 | isWirelessConnection: bool 106 | wirelessChannel: int 107 | transmitLinkSpeed: int 108 | 109 | 110 | class BroadcastStateStopped(BaseBroadcastStateMessage): 111 | pass 112 | 113 | 114 | class BroadcastStatePaused(BaseBroadcastStateMessage): 115 | pass 116 | 117 | 118 | TYPE_MAP = { 119 | BroadcastMessageType.StartGameStream: BroadcastStartStream, 120 | BroadcastMessageType.StopGameStream: BroadcastStopStream, 121 | BroadcastMessageType.GameStreamState: BaseBroadcastStateMessage, 122 | BroadcastMessageType.GameStreamEnabled: BroadcastStreamEnabled, 123 | BroadcastMessageType.GameStreamError: BroadcastError, 124 | BroadcastMessageType.Telemetry: BroadcastTelemetry, 125 | BroadcastMessageType.PreviewStatus: BroadcastPreviewStatus 126 | } 127 | 128 | STATE_MAP = { 129 | GameStreamState.Unknown: BroadcastStateUnknown, 130 | GameStreamState.Initializing: BroadcastStateInitializing, 131 | GameStreamState.Started: BroadcastStateStarted, 132 | GameStreamState.Stopped: BroadcastStateStopped, 133 | GameStreamState.Paused: BroadcastStatePaused 134 | } 135 | 136 | 137 | def parse( 138 | data: dict 139 | ) -> Union[ 140 | BroadcastStartStream, BroadcastStopStream, BroadcastStreamEnabled, 141 | BroadcastError, BroadcastTelemetry, BroadcastPreviewStatus, 142 | BroadcastStateUnknown, BroadcastStateInitializing, BroadcastStateStarted, 143 | BroadcastStateStopped, BroadcastStatePaused 144 | ]: 145 | msg_type = data.get('type') 146 | try: 147 | msg_type = BroadcastMessageType(msg_type) 148 | except ValueError: 149 | raise BroadcastJsonError( 150 | 'Broadcast message with unknown type: %i - %s' % (msg_type, data) 151 | ) 152 | 153 | if msg_type == BroadcastMessageType.GameStreamState: 154 | state = data.get('state') 155 | try: 156 | state = GameStreamState(state) 157 | return STATE_MAP[state].parse_obj(data) 158 | except ValueError: 159 | raise BroadcastJsonError( 160 | 'Broadcast message with unknown state: %i - %s' % (state, data) 161 | ) 162 | 163 | return TYPE_MAP[msg_type].parse_obj(data) 164 | -------------------------------------------------------------------------------- /tests/test_json.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from xbox.nano.packet import json 3 | from xbox.nano.enum import BroadcastMessageType, GameStreamState 4 | 5 | 6 | def test_invalid_msg_type(): 7 | with pytest.raises(json.BroadcastJsonError): 8 | json.parse(dict(type=42)) 9 | 10 | 11 | def test_invalid_state(): 12 | with pytest.raises(json.BroadcastJsonError): 13 | json.parse( 14 | dict(type=42, state=99) 15 | ) 16 | 17 | 18 | def test_stream_enabled(json_messages): 19 | data = json_messages['broadcast_stream_enabled'] 20 | msg = json.parse(data) 21 | 22 | assert msg.type == BroadcastMessageType.GameStreamEnabled.value 23 | assert msg.enabled is True 24 | assert msg.canBeEnabled is True 25 | assert msg.majorProtocolVersion == 6 26 | assert msg.minorProtocolVersion == 0 27 | 28 | 29 | def test_start_stream(json_messages): 30 | _config = { 31 | "urcpType": "0", 32 | "urcpFixedRate": "-1", 33 | "urcpMaximumWindow": "1310720", 34 | "urcpMinimumRate": "256000", 35 | "urcpMaximumRate": "10000000", 36 | "urcpKeepAliveTimeoutMs": "0", 37 | "audioFecType": "0", 38 | "videoFecType": "0", 39 | "videoFecLevel": "3", 40 | "videoPacketUtilization": "0", 41 | "enableDynamicBitrate": "false", 42 | "dynamicBitrateScaleFactor": "1", 43 | "dynamicBitrateUpdateMs": "5000", 44 | "sendKeyframesOverTCP": "false", 45 | "videoMaximumWidth": "1280", 46 | "videoMaximumHeight": "720", 47 | "videoMaximumFrameRate": "60", 48 | "videoPacketDefragTimeoutMs": "16", 49 | "enableVideoFrameAcks": "false", 50 | "enableAudioChat": "true", 51 | "audioBufferLengthHns": "10000000", 52 | "audioSyncPolicy": "1", 53 | "audioSyncMinLatency": "10", 54 | "audioSyncDesiredLatency": "40", 55 | "audioSyncMaxLatency": "170", 56 | "audioSyncCompressLatency": "100", 57 | "audioSyncCompressFactor": "0.99", 58 | "audioSyncLengthenFactor": "1.01", 59 | "enableOpusAudio": "false", 60 | "enableOpusChatAudio": "true", 61 | "inputReadsPerSecond": "120", 62 | "udpMaxSendPacketsInWinsock": "250", 63 | "udpSubBurstGroups": "5", 64 | "udpBurstDurationMs": "11" 65 | } 66 | 67 | data = json_messages['broadcast_start_stream'] 68 | msg = json.parse(data) 69 | 70 | assert msg.type == BroadcastMessageType.StartGameStream.value 71 | assert msg.reQueryPreviewStatus is True 72 | assert msg.configuration == _config 73 | 74 | 75 | @pytest.mark.skip() 76 | def test_stop_stream(json_messages): 77 | data = json_messages['broadcast_stop_stream'] 78 | msg = json.parse(data) 79 | 80 | assert msg.type == BroadcastMessageType.StopGameStream.value 81 | 82 | 83 | def test_state_unknown(json_messages): 84 | data = json_messages['broadcast_state_unknown'] 85 | msg = json.parse(data) 86 | 87 | assert msg.type == BroadcastMessageType.GameStreamState.value 88 | assert msg.state == GameStreamState.Unknown.value 89 | assert msg.sessionId == '' 90 | 91 | 92 | def test_state_init(json_messages): 93 | data = json_messages['broadcast_state_init'] 94 | msg = json.parse(data) 95 | 96 | assert msg.type == BroadcastMessageType.GameStreamState.value 97 | assert msg.state == GameStreamState.Initializing.value 98 | assert msg.sessionId == '{14608F3C-1C4A-4F32-9DA6-179CE1001E4A}' 99 | assert msg.udpPort == 49665 100 | assert msg.tcpPort == 53394 101 | 102 | 103 | def test_state_started(json_messages): 104 | data = json_messages['broadcast_state_started'] 105 | msg = json.parse(data) 106 | 107 | assert msg.type == BroadcastMessageType.GameStreamState.value 108 | assert msg.state == GameStreamState.Started.value 109 | assert msg.sessionId == '{14608F3C-1C4A-4F32-9DA6-179CE1001E4A}' 110 | assert msg.isWirelessConnection is False 111 | assert msg.wirelessChannel == 0 112 | assert msg.transmitLinkSpeed == 1000000000 113 | 114 | 115 | def test_state_stopped(json_messages): 116 | data = json_messages['broadcast_state_stopped'] 117 | msg = json.parse(data) 118 | 119 | assert msg.type == BroadcastMessageType.GameStreamState.value 120 | assert msg.state == GameStreamState.Stopped.value 121 | assert msg.sessionId == '{14608F3C-1C4A-4F32-9DA6-179CE1001E4A}' 122 | 123 | 124 | @pytest.mark.skip() 125 | def test_error(json_messages): 126 | data = json_messages['broadcast_error'] 127 | msg = json.parse(data) 128 | 129 | assert msg.type == BroadcastMessageType.GameStreamError.value 130 | 131 | 132 | @pytest.mark.skip() 133 | def test_telemetry(json_messages): 134 | data = json_messages['broadcast_telemetry'] 135 | msg = json.parse(data) 136 | 137 | assert msg.type == BroadcastMessageType.Telemetry.value 138 | 139 | 140 | def test_previewstatus(json_messages): 141 | data = json_messages['broadcast_previewstatus'] 142 | msg = json.parse(data) 143 | 144 | assert msg.type == BroadcastMessageType.PreviewStatus.value 145 | assert msg.isPublicPreview is False 146 | assert msg.isInternalPreview is False 147 | -------------------------------------------------------------------------------- /xbox/nano/render/input/sdl.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | 4 | import sdl2 5 | import sdl2.ext 6 | 7 | from xbox.nano.render.input.base import InputHandler, InputError, \ 8 | GamepadButton, GamepadButtonState, GamepadAxis 9 | 10 | log = logging.getLogger(__name__) 11 | 12 | 13 | SDL_BUTTON_MAP = { 14 | sdl2.SDL_CONTROLLER_BUTTON_DPAD_UP: GamepadButton.DPadUp, 15 | sdl2.SDL_CONTROLLER_BUTTON_DPAD_DOWN: GamepadButton.DPadDown, 16 | sdl2.SDL_CONTROLLER_BUTTON_DPAD_LEFT: GamepadButton.DPadLeft, 17 | sdl2.SDL_CONTROLLER_BUTTON_DPAD_RIGHT: GamepadButton.DPadRight, 18 | sdl2.SDL_CONTROLLER_BUTTON_START: GamepadButton.Start, 19 | sdl2.SDL_CONTROLLER_BUTTON_BACK: GamepadButton.Back, 20 | sdl2.SDL_CONTROLLER_BUTTON_LEFTSTICK: GamepadButton.LeftThumbstick, 21 | sdl2.SDL_CONTROLLER_BUTTON_RIGHTSTICK: GamepadButton.RightThumbstick, 22 | sdl2.SDL_CONTROLLER_BUTTON_LEFTSHOULDER: GamepadButton.LeftShoulder, 23 | sdl2.SDL_CONTROLLER_BUTTON_RIGHTSHOULDER: GamepadButton.RightShoulder, 24 | sdl2.SDL_CONTROLLER_BUTTON_GUIDE: GamepadButton.Guide, 25 | sdl2.SDL_CONTROLLER_BUTTON_INVALID: GamepadButton.Unknown, 26 | sdl2.SDL_CONTROLLER_BUTTON_A: GamepadButton.A, 27 | sdl2.SDL_CONTROLLER_BUTTON_B: GamepadButton.B, 28 | sdl2.SDL_CONTROLLER_BUTTON_X: GamepadButton.X, 29 | sdl2.SDL_CONTROLLER_BUTTON_Y: GamepadButton.Y 30 | } 31 | 32 | SDL_AXIS_MAP = { 33 | sdl2.SDL_CONTROLLER_AXIS_TRIGGERLEFT: GamepadAxis.LeftTrigger, 34 | sdl2.SDL_CONTROLLER_AXIS_TRIGGERRIGHT: GamepadAxis.RightTrigger, 35 | sdl2.SDL_CONTROLLER_AXIS_LEFTX: GamepadAxis.LeftThumbstick_X, 36 | sdl2.SDL_CONTROLLER_AXIS_LEFTY: GamepadAxis.LeftThumbstick_Y, 37 | sdl2.SDL_CONTROLLER_AXIS_RIGHTX: GamepadAxis.RightThumbstick_X, 38 | sdl2.SDL_CONTROLLER_AXIS_RIGHTY: GamepadAxis.RightThumbstick_Y 39 | } 40 | 41 | SDL_STATE_MAP = { 42 | sdl2.SDL_CONTROLLERBUTTONDOWN: GamepadButtonState.Pressed, 43 | sdl2.SDL_CONTROLLERBUTTONUP: GamepadButtonState.Released 44 | } 45 | 46 | 47 | class SDLInputHandler(InputHandler): 48 | def __init__(self): 49 | super(SDLInputHandler, self).__init__() 50 | 51 | sdl2.SDL_InitSubSystem(sdl2.SDL_INIT_GAMECONTROLLER) 52 | ret = sdl2.SDL_GameControllerAddMappingsFromFile( 53 | os.path.join( 54 | os.path.dirname(__file__), 'controller_db.txt' 55 | ).encode('utf-8') 56 | ) 57 | 58 | if ret == -1: 59 | raise InputError( 60 | "Failed to load GameControllerDB, %s", sdl2.SDL_GetError() 61 | ) 62 | 63 | def open(self, client): 64 | super(SDLInputHandler, self).open(client) 65 | # Enumerate already plugged controllers 66 | for i in range(sdl2.SDL_NumJoysticks()): 67 | if sdl2.SDL_IsGameController(i): 68 | if sdl2.SDL_GameControllerOpen(i): 69 | log.info("Opened controller: %i", i) 70 | self.client.controller_added(i) 71 | else: 72 | log.error("Unable to open controller: %i", i) 73 | else: 74 | log.error("Not a gamecontroller: %i", i) 75 | 76 | def pump(self): 77 | for event in sdl2.ext.get_events(): 78 | if event.type == sdl2.SDL_CONTROLLERDEVICEADDED: 79 | controller = event.cdevice.which 80 | log.debug('Controller added: %i' % controller) 81 | self.controller_added(controller) 82 | sdl2.SDL_GameControllerOpen(event.cdevice.which) 83 | 84 | elif event.type == sdl2.SDL_CONTROLLERDEVICEREMOVED: 85 | controller = event.cdevice.which 86 | log.debug('Controller removed: %i' % controller) 87 | self.controller_removed(controller) 88 | # sdl2.SDL_GameControllerClose(event.cdevice.which) 89 | 90 | elif event.type == sdl2.SDL_CONTROLLERBUTTONDOWN: 91 | button = event.cbutton.button 92 | self.set_button( 93 | SDL_BUTTON_MAP[button], GamepadButtonState.Pressed 94 | ) 95 | 96 | elif event.type == sdl2.SDL_CONTROLLERBUTTONUP: 97 | button = event.cbutton.button 98 | self.set_button( 99 | SDL_BUTTON_MAP[button], GamepadButtonState.Released 100 | ) 101 | 102 | elif event.type == sdl2.SDL_CONTROLLERAXISMOTION: 103 | axis = event.caxis.axis 104 | value = event.caxis.value 105 | if axis in (sdl2.SDL_CONTROLLER_AXIS_LEFTY, sdl2.SDL_CONTROLLER_AXIS_RIGHTY): 106 | value = -value 107 | if axis in (sdl2.SDL_CONTROLLER_AXIS_LEFTX, sdl2.SDL_CONTROLLER_AXIS_LEFTY, sdl2.SDL_CONTROLLER_AXIS_RIGHTX, sdl2.SDL_CONTROLLER_AXIS_RIGHTY): 108 | if value >= 32768: 109 | value = 32767 110 | elif value <= -32768: 111 | value = -32768 112 | if axis in (sdl2.SDL_CONTROLLER_AXIS_TRIGGERLEFT, sdl2.SDL_CONTROLLER_AXIS_TRIGGERRIGHT): 113 | value = int(value / 32767 * 255) 114 | 115 | self.set_axis(SDL_AXIS_MAP[axis], value) 116 | -------------------------------------------------------------------------------- /xbox/nano/render/input/base.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from enum import Enum 3 | from datetime import datetime 4 | 5 | from xbox.nano.render.sink import Sink 6 | from xbox.nano.packet.input import frame, input_frame_buttons, input_frame_analog, input_frame_extension 7 | 8 | log = logging.getLogger(__name__) 9 | 10 | 11 | class GamepadButtonState(Enum): 12 | Pressed = 1 13 | Released = 2 14 | 15 | 16 | class GamepadButton(Enum): 17 | DPadUp = 1 18 | DPadDown = 2 19 | DPadLeft = 3 20 | DPadRight = 4 21 | Start = 5 22 | Back = 6 23 | LeftThumbstick = 7 24 | RightThumbstick = 8 25 | LeftShoulder = 9 26 | RightShoulder = 10 27 | Guide = 11 28 | Unknown = 12 29 | A = 13 30 | B = 14 31 | X = 15 32 | Y = 16 33 | 34 | 35 | class GamepadAxis(Enum): 36 | LeftTrigger = 20 37 | RightTrigger = 21 38 | LeftThumbstick_X = 22 39 | LeftThumbstick_Y = 23 40 | RightThumbstick_X = 24 41 | RightThumbstick_Y = 25 42 | 43 | 44 | class GamepadFeedback(Enum): 45 | LeftTriggerRumble = 30 46 | RightTriggerRumble = 31 47 | LeftHandleRumble = 32 48 | RightHandleRumble = 33 49 | 50 | 51 | FRAME_MAPPING_BUTTONS = { 52 | GamepadButton.DPadUp: 'dpad_up', 53 | GamepadButton.DPadDown: 'dpad_down', 54 | GamepadButton.DPadLeft: 'dpad_left', 55 | GamepadButton.DPadRight: 'dpad_right', 56 | GamepadButton.Start: 'start', 57 | GamepadButton.Back: 'back', 58 | GamepadButton.LeftThumbstick: 'left_thumbstick', 59 | GamepadButton.RightThumbstick: 'right_thumbstick', 60 | GamepadButton.LeftShoulder: 'left_shoulder', 61 | GamepadButton.RightShoulder: 'right_shoulder', 62 | GamepadButton.Guide: 'guide', 63 | GamepadButton.Unknown: 'unknown', 64 | GamepadButton.A: 'a', 65 | GamepadButton.B: 'b', 66 | GamepadButton.X: 'x', 67 | GamepadButton.Y: 'y', 68 | } 69 | 70 | FRAME_MAPPING_ANALOG = { 71 | GamepadAxis.LeftTrigger: 'left_trigger', 72 | GamepadAxis.RightTrigger: 'right_trigger', 73 | GamepadAxis.LeftThumbstick_X: 'left_thumb_x', 74 | GamepadAxis.LeftThumbstick_Y: 'left_thumb_y', 75 | GamepadAxis.RightThumbstick_X: 'right_thumb_x', 76 | GamepadAxis.RightThumbstick_Y: 'right_thumb_y', 77 | 78 | GamepadFeedback.LeftTriggerRumble: 'rumble_trigger_l', 79 | GamepadFeedback.RightTriggerRumble: 'rumble_trigger_r', 80 | GamepadFeedback.LeftHandleRumble: 'rumble_handle_l', 81 | GamepadFeedback.RightHandleRumble: 'rumble_handle_r' 82 | } 83 | 84 | 85 | class InputError(Exception): 86 | pass 87 | 88 | 89 | class InputHandler(Sink): 90 | def __init__(self): 91 | self.client = None 92 | 93 | # Cache for button states 94 | self._button_states = {k: 0 for k in FRAME_MAPPING_BUTTONS.values()} 95 | self._analog_states = {k: 0 for k in FRAME_MAPPING_ANALOG.values()} 96 | 97 | def open(self, client): 98 | """ 99 | Initialize the input handler with a NanoClient instance 100 | 101 | Args: 102 | client (:class:`xbox.nano.protocol.NanoProtocol`): Instance of :class:`NanoProtocol` 103 | 104 | Returns: 105 | None 106 | """ 107 | self.client = client 108 | 109 | def send_frame(self): 110 | packet = frame( 111 | buttons=input_frame_buttons(**self._button_states).container, 112 | analog=input_frame_analog(**self._analog_states).container, 113 | extension=input_frame_extension(**dict(byte_6=1)).container 114 | ) 115 | self.client.send_input(packet, datetime.utcnow()) 116 | 117 | def controller_added(self, controller_index): 118 | self.client.controller_added(controller_index) 119 | 120 | def controller_removed(self, controller_index): 121 | self.client.controller_removed(controller_index) 122 | 123 | def set_button(self, button, state): 124 | """ 125 | Set controller button state 126 | 127 | Args: 128 | button (:class:`GamepadButton`): Member of :class:`GamepadButton` 129 | state (:class:`GamepadButtonState`): Member of :class:`GamepadButtonState` 130 | 131 | Returns: 132 | None 133 | """ 134 | field_name = FRAME_MAPPING_BUTTONS[button] 135 | current_val = self._button_states[field_name] 136 | 137 | if current_val == 0 or (current_val % 2) == 0: 138 | current_state = GamepadButtonState.Released 139 | else: 140 | current_state = GamepadButtonState.Pressed 141 | 142 | if current_state == state: 143 | return 144 | 145 | log.debug('Button: %s - %s' % ( 146 | GamepadButton(button), GamepadButtonState(state) 147 | )) 148 | 149 | # Cache button state 150 | self._button_states[field_name] = current_val + 1 151 | self.send_frame() 152 | 153 | def set_axis(self, axis, value): 154 | """ 155 | Set controller analog axis value 156 | 157 | Args: 158 | axis (:class:`GamepadAxis`): Member of :class:`GamepadAxis` 159 | value (int): Axis position 160 | 161 | Returns: 162 | None 163 | """ 164 | field_name = FRAME_MAPPING_ANALOG[axis] 165 | 166 | log.debug('Axis move: %s - Value: %i' % (GamepadAxis(axis), value)) 167 | 168 | self._analog_states[field_name] = value 169 | self.send_frame() 170 | -------------------------------------------------------------------------------- /xbox/nano/render/client/gst.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import asyncio 3 | from typing import Optional 4 | from asyncio import Queue 5 | 6 | import gi 7 | from gi.repository import Gst, GObject, GLib 8 | 9 | from xbox.nano.render.client.base import Client 10 | from xbox.nano.render.audio.aac import AACFrame, AACProfile 11 | 12 | gi.require_version('Gst', '1.0') 13 | log = logging.getLogger(__name__) 14 | 15 | 16 | class GstClient(Client): 17 | def __init__(self): 18 | self.protocol = None 19 | 20 | self._running = False 21 | self._loop_task: Optional[asyncio.Task] = None 22 | 23 | self._video_frames = Queue() 24 | self._audio_frames = Queue() 25 | 26 | GObject.threads_init() 27 | Gst.init(None) 28 | 29 | self.pipeline = Gst.Pipeline.new("nanostream") 30 | 31 | self.bus = self.pipeline.get_bus() 32 | self.bus.add_signal_watch() 33 | self.bus.connect("message", self.on_message) 34 | 35 | # stream-type=stream should set appsrc to PUSH mode 36 | video_pipeline = " ! ".join([ 37 | "appsrc name=videosrc stream-type=stream", 38 | "decodebin", 39 | "videoconvert", 40 | "queue", 41 | "autovideosink" 42 | ]) 43 | 44 | audio_pipeline = " ! ".join([ 45 | "appsrc name=audiosrc stream-type=stream", 46 | "decodebin", 47 | "audioconvert", 48 | "queue", 49 | "autoaudiosink" 50 | ]) 51 | 52 | pipeline_string = video_pipeline + " " + audio_pipeline 53 | log.debug("Gst Pipeline: %s" % pipeline_string) 54 | self.pipeline = Gst.parse_launch(pipeline_string) 55 | 56 | self.a_src = self.pipeline.get_by_name("audiosrc") 57 | self.v_src = self.pipeline.get_by_name("videosrc") 58 | 59 | self.a_src.connect('need-data', self.need_data) 60 | self.v_src.connect('need-data', self.need_data) 61 | self.a_src.connect('enough-data', self.enough_data) 62 | self.v_src.connect('enough-data', self.enough_data) 63 | 64 | ret = self.pipeline.set_state(Gst.State.PLAYING) 65 | if ret == Gst.StateChangeReturn.FAILURE: 66 | raise Exception("Unable to set the pipeline to the playing state") 67 | 68 | self.context = GLib.MainContext.default() 69 | 70 | super(GstClient, self).__init__(None, None, None) 71 | 72 | def open(self, protocol): 73 | log.debug('Opening client') 74 | self.protocol = protocol 75 | self.start_loop() 76 | 77 | def close(self): 78 | self._running = False 79 | 80 | def start_loop(self): 81 | self._loop_task = asyncio.create_task(self.loop()) 82 | 83 | async def loop(self): 84 | self._running = True 85 | while self._running: 86 | self.pump() 87 | await asyncio.sleep(0.0) 88 | 89 | def pump(self): 90 | self.context.iteration(may_block=False) 91 | 92 | def on_message(self, bus, message): 93 | struct = message.get_structure() 94 | 95 | if message.type == Gst.MessageType.EOS: 96 | log.debug('EOS') 97 | 98 | elif message.type == Gst.MessageType.TAG and message.parse_tag() and struct.has_field('taglist'): 99 | log.debug('TAG') 100 | taglist = struct.get_value('taglist') 101 | for x in range(taglist.n_tags()): 102 | name = taglist.nth_tag_name(x) 103 | log.debug(' %s: %s' % (name, taglist.get_string(name)[1])) 104 | 105 | elif message.type == Gst.MessageType.ERROR: 106 | err, debug = message.parse_error() 107 | log.error("Error: %s" % err, debug) 108 | 109 | elif message.type == Gst.MessageType.WARNING: 110 | err, debug = message.parse_warning() 111 | log.warning("Warning: %s" % err, debug) 112 | 113 | elif message.type == Gst.MessageType.STATE_CHANGED: 114 | old_state, new_state, pending_state = message.parse_state_changed() 115 | if message.src == self.pipeline: 116 | log.debug("Pipeline state changed from '{0:s}' to '{1:s}'".format( 117 | Gst.Element.state_get_name(old_state), 118 | Gst.Element.state_get_name(new_state))) 119 | 120 | else: 121 | log.error('Unhandled messagetype: %s' % message.type) 122 | 123 | def need_data(self, src, length): 124 | if src == self.v_src: 125 | frame_buf = self._video_frames 126 | elif src == self.a_src: 127 | frame_buf = self._audio_frames 128 | else: 129 | raise Exception('src %s is not handled' % src.get_name()) 130 | 131 | # log.debug('%s needs %i bytes of data' % (src.get_name(), length)) 132 | try: 133 | data = frame_buf.get_nowait() 134 | except asyncio.QueueEmpty as e: 135 | # log.debug('%s Queue is empty %s' % (src.get_name(), e)) 136 | # FIXME: Passing an empty buffer produces: 137 | # FIXME: GStreamer-CRITICAL **: gst_memory_get_sizes: assertion 'mem != NULL' failed 138 | data = b'' 139 | 140 | buf = Gst.Buffer.new_wrapped(data) 141 | src.emit("push-buffer", buf) 142 | 143 | def enough_data(self, src): 144 | log.debug('Enough data for %s' % src.get_name()) 145 | 146 | def set_video_format(self, video_fmt): 147 | pass 148 | 149 | def set_audio_format(self, audio_fmt): 150 | pass 151 | 152 | def render_video(self, data): 153 | self._video_frames.put(data) 154 | 155 | def render_audio(self, data): 156 | data = AACFrame.generate_header(len(data), AACProfile.Main, 48000, 2) + data 157 | self._audio_frames.put(data) 158 | 159 | def send_input(self, frame, timestamp): 160 | pass 161 | 162 | def controller_added(self, controller_index): 163 | pass 164 | 165 | def controller_removed(self, controller_index): 166 | pass 167 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Configuration file for the Sphinx documentation builder. 4 | # 5 | # This file does only contain a selection of the most common options. For a 6 | # full list see the documentation: 7 | # http://www.sphinx-doc.org/en/stable/config 8 | 9 | # -- Path setup -------------------------------------------------------------- 10 | 11 | # If extensions (or modules to document with autodoc) are in another directory, 12 | # add these directories to sys.path here. If the directory is relative to the 13 | # documentation root, use os.path.abspath to make it absolute, like shown here. 14 | # 15 | # import os 16 | # import sys 17 | # sys.path.insert(0, os.path.abspath('.')) 18 | import os 19 | import sys 20 | sys.path.insert(0, os.path.abspath('../')) 21 | 22 | # -- Project information ----------------------------------------------------- 23 | 24 | project = 'Xbox-Smartglass-Nano' 25 | copyright = '2018, OpenXbox' 26 | author = 'OpenXbox' 27 | 28 | # The short X.Y version 29 | version = '1.0' 30 | # The full version, including alpha/beta/rc tags 31 | release = '0.10.1' 32 | 33 | 34 | # -- General configuration --------------------------------------------------- 35 | 36 | # If your documentation needs a minimal Sphinx version, state it here. 37 | # 38 | # needs_sphinx = '1.0' 39 | 40 | # Add any Sphinx extension module names here, as strings. They can be 41 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 42 | # ones. 43 | extensions = [ 44 | 'sphinx.ext.autodoc', 45 | 'sphinx.ext.intersphinx', 46 | 'sphinx.ext.napoleon', 47 | 'recommonmark' 48 | ] 49 | 50 | # Add any paths that contain templates here, relative to this directory. 51 | templates_path = ['_templates'] 52 | 53 | # The suffix(es) of source filenames. 54 | # You can specify multiple suffix as a list of string: 55 | # 56 | source_suffix = ['.rst', '.md'] 57 | 58 | # The master toctree document. 59 | master_doc = 'index' 60 | 61 | # The language for content autogenerated by Sphinx. Refer to documentation 62 | # for a list of supported languages. 63 | # 64 | # This is also used if you do content translation via gettext catalogs. 65 | # Usually you set "language" from the command line for these cases. 66 | language = None 67 | 68 | # List of patterns, relative to source directory, that match files and 69 | # directories to ignore when looking for source files. 70 | # This pattern also affects html_static_path and html_extra_path . 71 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 72 | 73 | # The name of the Pygments (syntax highlighting) style to use. 74 | pygments_style = 'sphinx' 75 | 76 | 77 | # -- Options for HTML output ------------------------------------------------- 78 | 79 | # The theme to use for HTML and HTML Help pages. See the documentation for 80 | # a list of builtin themes. 81 | # 82 | html_theme = 'sphinx_rtd_theme' 83 | 84 | # Theme options are theme-specific and customize the look and feel of a theme 85 | # further. For a list of options available for each theme, see the 86 | # documentation. 87 | # 88 | # html_theme_options = {} 89 | 90 | # Add any paths that contain custom static files (such as style sheets) here, 91 | # relative to this directory. They are copied after the builtin static files, 92 | # so a file named "default.css" will overwrite the builtin "default.css". 93 | html_static_path = ['_static'] 94 | 95 | # Custom sidebar templates, must be a dictionary that maps document names 96 | # to template names. 97 | # 98 | # The default sidebars (for documents that don't match any pattern) are 99 | # defined by theme itself. Builtin themes are using these templates by 100 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 101 | # 'searchbox.html']``. 102 | # 103 | # html_sidebars = {} 104 | 105 | 106 | # -- Options for HTMLHelp output --------------------------------------------- 107 | 108 | # Output file base name for HTML help builder. 109 | htmlhelp_basename = 'Xbox-Smartglass-Nanodoc' 110 | 111 | 112 | # -- Options for LaTeX output ------------------------------------------------ 113 | 114 | latex_elements = { 115 | # The paper size ('letterpaper' or 'a4paper'). 116 | # 117 | # 'papersize': 'letterpaper', 118 | 119 | # The font size ('10pt', '11pt' or '12pt'). 120 | # 121 | # 'pointsize': '10pt', 122 | 123 | # Additional stuff for the LaTeX preamble. 124 | # 125 | # 'preamble': '', 126 | 127 | # Latex figure (float) alignment 128 | # 129 | # 'figure_align': 'htbp', 130 | } 131 | 132 | # Grouping the document tree into LaTeX files. List of tuples 133 | # (source start file, target name, title, 134 | # author, documentclass [howto, manual, or own class]). 135 | latex_documents = [ 136 | (master_doc, 'Xbox-Smartglass-Nano.tex', 'Xbox-Smartglass-Nano Documentation', 137 | 'OpenXbox', 'manual'), 138 | ] 139 | 140 | 141 | # -- Options for manual page output ------------------------------------------ 142 | 143 | # One entry per manual page. List of tuples 144 | # (source start file, name, description, authors, manual section). 145 | man_pages = [ 146 | (master_doc, 'xbox-smartglass-nano', 'Xbox-Smartglass-Nano Documentation', 147 | [author], 1) 148 | ] 149 | 150 | 151 | # -- Options for Texinfo output ---------------------------------------------- 152 | 153 | # Grouping the document tree into Texinfo files. List of tuples 154 | # (source start file, target name, title, author, 155 | # dir menu entry, description, category) 156 | texinfo_documents = [ 157 | (master_doc, 'Xbox-Smartglass-Nano', 'Xbox-Smartglass-Nano Documentation', 158 | author, 'Xbox-Smartglass-Nano', 'One line description of project.', 159 | 'Miscellaneous'), 160 | ] 161 | 162 | 163 | # -- Extension configuration ------------------------------------------------- 164 | 165 | # -- Options for intersphinx extension --------------------------------------- 166 | 167 | # Example configuration for intersphinx: refer to the Python standard library. 168 | intersphinx_mapping = {'https://docs.python.org/': None} 169 | 170 | # -- Options for napoleon extension --------------------------------------- 171 | napoleon_google_docstring = True 172 | napoleon_numpy_docstring = True 173 | napoleon_include_init_with_doc = True 174 | napoleon_include_private_with_doc = True 175 | 176 | # -- Autodoc settings 177 | autodoc_member_order = 'bysource' 178 | -------------------------------------------------------------------------------- /xbox/nano/manager.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from xbox.sg.manager import Manager 3 | from xbox.sg.enum import ServiceChannel 4 | from xbox.sg.utils.events import Event 5 | 6 | from xbox.nano.packet import json 7 | from xbox.nano.protocol import NanoProtocol 8 | from xbox.nano.enum import GameStreamState, BroadcastMessageType 9 | 10 | log = logging.getLogger(__name__) 11 | 12 | DEFAULT_CONFIG = { 13 | "audioFecType": "0", 14 | "audioSyncPolicy": "1", 15 | "audioSyncMaxLatency": "170", 16 | "audioSyncDesiredLatency": "40", 17 | "audioSyncMinLatency": "10", 18 | "audioSyncCompressLatency": "100", 19 | "audioSyncCompressFactor": "0.99", 20 | "audioSyncLengthenFactor": "1.01", 21 | "audioBufferLengthHns": "10000000", 22 | 23 | "enableOpusChatAudio": "true", 24 | "enableDynamicBitrate": "false", 25 | "enableAudioChat": "true", 26 | "enableVideoFrameAcks": "false", 27 | "enableOpusAudio": "false", 28 | 29 | "dynamicBitrateUpdateMs": "5000", 30 | "dynamicBitrateScaleFactor": "1", 31 | 32 | "inputReadsPerSecond": "120", 33 | 34 | "videoFecType": "0", 35 | "videoFecLevel": "3", 36 | "videoMaximumWidth": "1280", 37 | "videoMaximumHeight": "720", 38 | "videoMaximumFrameRate": "60", 39 | "videoPacketUtilization": "0", 40 | "videoPacketDefragTimeoutMs": "16", 41 | "sendKeyframesOverTCP": "false", 42 | 43 | "udpSubBurstGroups": "5", 44 | "udpBurstDurationMs": "11", 45 | "udpMaxSendPacketsInWinsock": "250", 46 | 47 | "urcpType": "0", 48 | "urcpFixedRate": "-1", 49 | "urcpMaximumRate": "10000000", 50 | "urcpMinimumRate": "256000", 51 | "urcpMaximumWindow": "1310720", 52 | "urcpKeepAliveTimeoutMs": "0" 53 | } 54 | 55 | 56 | class NanoManagerError(Exception): 57 | pass 58 | 59 | 60 | class NanoManager(Manager): 61 | __namespace__ = 'nano' 62 | PROTOCOL_MAJOR_VERSION = 6 63 | PROTOCOL_MINOR_VERSION = 0 64 | 65 | def __init__(self, console): 66 | super(NanoManager, self).__init__( 67 | console, ServiceChannel.SystemBroadcast 68 | ) 69 | 70 | self.client = None 71 | self._protocol = None 72 | self._connected = False 73 | self._current_state = GameStreamState.Unknown 74 | 75 | self.on_gamestream_error = Event() 76 | 77 | self._stream_states = {} 78 | self._stream_enabled = None 79 | self._stream_error = None 80 | self._stream_telemetry = None 81 | self._stream_previewstatus = None 82 | 83 | async def start_stream(self, config: dict = DEFAULT_CONFIG): 84 | msg = json.BroadcastStartStream( 85 | type=BroadcastMessageType.StartGameStream, 86 | reQueryPreviewStatus=True, 87 | configuration=config 88 | ) 89 | await self._send_json(msg.dict()) 90 | 91 | async def stop_stream(self): 92 | if self._connected and self._protocol: 93 | self._protocol.disconnect() 94 | self._protocol.stop() 95 | self._connected = False 96 | 97 | msg = json.BroadcastStopStream( 98 | type=BroadcastMessageType.StopGameStream 99 | ) 100 | await self._send_json(msg.dict()) 101 | 102 | async def start_gamestream(self, client): 103 | if not self.streaming: 104 | raise NanoManagerError('start_gamestream: Connection params not ready') 105 | 106 | self._protocol = NanoProtocol( 107 | client, self.console.address, self.session_id, self.tcp_port, self.udp_port 108 | ) 109 | await self._protocol.start() 110 | await self._protocol.connect() 111 | self._connected = True 112 | 113 | def _on_json(self, data, service_channel): 114 | msg = json.parse(data) 115 | 116 | # Convert integer representation to BroadcastMessageType enum 117 | msg_type = BroadcastMessageType(msg.type) 118 | 119 | if msg_type == BroadcastMessageType.GameStreamState: 120 | # Convert integer representation to GameStreamState enum 121 | msg_state = GameStreamState(msg.state) 122 | if msg_state in [GameStreamState.Stopped, GameStreamState.Unknown]: 123 | # Clear previously received states 124 | self._stream_states = {} 125 | 126 | self._stream_states[msg_state] = msg 127 | self._current_state = msg_state 128 | elif msg_type == BroadcastMessageType.GameStreamEnabled: 129 | self._stream_enabled = msg 130 | elif msg_type == BroadcastMessageType.PreviewStatus: 131 | self._stream_previewstatus = msg 132 | elif msg_type == BroadcastMessageType.Telemetry: 133 | self._stream_telemetry = msg 134 | elif msg_type == BroadcastMessageType.GameStreamError: 135 | self._stream_error = msg 136 | self.on_gamestream_error(msg) 137 | elif msg_type in [BroadcastMessageType.StartGameStream, 138 | BroadcastMessageType.StopGameStream]: 139 | raise NanoManagerError('{0} received on client side'.format(msg_type.name)) 140 | 141 | @property 142 | def client_major_version(self): 143 | return self.PROTOCOL_MAJOR_VERSION 144 | 145 | @property 146 | def client_minor_version(self): 147 | return self.PROTOCOL_MINOR_VERSION 148 | 149 | @property 150 | def stream_connected(self): 151 | return self._connected 152 | 153 | @property 154 | def stream_state(self): 155 | return self._current_state 156 | 157 | @property 158 | def streaming(self): 159 | return GameStreamState.Started in self._stream_states 160 | 161 | @property 162 | def stream_enabled(self): 163 | if self._stream_enabled: 164 | return self._stream_enabled.enabled 165 | 166 | @property 167 | def stream_can_be_enabled(self): 168 | if self._stream_enabled: 169 | return self._stream_enabled.canBeEnabled 170 | 171 | @property 172 | def server_major_version(self): 173 | if self._stream_enabled: 174 | return self._stream_enabled.majorProtocolVersion 175 | 176 | @property 177 | def server_minor_version(self): 178 | if self._stream_enabled: 179 | return self._stream_enabled.minorProtocolVersion 180 | 181 | @property 182 | def wireless(self): 183 | if GameStreamState.Started in self._stream_states: 184 | return self._stream_states[GameStreamState.Started].isWirelessConnection 185 | 186 | @property 187 | def transmit_linkspeed(self): 188 | if GameStreamState.Started in self._stream_states: 189 | return self._stream_states[GameStreamState.Started].transmitLinkSpeed 190 | 191 | @property 192 | def wireless_channel(self): 193 | if GameStreamState.Started in self._stream_states: 194 | return self._stream_states[GameStreamState.Started].wirelessChannel 195 | 196 | @property 197 | def session_id(self): 198 | if GameStreamState.Initializing in self._stream_states: 199 | return self._stream_states[GameStreamState.Initializing].sessionId 200 | 201 | @property 202 | def tcp_port(self): 203 | if GameStreamState.Initializing in self._stream_states: 204 | return self._stream_states[GameStreamState.Initializing].tcpPort 205 | 206 | @property 207 | def udp_port(self): 208 | if GameStreamState.Initializing in self._stream_states: 209 | return self._stream_states[GameStreamState.Initializing].udpPort 210 | -------------------------------------------------------------------------------- /xbox/nano/protocol.py: -------------------------------------------------------------------------------- 1 | import time 2 | import random 3 | import logging 4 | from typing import Optional, Tuple, List 5 | 6 | import asyncio 7 | from asyncio.streams import StreamReader, StreamWriter 8 | from asyncio.transports import DatagramTransport 9 | from asyncio.protocols import DatagramProtocol 10 | 11 | from xbox.sg.utils.events import Event 12 | from xbox.nano import factory, packer 13 | from xbox.nano.enum import RtpPayloadType, ChannelControlPayloadType 14 | from xbox.nano.channel import CHANNEL_CLASS_MAP 15 | 16 | log = logging.getLogger(__name__) 17 | 18 | 19 | class NanoProtocolError(Exception): 20 | pass 21 | 22 | 23 | class NanoProtocol(object): 24 | """ 25 | Client sends ChannelClientHandshake with generated connection id 26 | Server responds with ChannelServerHandshake with connection id 27 | 28 | UDP protocol sends HandShakeUDP 0x1 with connection ID in RTP header 29 | 30 | Server sends ChannelCreates and ChannelOpens 31 | Client responds with ChannelOpens (copying possible flags) 32 | """ 33 | def __init__(self, client, address: str, session_id, tcp_port: int, udp_port: int): 34 | self.loop = asyncio.get_running_loop() 35 | 36 | self.client = client 37 | self.session_id = session_id 38 | 39 | self.remote_addr = address 40 | self.tcp_port = tcp_port 41 | self.udp_port = udp_port 42 | 43 | self.channels = {} 44 | self.connection_id = 0 45 | self.connected = asyncio.Future() 46 | 47 | self.control_protocol: ControlProtocol = ControlProtocol(address, tcp_port, self) 48 | self.streamer_transport: Optional[DatagramTransport] = None 49 | self.streamer_protocol: Optional[StreamerProtocol] = None 50 | 51 | async def start(self): 52 | # Initialize TCP socket 53 | self.control_protocol.on_message += self._on_control_message 54 | await self.control_protocol.start() 55 | 56 | # Initialize UDP socket 57 | self.streamer_transport, self.streamer_protocol = await self.loop.create_datagram_endpoint( 58 | lambda: StreamerProtocol(self), 59 | remote_addr=(self.remote_addr, self.udp_port) 60 | ) 61 | self.streamer_protocol.on_message += self._on_streamer_message 62 | 63 | self.client.open(self) 64 | 65 | async def stop(self): 66 | self.control_protocol.on_message -= self._on_control_message 67 | self.streamer_protocol.on_message -= self._on_streamer_message 68 | 69 | # TODO: close channels and stuff? 70 | await self.control_protocol.stop() 71 | self.streamer_transport.close() 72 | 73 | async def connect(self, timeout=10): 74 | self.channel_control_handshake() 75 | 76 | async def udp_handshake_loop(): 77 | while not self.streamer_protocol.connected.done(): 78 | self.udp_handshake() 79 | await asyncio.sleep(0.5) 80 | 81 | asyncio.create_task(udp_handshake_loop()) 82 | 83 | def get_channel(self, channel_class): 84 | """ 85 | Get channel instance by channel class identifier 86 | 87 | Args: 88 | channel_class (:class:`.ChannelClass`): Enum member of 89 | :class:`.ChannelClass` 90 | 91 | Returns: 92 | :obj:`.Channel`: Instance of channel 93 | """ 94 | _class = CHANNEL_CLASS_MAP[channel_class] 95 | for channel in self.channels.values(): 96 | if isinstance(channel, _class): 97 | return channel 98 | 99 | def _on_control_message(self, msg): 100 | payload_type = msg.header.flags.payload_type 101 | channel_id = msg.header.ssrc.channel_id 102 | 103 | if payload_type == RtpPayloadType.Control and \ 104 | msg.payload.type == ChannelControlPayloadType.ServerHandshake: 105 | self.connection_id = msg.payload.connection_id 106 | self.connected.set_result(True) 107 | 108 | elif payload_type == RtpPayloadType.ChannelControl: 109 | if msg.payload.type == ChannelControlPayloadType.ChannelCreate: 110 | channel_name = msg.payload.name 111 | 112 | if channel_name not in CHANNEL_CLASS_MAP: 113 | raise NanoProtocolError( 114 | "Unsupported channel: %s", channel_name 115 | ) 116 | 117 | channel = CHANNEL_CLASS_MAP[channel_name]( 118 | self.client, self, 119 | channel_id, channel_name, msg.payload.flags 120 | ) 121 | 122 | self.channels[channel_id] = channel 123 | log.info("Channel created: %s", channel) 124 | 125 | elif channel_id not in self.channels: 126 | raise NanoProtocolError("Unknown channel: %d", channel_id) 127 | 128 | elif msg.payload.type == ChannelControlPayloadType.ChannelOpen: 129 | channel = self.channels[channel_id] 130 | channel.open = True 131 | channel.on_open(msg.payload.flags) 132 | 133 | log.info("Channel opened with flags %s: %s", 134 | msg.payload.flags, channel) 135 | 136 | elif msg.payload.type == ChannelControlPayloadType.ChannelClose: 137 | channel = self.channels[channel_id] 138 | channel.open = False 139 | channel.on_close(msg.payload.flags) 140 | 141 | log.info("Channel closed: %s", channel) 142 | 143 | elif payload_type == RtpPayloadType.Streamer: 144 | self.channels[channel_id].on_message(msg) 145 | 146 | else: 147 | log.warning("Unknown payload type", extra={'_msg': msg}) 148 | 149 | def _on_streamer_message(self, msg): 150 | channel_id = msg.header.ssrc.channel_id 151 | 152 | if channel_id not in self.channels: 153 | log.warning("Unknown channel id: %d", channel_id) 154 | # TODO: what to do here? 155 | log.warning(msg) 156 | return 157 | 158 | self.channels[channel_id].on_message(msg) 159 | 160 | def channel_control_handshake(self, connection_id=None): 161 | if not connection_id: 162 | connection_id = random.randint(50000, 60000) 163 | 164 | msg = factory.channel.control_handshake(connection_id) 165 | self.control_protocol.send_message(msg) 166 | 167 | def channel_create(self, name, flags, channel_id): 168 | msg = factory.channel.create(name, flags, channel_id) 169 | self.control_protocol.send_message(msg) 170 | 171 | def channel_open(self, flags, channel_id): 172 | msg = factory.channel.open(flags, channel_id) 173 | self.control_protocol.send_message(msg) 174 | 175 | def channel_close(self, flags, channel_id): 176 | msg = factory.channel.close(flags, channel_id) 177 | self.control_protocol.send_message(msg) 178 | 179 | def udp_handshake(self): 180 | msg = factory.udp_handshake(self.connection_id) 181 | self.streamer_protocol.send_message(msg) 182 | 183 | 184 | class ControlProtocolError(Exception): 185 | pass 186 | 187 | 188 | class ControlProtocol(object): 189 | BUFFER_SIZE = 4096 190 | 191 | def __init__(self, address: str, port: int, nano: NanoProtocol): 192 | self.host: Tuple[str, int] = (address, port) 193 | self._nano = nano # Do we want this? Circular reference.. 194 | self._q = [] 195 | self._reader: Optional[StreamReader] = None 196 | self._writer: Optional[StreamWriter] = None 197 | self._recv_task: Optional[asyncio.Task] = None 198 | 199 | self.on_message = Event() 200 | 201 | async def start(self): 202 | address, port = self.host 203 | self._reader, self._writer = await asyncio.open_connection(address, port) 204 | self._recv_task = asyncio.create_task(self._recv()) 205 | 206 | async def stop(self): 207 | if self._recv_task: 208 | self._recv_task.cancel() 209 | try: 210 | await self._recv_task 211 | except asyncio.CancelledError: 212 | log.warning('ControlProtocol: Cancelled recv task') 213 | 214 | if self._writer: 215 | self._writer.close() 216 | await self._writer.wait_closed() 217 | 218 | async def handle(self, data): 219 | try: 220 | for msg in packer.unpack_tcp(data, self._nano.channels): 221 | self.on_message(msg) 222 | except Exception as e: 223 | log.exception("Exception in ControlProtocol message handler") 224 | 225 | async def _recv(self): 226 | while True: 227 | data = await self._reader.read(self.BUFFER_SIZE) 228 | await self.handle(data) 229 | 230 | def _send(self, msgs): 231 | data = packer.pack_tcp(msgs, self._nano.channels) 232 | 233 | if not data: 234 | raise ControlProtocolError('No data') 235 | 236 | self._writer.write(data) 237 | # await self._writer.drain() 238 | 239 | def queue(self, msg): 240 | self._q.append(msg) 241 | 242 | def flush(self): 243 | self._send(self._q) 244 | self._q = [] 245 | 246 | def send_message(self, msg): 247 | self.queue(msg) 248 | self.flush() 249 | 250 | 251 | class StreamerProtocolError(Exception): 252 | pass 253 | 254 | 255 | class StreamerProtocol(object): 256 | def __init__(self, nano: NanoProtocol): 257 | self._nano: NanoProtocol = nano # Do we want this? Circular reference.. 258 | 259 | self.connected = asyncio.Future() 260 | self.transport: Optional[DatagramTransport] = None 261 | 262 | self.on_message = Event() 263 | 264 | def connection_made(self, transport): 265 | self.transport = transport 266 | 267 | def datagram_received(self, data, addr): 268 | if not self.connected.done(): 269 | self.connected.set_result(True) 270 | 271 | try: 272 | msg = packer.unpack(data, self._nano.channels) 273 | msg(_incoming_ts=time.time()) 274 | self.on_message(msg) 275 | except Exception as e: 276 | log.exception("Exception in StreamerProtocol message handler") 277 | 278 | def error_received(self, exc): 279 | print('Error received:', exc) 280 | 281 | def connection_lost(self, exc): 282 | print("Connection closed") 283 | 284 | def send_message(self, msg): 285 | data = packer.pack(msg) 286 | 287 | if not data: 288 | raise StreamerProtocolError('No data') 289 | 290 | self.transport.sendto(data) 291 | -------------------------------------------------------------------------------- /xbox/nano/channel.py: -------------------------------------------------------------------------------- 1 | import time 2 | import random 3 | import logging 4 | from collections import deque 5 | from datetime import datetime 6 | 7 | from xbox.nano import factory 8 | from xbox.nano.packet import audio 9 | from xbox.nano.enum import ChannelClass, VideoPayloadType, AudioPayloadType, \ 10 | InputPayloadType, ControlPayloadType, ControllerEvent, VideoQuality 11 | 12 | log = logging.getLogger(__name__) 13 | 14 | 15 | class Channel(object): 16 | def __init__(self, client, protocol, channel_id, name, flags): 17 | self.client = client 18 | self.protocol = protocol 19 | 20 | self.id = channel_id 21 | self.name = name 22 | self.flags = flags 23 | self.open = False 24 | 25 | self._sequence_num = 0 26 | self._frame_id = 0 27 | self._ref_timestamp = None 28 | 29 | def __repr__(self): 30 | return '<{:s} id={:d} class={:s} flags=0x{:08x} opened={:}>'.format( 31 | self.__class__.__name__, self.id, self.name, self.flags, self.open 32 | ) 33 | 34 | @property 35 | def sequence_num(self): 36 | return self._sequence_num 37 | 38 | @property 39 | def next_sequence_num(self): 40 | self._sequence_num += 1 41 | return self._sequence_num 42 | 43 | @property 44 | def reference_timestamp(self): 45 | return self._ref_timestamp 46 | 47 | @reference_timestamp.setter 48 | def reference_timestamp(self, val): 49 | self._ref_timestamp = val 50 | log.debug("%s - Set Reference timestamp: %s", self.name, self._ref_timestamp) 51 | 52 | def generate_reference_timestamp(self): 53 | self.reference_timestamp = datetime.utcnow() 54 | return self.reference_timestamp 55 | 56 | @property 57 | def frame_id(self): 58 | return self._frame_id 59 | 60 | @property 61 | def next_frame_id(self): 62 | self._frame_id += 1 63 | return self._frame_id 64 | 65 | @frame_id.setter 66 | def frame_id(self, val): 67 | log.debug("%s - Initial Frame Id: %i", self.name, val) 68 | self._frame_id = val 69 | 70 | def generate_initial_frame_id(self): 71 | self._frame_id = random.randint(0, 500) 72 | return self._frame_id 73 | 74 | def send_tcp_streamer(self, payload_type, payload): 75 | prev_seq = self.sequence_num 76 | msg = factory.streamer_tcp( 77 | self.next_sequence_num, prev_seq, payload_type, payload, 78 | channel_id=self.id 79 | ) 80 | 81 | self.protocol.control_protocol.send_message(msg) 82 | 83 | def send_udp_streamer(self, payload_type, payload): 84 | msg = factory.streamer_udp( 85 | payload_type, payload, 86 | connection_id=self.protocol.connection_id, channel_id=self.id, 87 | sequence_num=self.next_sequence_num 88 | ) 89 | 90 | self.protocol.streamer_protocol.send_message(msg) 91 | 92 | def on_message(self, msg): 93 | raise NotImplementedError() 94 | 95 | def on_open(self, flags): 96 | raise NotImplementedError() 97 | 98 | def on_close(self, flags): 99 | raise NotImplementedError() 100 | 101 | 102 | class VideoChannel(Channel): 103 | def __init__(self, *args, **kwargs): 104 | super(VideoChannel, self).__init__(*args, **kwargs) 105 | self._frame_buf = {} 106 | self._frame_expiry_time = 3.0 107 | self._render_queue = deque() 108 | 109 | def on_message(self, msg): 110 | if VideoPayloadType.Data == msg.header.streamer.type: 111 | self.on_data(msg) 112 | elif VideoPayloadType.ServerHandshake == msg.header.streamer.type: 113 | self.on_server_handshake(msg) 114 | else: 115 | log.warning("Unknown message received on VideoChannel", extra={'_msg': msg}) 116 | 117 | def on_open(self, flags): 118 | self.protocol.channel_open(flags, self.id) 119 | 120 | def on_close(self, flags): 121 | raise NotImplementedError() 122 | 123 | def client_handshake(self, video_format): 124 | payload = factory.video.client_handshake( 125 | initial_frame_id=self.generate_initial_frame_id(), 126 | requested_format=video_format 127 | ) 128 | self.client.set_video_format(video_format) 129 | self.send_tcp_streamer(VideoPayloadType.ClientHandshake, payload) 130 | 131 | def on_server_handshake(self, msg): 132 | payload = msg.payload 133 | log.debug("VideoChannel server handshake", extra={'_msg': msg}) 134 | self.reference_timestamp = payload.reference_timestamp 135 | self.client_handshake(payload.formats[0]) 136 | # You could set initial video format here 137 | # self.protocol.get_channel(ChannelClass.Control).change_video_quality(VideoQuality.Middle) 138 | self.control() 139 | 140 | def on_data(self, msg): 141 | flags = msg.payload.flags 142 | frame_id = msg.payload.frame_id 143 | timestamp = msg.payload.timestamp 144 | packet_count = msg.payload.packet_count 145 | 146 | if packet_count == 1: 147 | self._render_queue.append(( 148 | frame_id, flags, timestamp, msg.payload.data 149 | )) 150 | else: 151 | if frame_id not in self._frame_buf: 152 | # msg list, current count, packet count 153 | frame_buf = [[msg], 1, packet_count, time.time()] 154 | self._frame_buf[frame_id] = frame_buf 155 | else: 156 | frame_buf = self._frame_buf[frame_id] 157 | frame_buf[0].append(msg) 158 | frame_buf[1] += 1 159 | 160 | # current count == packet count 161 | if frame_buf[1] == frame_buf[2]: 162 | data_buf = frame_buf[0] 163 | data_buf.sort(key=lambda x: x.payload.offset) 164 | frame = b''.join([packet.payload.data for packet in data_buf]) 165 | 166 | self.client.render_video(frame) 167 | del self._frame_buf[frame_id] 168 | 169 | # Discard frames older than self._frame_expiry_time 170 | self._frame_buf = {k: v for (k, v) in self._frame_buf.items() 171 | if (time.time() - v[3]) < self._frame_expiry_time} 172 | 173 | def control(self, start_stream=True): 174 | # TODO 175 | if start_stream: 176 | payload = factory.video.control( 177 | request_keyframe=True, start_stream=True 178 | ) 179 | else: 180 | payload = factory.video.control(stop_stream=True) 181 | 182 | self.send_tcp_streamer(VideoPayloadType.Control, payload) 183 | 184 | 185 | class AudioChannel(Channel): 186 | def on_message(self, msg): 187 | if AudioPayloadType.Data == msg.header.streamer.type: 188 | self.on_data(msg) 189 | elif AudioPayloadType.ServerHandshake == msg.header.streamer.type: 190 | self.on_server_handshake(msg) 191 | else: 192 | log.warning("Unknown message received on AudioChannel", extra={'_msg': msg}) 193 | 194 | def on_open(self, flags): 195 | self.protocol.channel_open(flags, self.id) 196 | 197 | def on_close(self, flags): 198 | self.client.close() 199 | 200 | def client_handshake(self, audio_format): 201 | payload = factory.audio.client_handshake( 202 | initial_frame_id=self.generate_initial_frame_id(), 203 | requested_format=audio_format 204 | ) 205 | self.client.set_audio_format(audio_format) 206 | self.send_tcp_streamer(AudioPayloadType.ClientHandshake, payload) 207 | 208 | def on_server_handshake(self, msg): 209 | payload = msg.payload 210 | log.debug("AudioChannel server handshake", extra={'_msg': msg}) 211 | self.reference_timestamp = payload.reference_timestamp 212 | self.client_handshake(payload.formats[0]) 213 | self.control() 214 | 215 | def on_data(self, msg): 216 | # print('AudioChannel:on_data ', msg) 217 | self.client.render_audio(msg.payload.data) 218 | 219 | def control(self): 220 | payload = factory.audio.control( 221 | reinitialize=False, start_stream=True, stop_stream=False 222 | ) 223 | self.send_tcp_streamer(AudioPayloadType.Control, payload) 224 | 225 | 226 | class ChatAudioChannel(Channel): 227 | """ 228 | This one is special 229 | 1. Client sends ServerHandshake initially 230 | 2. Host responds with ClientHandshake 231 | """ 232 | def on_message(self, msg): 233 | if AudioPayloadType.ClientHandshake == msg.header.streamer.type: 234 | self.on_client_handshake(msg) 235 | elif AudioPayloadType.Control == msg.header.streamer.type: 236 | self.on_control(msg) 237 | else: 238 | log.warning("Unknown message received on ChatAudioChannel", 239 | extra={'_msg': msg}) 240 | 241 | def on_open(self, flags): 242 | self.protocol.channel_open(flags, self.id) 243 | 244 | def on_close(self, flags): 245 | self.client.close() 246 | 247 | def on_client_handshake(self, msg): 248 | log.debug("ChatAudioChannel client handshake", extra={'_msg': msg}) 249 | raise NotImplementedError("We should configure audio input parameters / encoder here") 250 | 251 | def server_handshake(self): 252 | # 1 Channel, Samplerate: 24000, Codec: Opus 253 | formats = [audio.fmt(1, 24000, 0)] 254 | payload = factory.audio.server_handshake( 255 | protocol_version=4, 256 | reference_timestamp=self.generate_reference_timestamp(), 257 | formats=formats 258 | ) 259 | self.send_tcp_streamer(AudioPayloadType.ServerHandshake, payload) 260 | 261 | def on_control(self, msg): 262 | raise NotImplementedError("We should start streaming ChatAudio frames here") 263 | 264 | def data(self, data): 265 | ts = int(time.time()) 266 | payload = factory.audio.data( 267 | flags=4, frame_id=0, timestamp=ts, data=data 268 | ) 269 | self.send_udp_streamer(AudioPayloadType.Data, payload) 270 | 271 | 272 | class InputChannel(Channel): 273 | def get_input_timestamp_from_dt(self, datetime_obj): 274 | """ 275 | Nanoseconds (1/1000000)s 276 | """ 277 | delta = (datetime_obj - self.reference_timestamp) 278 | return int(delta.total_seconds() * 100000) 279 | 280 | def get_input_timestamp_now(self): 281 | return self.get_input_timestamp_from_dt(datetime.utcnow()) 282 | 283 | def on_message(self, msg): 284 | if InputPayloadType.ServerHandshake == msg.header.streamer.type: 285 | self.on_server_handshake(msg) 286 | elif InputPayloadType.FrameAck == msg.header.streamer.type: 287 | self.on_frame_ack(msg) 288 | else: 289 | log.warning("Unknown message received on InputChannel", extra={'_msg': msg}) 290 | 291 | def on_open(self, flags): 292 | self.protocol.channel_open(flags, self.id) 293 | 294 | def on_close(self, flags): 295 | pass 296 | 297 | def client_handshake(self, max_touches=10): 298 | payload = factory.input.client_handshake( 299 | max_touches=max_touches, 300 | reference_timestamp=self.generate_reference_timestamp() 301 | ) 302 | self.send_tcp_streamer(InputPayloadType.ClientHandshake, payload) 303 | 304 | def on_server_handshake(self, msg): 305 | log.debug("InputChannel server handshake", extra={'_msg': msg}) 306 | self.frame_id = msg.payload.initial_frame_id 307 | self.client_handshake() 308 | 309 | def on_frame_ack(self, msg): 310 | log.debug("Acked InputFrame: %s", msg.payload.acked_frame) 311 | 312 | def send_frame(self, input_frame, created_dt): 313 | input_frame = input_frame( 314 | frame_id=self.next_frame_id, 315 | timestamp=self.get_input_timestamp_now(), 316 | created_ts=self.get_input_timestamp_from_dt(created_dt) 317 | ) 318 | log.debug("Sending Input Frame msg: %s", input_frame) 319 | self.send_udp_streamer(InputPayloadType.Frame, input_frame) 320 | 321 | 322 | class InputFeedbackChannel(Channel): 323 | """ 324 | This one is special 325 | 1. Client sends ServerHandshake initially 326 | 2. Host responds with ClientHandshake 327 | """ 328 | def on_message(self, msg): 329 | if InputPayloadType.ClientHandshake == msg.header.streamer.type: 330 | self.on_client_handshake(msg) 331 | elif InputPayloadType.Frame == msg.header.streamer.type: 332 | self.on_frame(msg) 333 | else: 334 | log.warning("Unknown message received on InputFeedbackChannel", extra={'_msg': msg}) 335 | 336 | def on_open(self, flags): 337 | self.protocol.channel_open(flags, self.id) 338 | self.server_handshake() 339 | 340 | def on_close(self, flags): 341 | raise NotImplementedError() 342 | 343 | def on_client_handshake(self, msg): 344 | log.debug("InputFeedbackChannel ClientHandshake", extra={'_msg': msg}) 345 | 346 | def on_frame(self, msg): 347 | log.debug("InputFeedbackChannel Frame", extra={'_msg': msg}) 348 | # raise NotImplementedError() 349 | 350 | def server_handshake(self): 351 | frame_id = random.randint(0, 500) 352 | # TODO: Do not hardcode desktop width/height 353 | payload = factory.input.server_handshake( 354 | protocol_version=3, 355 | desktop_width=1280, 356 | desktop_height=720, 357 | max_touches=0, 358 | initial_frame_id=frame_id 359 | ) 360 | self.send_tcp_streamer(InputPayloadType.ServerHandshake, payload) 361 | 362 | 363 | class ControlChannel(Channel): 364 | def on_message(self, msg): 365 | opcode = msg.payload.opcode 366 | if ControlPayloadType.RealtimeTelemetry == opcode: 367 | # Mute telemetry... 368 | pass 369 | else: 370 | log.warning("Unknown message received on ControlChannel", extra={'_msg': msg}) 371 | 372 | def on_open(self, flags): 373 | self.protocol.channel_open(flags, self.id) 374 | # self.client.on_control_channel_established() 375 | 376 | def on_close(self, flags): 377 | raise NotImplementedError() 378 | 379 | def client_handshake(self): 380 | raise NotImplementedError() 381 | 382 | def server_handshake(self): 383 | raise NotImplementedError() 384 | 385 | def _send_control_msg(self, opcode, payload): 386 | msg = factory.control.control_header( 387 | prev_seq_dup=self.sequence_num, 388 | unk1=1, 389 | unk2=1406, 390 | opcode=opcode, 391 | payload=payload.container 392 | ) 393 | self.send_tcp_streamer(0, msg) 394 | 395 | def change_video_quality(self, quality): 396 | payload = factory.control.change_video_quality( 397 | quality[0], quality[1], quality[2], 398 | quality[3], quality[4], quality[5] 399 | ) 400 | log.debug("Sending Change Video Quality msg: %s", payload) 401 | self._send_control_msg(ControlPayloadType.ChangeVideoQuality, payload) 402 | 403 | def controller_added(self, num=0): 404 | payload = factory.control.controller_event(ControllerEvent.Added, num) 405 | log.debug("Sending Controller added msg: %s", payload) 406 | self._send_control_msg(ControlPayloadType.ControllerEvent, payload) 407 | 408 | def controller_removed(self, num=0): 409 | payload = factory.control.controller_event( 410 | ControllerEvent.Removed, num 411 | ) 412 | log.debug("Sending Controller removed msg: %s", payload) 413 | self._send_control_msg(ControlPayloadType.ControllerEvent, payload) 414 | 415 | 416 | CHANNEL_CLASS_MAP = { 417 | ChannelClass.Video: VideoChannel, 418 | ChannelClass.Audio: AudioChannel, 419 | ChannelClass.ChatAudio: ChatAudioChannel, 420 | ChannelClass.Input: InputChannel, 421 | ChannelClass.InputFeedback: InputFeedbackChannel, 422 | ChannelClass.Control: ControlChannel, 423 | } 424 | -------------------------------------------------------------------------------- /tests/test_packer.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from binascii import hexlify 3 | 4 | from xbox.nano import packer, enum 5 | from xbox.nano.packet import message 6 | 7 | 8 | 9 | def test_rtpheader_tcp(packets, channels): 10 | unpacked = packer.unpack(packets['tcp_control_handshake'], channels) 11 | 12 | assert unpacked.header.flags.version, 2 13 | assert unpacked.header.flags.padding is True 14 | assert unpacked.header.flags.extension is False 15 | assert unpacked.header.flags.csrc_count == 0 16 | assert unpacked.header.flags.marker is False 17 | assert unpacked.header.flags.payload_type == enum.RtpPayloadType.Control 18 | assert unpacked.header.sequence_num == 0 19 | assert unpacked.header.timestamp == 2847619159 20 | assert unpacked.header.ssrc.connection_id == 0 21 | assert unpacked.header.ssrc.channel_id == 0 22 | assert len(unpacked.header.csrc_list) == 0 23 | 24 | 25 | def test_rtpheader_udp(packets, channels): 26 | unpacked = packer.unpack(packets['udp_video_data'], channels) 27 | 28 | assert unpacked.header.flags.version == 2 29 | assert unpacked.header.flags.padding is True 30 | assert unpacked.header.flags.extension is False 31 | assert unpacked.header.flags.csrc_count == 0 32 | assert unpacked.header.flags.marker is False 33 | assert unpacked.header.flags.payload_type == enum.RtpPayloadType.Streamer 34 | assert unpacked.header.sequence_num == 1 35 | assert unpacked.header.timestamp == 0 36 | assert unpacked.header.ssrc.connection_id == 35795 37 | assert unpacked.header.ssrc.channel_id == 1024 38 | assert len(unpacked.header.csrc_list) == 0 39 | 40 | 41 | def test_control_handshake(packets, channels): 42 | unpacked = packer.unpack(packets['tcp_control_handshake'], channels) 43 | 44 | assert unpacked.header.flags.payload_type == enum.RtpPayloadType.Control 45 | assert unpacked.payload.type == enum.ChannelControlPayloadType.ClientHandshake 46 | assert unpacked.payload.connection_id == 40084 47 | 48 | 49 | def test_udp_handshake(packets, channels): 50 | unpacked = packer.unpack(packets['udp_handshake'], channels) 51 | 52 | assert unpacked.header.flags.payload_type == enum.RtpPayloadType.UDPHandshake 53 | assert unpacked.header.ssrc.connection_id == 35795 54 | 55 | assert unpacked.payload.unk == 1 56 | 57 | 58 | def test_control_msg_with_header(packets, channels): 59 | unpacked = packer.unpack(packets['tcp_control_msg_with_header'], channels) 60 | assert unpacked.header.flags.payload_type == enum.RtpPayloadType.Streamer 61 | 62 | assert unpacked.header.streamer.streamer_version == 3 63 | assert unpacked.header.streamer.sequence_num == 1 64 | assert unpacked.header.streamer.prev_sequence_num == 0 65 | assert unpacked.header.streamer.type == 0 66 | 67 | assert unpacked.payload.prev_seq_dup == 0 68 | assert unpacked.payload.unk1 == 1 69 | assert unpacked.payload.unk2 == 1406 70 | assert unpacked.payload.opcode == enum.ControlPayloadType.RealtimeTelemetry 71 | 72 | 73 | def test_channel_open_no_flags(packets, channels): 74 | unpacked = packer.unpack(packets['tcp_channel_open_no_flags'], channels) 75 | assert unpacked.header.flags.payload_type == enum.RtpPayloadType.ChannelControl 76 | 77 | assert unpacked.payload.type == enum.ChannelControlPayloadType.ChannelOpen 78 | assert unpacked.payload.flags == b'' 79 | 80 | 81 | def test_channel_open_with_flags(packets, channels): 82 | unpacked = packer.unpack(packets['tcp_channel_open_with_flags'], channels) 83 | assert unpacked.header.flags.payload_type == enum.RtpPayloadType.ChannelControl 84 | 85 | assert unpacked.payload.type == enum.ChannelControlPayloadType.ChannelOpen 86 | assert unpacked.payload.flags == b'\x01\x00\x02\x00' 87 | 88 | 89 | def test_channel_create(packets, channels): 90 | unpacked = packer.unpack(packets['tcp_channel_create'], channels) 91 | assert unpacked.header.flags.payload_type == enum.RtpPayloadType.ChannelControl 92 | 93 | assert unpacked.payload.type == enum.ChannelControlPayloadType.ChannelCreate 94 | assert unpacked.payload.name == enum.ChannelClass.Video 95 | assert unpacked.payload.flags == 0 96 | 97 | 98 | def test_channel_close(packets, channels): 99 | unpacked = packer.unpack(packets['tcp_channel_close'], channels) 100 | assert unpacked.header.flags.payload_type == enum.RtpPayloadType.ChannelControl 101 | 102 | assert unpacked.payload.type == enum.ChannelControlPayloadType.ChannelClose 103 | assert unpacked.payload.flags == 0 104 | 105 | 106 | def test_audio_client_handshake(packets, channels): 107 | unpacked = packer.unpack(packets['tcp_audio_client_handshake'], channels) 108 | assert unpacked.header.flags.payload_type == enum.RtpPayloadType.Streamer 109 | 110 | assert unpacked.header.streamer.streamer_version == 3 111 | assert unpacked.header.streamer.sequence_num == 1 112 | assert unpacked.header.streamer.prev_sequence_num == 0 113 | assert unpacked.header.streamer.type == enum.AudioPayloadType.ClientHandshake 114 | 115 | assert unpacked.payload.initial_frame_id == 693041842 116 | assert unpacked.payload.requested_format.channels == 2 117 | assert unpacked.payload.requested_format.sample_rate == 48000 118 | assert unpacked.payload.requested_format.codec == enum.AudioCodec.AAC 119 | 120 | 121 | def test_audio_server_handshake(packets, channels): 122 | unpacked = packer.unpack(packets['tcp_audio_server_handshake'], channels) 123 | assert unpacked.header.flags.payload_type == enum.RtpPayloadType.Streamer 124 | 125 | assert unpacked.header.streamer.streamer_version == 3 126 | assert unpacked.header.streamer.sequence_num == 1 127 | assert unpacked.header.streamer.prev_sequence_num == 0 128 | assert unpacked.header.streamer.type == enum.AudioPayloadType.ServerHandshake 129 | 130 | assert unpacked.payload.protocol_version == 4 131 | assert unpacked.payload.reference_timestamp == datetime.datetime.utcfromtimestamp(1495315092424 / 1000) 132 | assert len(unpacked.payload.formats) == 1 133 | assert unpacked.payload.formats[0].channels == 2 134 | assert unpacked.payload.formats[0].sample_rate == 48000 135 | assert unpacked.payload.formats[0].codec == enum.AudioCodec.AAC 136 | 137 | 138 | def test_audio_control(packets, channels): 139 | unpacked = packer.unpack(packets['tcp_audio_control'], channels) 140 | assert unpacked.header.flags.payload_type == enum.RtpPayloadType.Streamer 141 | 142 | assert unpacked.header.streamer.streamer_version == 3 143 | assert unpacked.header.streamer.sequence_num == 2 144 | assert unpacked.header.streamer.prev_sequence_num == 1 145 | assert unpacked.header.streamer.type == enum.AudioPayloadType.Control 146 | 147 | assert unpacked.payload.flags.reinitialize is False 148 | assert unpacked.payload.flags.start_stream is True 149 | assert unpacked.payload.flags.stop_stream is False 150 | 151 | 152 | def test_audio_data(packets, channels): 153 | unpacked = packer.unpack(packets['udp_audio_data'], channels) 154 | assert unpacked.header.flags.payload_type == enum.RtpPayloadType.Streamer 155 | 156 | assert unpacked.header.streamer.streamer_version == 0 157 | assert unpacked.header.streamer.type == enum.AudioPayloadType.Data 158 | 159 | assert unpacked.payload.flags == 4 160 | assert unpacked.payload.frame_id == 0 161 | assert unpacked.payload.timestamp == 3365588462 162 | assert len(unpacked.payload.data) == 357 163 | 164 | 165 | def test_video_client_handshake(packets, channels): 166 | unpacked = packer.unpack(packets['tcp_video_client_handshake'], channels) 167 | assert unpacked.header.flags.payload_type == enum.RtpPayloadType.Streamer 168 | 169 | assert unpacked.header.streamer.streamer_version == 3 170 | assert unpacked.header.streamer.sequence_num == 1 171 | assert unpacked.header.streamer.prev_sequence_num == 0 172 | assert unpacked.header.streamer.type == enum.VideoPayloadType.ClientHandshake 173 | 174 | assert unpacked.payload.initial_frame_id == 3715731054 175 | assert unpacked.payload.requested_format.fps == 30 176 | assert unpacked.payload.requested_format.width == 1280 177 | assert unpacked.payload.requested_format.height == 720 178 | assert unpacked.payload.requested_format.codec == enum.VideoCodec.H264 179 | 180 | 181 | def test_video_server_handshake(packets, channels): 182 | unpacked = packer.unpack(packets['tcp_video_server_handshake'], channels) 183 | assert unpacked.header.flags.payload_type == enum.RtpPayloadType.Streamer 184 | 185 | assert unpacked.header.streamer.streamer_version == 3 186 | assert unpacked.header.streamer.sequence_num == 1 187 | assert unpacked.header.streamer.prev_sequence_num == 0 188 | assert unpacked.header.streamer.type == enum.VideoPayloadType.ServerHandshake 189 | 190 | assert unpacked.payload.protocol_version == 5 191 | assert unpacked.payload.width == 1280 192 | assert unpacked.payload.height == 720 193 | assert unpacked.payload.fps == 30 194 | assert unpacked.payload.reference_timestamp == datetime.datetime.utcfromtimestamp(1495315092425 / 1000) 195 | assert len(unpacked.payload.formats) == 4 196 | 197 | assert unpacked.payload.formats[0].fps == 30 198 | assert unpacked.payload.formats[0].width == 1280 199 | assert unpacked.payload.formats[0].height == 720 200 | assert unpacked.payload.formats[0].codec == enum.VideoCodec.H264 201 | assert unpacked.payload.formats[1].fps == 30 202 | assert unpacked.payload.formats[1].width == 960 203 | assert unpacked.payload.formats[1].height == 540 204 | assert unpacked.payload.formats[1].codec == enum.VideoCodec.H264 205 | assert unpacked.payload.formats[2].fps == 30 206 | assert unpacked.payload.formats[2].width == 640 207 | assert unpacked.payload.formats[2].height == 360 208 | assert unpacked.payload.formats[2].codec == enum.VideoCodec.H264 209 | assert unpacked.payload.formats[3].fps == 30 210 | assert unpacked.payload.formats[3].width == 320 211 | assert unpacked.payload.formats[3].height == 180 212 | assert unpacked.payload.formats[3].codec == enum.VideoCodec.H264 213 | 214 | 215 | def test_video_control(packets, channels): 216 | unpacked = packer.unpack(packets['tcp_video_control'], channels) 217 | assert unpacked.header.flags.payload_type == enum.RtpPayloadType.Streamer 218 | 219 | assert unpacked.header.streamer.streamer_version == 3 220 | assert unpacked.header.streamer.sequence_num == 2 221 | assert unpacked.header.streamer.prev_sequence_num == 1 222 | assert unpacked.header.streamer.type == enum.VideoPayloadType.Control 223 | 224 | assert unpacked.payload.flags.request_keyframe is True 225 | assert unpacked.payload.flags.start_stream is True 226 | assert unpacked.payload.flags.stop_stream is False 227 | assert unpacked.payload.flags.queue_depth is False 228 | assert unpacked.payload.flags.lost_frames is False 229 | assert unpacked.payload.flags.last_displayed_frame is False 230 | 231 | 232 | def test_video_data(packets, channels): 233 | unpacked = packer.unpack(packets['udp_video_data'], channels) 234 | assert unpacked.header.flags.payload_type == enum.RtpPayloadType.Streamer 235 | 236 | assert unpacked.header.streamer.streamer_version == 0 237 | assert unpacked.header.streamer.type == enum.VideoPayloadType.Data 238 | 239 | assert unpacked.payload.flags == 4 240 | assert unpacked.payload.frame_id == 3715731054 241 | assert unpacked.payload.timestamp == 3365613642 242 | assert unpacked.payload.total_size == 5594 243 | assert unpacked.payload.packet_count == 5 244 | assert unpacked.payload.offset == 0 245 | assert len(unpacked.payload.data) == 1119 246 | 247 | 248 | def test_input_client_handshake(packets, channels): 249 | unpacked = packer.unpack(packets['tcp_input_client_handshake'], channels) 250 | assert unpacked.header.flags.payload_type == enum.RtpPayloadType.Streamer 251 | 252 | assert unpacked.header.streamer.streamer_version == 3 253 | assert unpacked.header.streamer.sequence_num == 1 254 | assert unpacked.header.streamer.prev_sequence_num == 0 255 | assert unpacked.header.streamer.type == enum.InputPayloadType.ClientHandshake 256 | 257 | assert unpacked.payload.max_touches == 10 258 | assert unpacked.payload.reference_timestamp == datetime.datetime.utcfromtimestamp(1498690645999 / 1000) 259 | 260 | 261 | def test_input_server_handshake(packets, channels): 262 | unpacked = packer.unpack(packets['tcp_input_server_handshake'], channels) 263 | assert unpacked.header.flags.payload_type == enum.RtpPayloadType.Streamer 264 | 265 | assert unpacked.header.streamer.streamer_version == 3 266 | assert unpacked.header.streamer.sequence_num == 1 267 | assert unpacked.header.streamer.prev_sequence_num == 0 268 | assert unpacked.header.streamer.type == enum.InputPayloadType.ServerHandshake 269 | 270 | assert unpacked.payload.protocol_version == 3 271 | assert unpacked.payload.desktop_width == 1280 272 | assert unpacked.payload.desktop_height == 720 273 | assert unpacked.payload.max_touches == 0 274 | assert unpacked.payload.initial_frame_id == 672208545 275 | 276 | 277 | def test_input_frame_ack(packets, channels): 278 | unpacked = packer.unpack(packets['udp_input_frame_ack'], channels) 279 | assert unpacked.header.flags.payload_type == enum.RtpPayloadType.Streamer 280 | 281 | assert unpacked.header.streamer.streamer_version == 0 282 | assert unpacked.header.streamer.type == enum.InputPayloadType.FrameAck 283 | 284 | assert unpacked.payload.acked_frame == 672208545 285 | 286 | 287 | def test_input_frame(packets, channels): 288 | unpacked = packer.unpack(packets['udp_input_frame'], channels) 289 | assert unpacked.header.flags.payload_type == enum.RtpPayloadType.Streamer 290 | 291 | assert unpacked.header.streamer.streamer_version == 0 292 | assert unpacked.header.streamer.type == enum.InputPayloadType.Frame 293 | 294 | assert unpacked.payload.frame_id == 672208564 295 | assert unpacked.payload.timestamp == 583706515 296 | assert unpacked.payload.created_ts == 583706495 297 | assert unpacked.payload.buttons.dpad_up == 0 298 | assert unpacked.payload.buttons.dpad_down == 0 299 | assert unpacked.payload.buttons.dpad_left == 0 300 | assert unpacked.payload.buttons.dpad_right == 1 301 | assert unpacked.payload.buttons.start == 0 302 | assert unpacked.payload.buttons.back == 0 303 | assert unpacked.payload.buttons.left_thumbstick == 0 304 | assert unpacked.payload.buttons.right_thumbstick == 0 305 | assert unpacked.payload.buttons.left_shoulder == 0 306 | assert unpacked.payload.buttons.right_shoulder == 0 307 | assert unpacked.payload.buttons.guide == 0 308 | assert unpacked.payload.buttons.unknown == 0 309 | assert unpacked.payload.buttons.a == 0 310 | assert unpacked.payload.buttons.b == 0 311 | assert unpacked.payload.buttons.x == 0 312 | assert unpacked.payload.buttons.y == 0 313 | assert unpacked.payload.analog.left_trigger == 0 314 | assert unpacked.payload.analog.right_trigger == 0 315 | assert unpacked.payload.analog.left_thumb_x == 1752 316 | assert unpacked.payload.analog.left_thumb_y == 684 317 | assert unpacked.payload.analog.right_thumb_x == 1080 318 | assert unpacked.payload.analog.right_thumb_y == 242 319 | assert unpacked.payload.analog.rumble_trigger_l == 0 320 | assert unpacked.payload.analog.rumble_trigger_r == 0 321 | assert unpacked.payload.analog.rumble_handle_l == 0 322 | assert unpacked.payload.analog.rumble_handle_r == 0 323 | assert unpacked.payload.extension.byte_6 == 1 324 | assert unpacked.payload.extension.byte_7 == 0 325 | assert unpacked.payload.extension.rumble_trigger_l2 == 0 326 | assert unpacked.payload.extension.rumble_trigger_r2 == 0 327 | assert unpacked.payload.extension.rumble_handle_l2 == 0 328 | assert unpacked.payload.extension.rumble_handle_r2 == 0 329 | assert unpacked.payload.extension.byte_12 == 0 330 | assert unpacked.payload.extension.byte_13 == 0 331 | assert unpacked.payload.extension.byte_14 == 0 332 | 333 | 334 | def _test_repack_all(packets, channels): 335 | for f in packets: 336 | unpacked = packer.unpack(packets[f]) 337 | msg = message.struct(**unpacked) 338 | repacked = packer.pack(msg, channels) 339 | 340 | assert repacked == packets[f], \ 341 | '%s was not repacked correctly:\n(repacked)%s\n!=\n(original)%s' \ 342 | % (f, hexlify(repacked), hexlify(packets[f])) 343 | -------------------------------------------------------------------------------- /xbox/nano/xpacker.py: -------------------------------------------------------------------------------- 1 | import struct 2 | import bitstruct 3 | from io import BytesIO 4 | from construct import Container 5 | 6 | from xbox.nano import enum, packer 7 | from xbox.nano.enum import ( 8 | RtpPayloadType, ChannelControlPayloadType, ChannelClass, 9 | VideoPayloadType, AudioPayloadType, InputPayloadType, ControlPayloadType, 10 | ControllerEvent 11 | ) 12 | 13 | pack = packer.pack 14 | 15 | RTP_FLAGS = bitstruct.compile('u2b1b1u4b1u7') 16 | VIDEO_CONTROL_FLAGS = bitstruct.compile('p2b1b1b1b1b1b1p24') 17 | AUDIO_CONTROL_FLAGS = bitstruct.compile('p1b1p1b1b1p27') 18 | 19 | STREAMER_TYPE_MAP = { 20 | ChannelClass.Video: VideoPayloadType, 21 | ChannelClass.Audio: AudioPayloadType, 22 | ChannelClass.ChatAudio: AudioPayloadType, 23 | ChannelClass.Input: InputPayloadType, 24 | ChannelClass.InputFeedback: InputPayloadType, 25 | ChannelClass.Control: lambda _: 0 26 | } 27 | 28 | 29 | class PackerError(Exception): 30 | pass 31 | 32 | 33 | def unpack_tcp(buf, channels=None): 34 | while len(buf): 35 | size = struct.unpack('HI2H', stream.read(10)) 95 | 96 | r = Container() 97 | rflags = Container() 98 | rssrc = Container() 99 | 100 | rflags['version'] = flags[0] 101 | rflags['padding'] = flags[1] 102 | rflags['extension'] = flags[2] 103 | rflags['csrc_count'] = flags[3] 104 | rflags['marker'] = flags[4] 105 | rflags['payload_type'] = RtpPayloadType(flags[5]) 106 | r['flags'] = rflags 107 | r['sequence_num'] = data[0] 108 | r['timestamp'] = data[1] 109 | rssrc['connection_id'] = data[2] 110 | rssrc['channel_id'] = data[3] 111 | r['ssrc'] = rssrc 112 | r['csrc_list'] = struct.unpack('>{}I'.format(flags[3]), stream.read(4 * flags[3])) 113 | 114 | return r 115 | 116 | 117 | def streamer(header, channel, stream): 118 | streamer_header = Container() 119 | if header['ssrc']['connection_id'] == 0: 120 | # TCP 121 | data = struct.unpack('<4I', stream.read(16)) 122 | streamer_header['streamer_version'] = data[0] 123 | streamer_header['sequence_num'] = data[1] 124 | streamer_header['prev_sequence_num'] = data[2] 125 | streamer_header['type'] = STREAMER_TYPE_MAP[channel](data[3]) 126 | else: 127 | # UDP 128 | data = struct.unpack('<2I', stream.read(8)) 129 | streamer_header['streamer_version'] = data[0] 130 | streamer_header['type'] = STREAMER_TYPE_MAP[channel](data[1]) 131 | 132 | if header['ssrc']['connection_id'] == 0 and streamer_header['type'] == 0: 133 | pass 134 | else: 135 | stream.read(4) 136 | 137 | header['streamer'] = streamer_header 138 | payload = Container() 139 | payload_type = streamer_header['type'] 140 | 141 | if channel == ChannelClass.Control: 142 | if payload_type == 0: 143 | payload = control(stream) 144 | 145 | elif channel == ChannelClass.Video: 146 | if payload_type == VideoPayloadType.Data: 147 | data = struct.unpack('<2IQ4I', stream.read(32)) 148 | payload['flags'] = data[0] 149 | payload['frame_id'] = data[1] 150 | payload['timestamp'] = data[2] 151 | payload['total_size'] = data[3] 152 | payload['packet_count'] = data[4] 153 | payload['offset'] = data[5] 154 | payload['data'] = stream.read(data[6]) 155 | elif payload_type == VideoPayloadType.ServerHandshake: 156 | data = struct.unpack('<4IQI', stream.read(28)) 157 | payload['protocol_version'] = data[0] 158 | payload['width'] = data[1] 159 | payload['height'] = data[2] 160 | payload['fps'] = data[3] 161 | payload['reference_timestamp'] = data[4] 162 | formats = [] 163 | for _ in range(data[5]): 164 | formats.append(video_fmt(stream)) 165 | payload['formats'] = formats 166 | elif payload_type == VideoPayloadType.ClientHandshake: 167 | data = struct.unpack('18B4H13B', stream.read(39)) 231 | 232 | buttons['dpad_up'] = data[0] 233 | buttons['dpad_down'] = data[1] 234 | buttons['dpad_left'] = data[2] 235 | buttons['dpad_right'] = data[3] 236 | buttons['start'] = data[4] 237 | buttons['back'] = data[5] 238 | buttons['left_thumbstick'] = data[6] 239 | buttons['right_thumbstick'] = data[7] 240 | buttons['left_shoulder'] = data[8] 241 | buttons['right_shoulder'] = data[9] 242 | buttons['guide'] = data[10] 243 | buttons['unknown'] = data[11] 244 | buttons['a'] = data[12] 245 | buttons['b'] = data[13] 246 | buttons['x'] = data[14] 247 | buttons['y'] = data[15] 248 | analog['left_trigger'] = data[16] 249 | analog['right_trigger'] = data[17] 250 | analog['left_thumb_x'] = data[18] 251 | analog['left_thumb_y'] = data[19] 252 | analog['right_thumb_x'] = data[20] 253 | analog['right_thumb_y'] = data[21] 254 | analog['rumble_trigger_l'] = data[22] 255 | analog['rumble_trigger_r'] = data[23] 256 | analog['rumble_handle_l'] = data[24] 257 | analog['rumble_handle_r'] = data[25] 258 | extension['byte_6'] = data[26] 259 | extension['byte_7'] = data[27] 260 | extension['rumble_trigger_l2'] = data[28] 261 | extension['rumble_trigger_r2'] = data[29] 262 | extension['rumble_handle_l2'] = data[30] 263 | extension['rumble_handle_r2'] = data[31] 264 | extension['byte_12'] = data[32] 265 | extension['byte_13'] = data[33] 266 | extension['byte_14'] = data[34] 267 | 268 | payload['buttons'] = buttons 269 | payload['analog'] = analog 270 | payload['extension'] = extension 271 | elif payload_type == InputPayloadType.ServerHandshake: 272 | data = struct.unpack('<5I', stream.read(20)) 273 | payload['protocol_version'] = data[0] 274 | payload['desktop_width'] = data[1] 275 | payload['desktop_height'] = data[2] 276 | payload['max_touches'] = data[3] 277 | payload['initial_frame_id'] = data[4] 278 | elif payload_type == InputPayloadType.ClientHandshake: 279 | data = struct.unpack('