From ca0a880c803daca1fa15cf3cf95bccae9684e45c Mon Sep 17 00:00:00 2001 From: Justin Lin Date: Thu, 9 Oct 2025 16:31:57 +1100 Subject: [PATCH] feat: init project --- Makefile | 55 +++ README.md | 162 ++++++++ example.py | 120 ++++++ poetry.lock | 413 ++++++++++++++++++++ pyproject.toml | 24 ++ setup.py | 45 +++ test_vlchan.py | 112 ++++++ vlchan/__init__.py | 6 + vlchan/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 370 bytes vlchan/__pycache__/player.cpython-313.pyc | Bin 0 -> 10265 bytes vlchan/__pycache__/synchan.cpython-313.pyc | Bin 0 -> 5988 bytes vlchan/player.py | 207 ++++++++++ vlchan/synchan.py | 118 ++++++ 13 files changed, 1262 insertions(+) create mode 100644 Makefile create mode 100644 README.md create mode 100644 example.py create mode 100644 poetry.lock create mode 100644 pyproject.toml create mode 100644 setup.py create mode 100644 test_vlchan.py create mode 100644 vlchan/__init__.py create mode 100644 vlchan/__pycache__/__init__.cpython-313.pyc create mode 100644 vlchan/__pycache__/player.cpython-313.pyc create mode 100644 vlchan/__pycache__/synchan.cpython-313.pyc create mode 100644 vlchan/player.py create mode 100644 vlchan/synchan.py diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..2e387e7 --- /dev/null +++ b/Makefile @@ -0,0 +1,55 @@ +.PHONY: install test lint format clean run-example + +# Install dependencies +install: + poetry install + +# Install with development dependencies +install-dev: + poetry install --with dev + +# Run tests +test: + poetry run python test_vlchan.py + +# Run example +run-example: + @echo "Usage: make run-example VIDEO=/path/to/video.mp4 [SYNCHAN=http://localhost:3000]" + poetry run python example.py $(VIDEO) $(SYNCHAN) + +# Run player directly +run-player: + @echo "Usage: make run-player VIDEO=/path/to/video.mp4 [SYNCHAN=http://localhost:3000]" + poetry run python -m vlchan.player $(VIDEO) $(SYNCHAN) + +# Lint code +lint: + poetry run ruff check . + +# Format code +format: + poetry run ruff format . + +# Type check +type-check: + poetry run mypy vlchan/ + +# Clean up +clean: + find . -type d -name "__pycache__" -exec rm -rf {} + + find . -type f -name "*.pyc" -delete + find . -type f -name "*.pyo" -delete + +# Show help +help: + @echo "Available targets:" + @echo " install - Install dependencies" + @echo " install-dev - Install with development dependencies" + @echo " test - Run tests" + @echo " run-example - Run example script (requires VIDEO=path)" + @echo " run-player - Run player directly (requires VIDEO=path)" + @echo " lint - Run linter" + @echo " format - Format code" + @echo " type-check - Run type checker" + @echo " clean - Clean up cache files" + @echo " help - Show this help" diff --git a/README.md b/README.md new file mode 100644 index 0000000..95a8702 --- /dev/null +++ b/README.md @@ -0,0 +1,162 @@ +# VLChan + +A Python video player using VLC SDK with synchan server timecode synchronization. + +## Features + +- VLC-based video playback with full media support +- Real-time synchronization with synchan server +- Reactive programming with RxPY +- Automatic timecode correction +- Playback state synchronization +- Volume and fullscreen controls + +## Installation + +1. Install VLC media player on your system: + - Ubuntu/Debian: `sudo apt install vlc` + - macOS: `brew install vlc` + - Windows: Download from [videolan.org](https://www.videolan.org/vlc/) + +2. Install the Python package: + ```bash + cd vlchan + poetry install + ``` + +## Usage + +### Basic Usage + +```python +from vlchan import VLChanPlayer + +# Create player with video file +player = VLChanPlayer( + synchan_url="http://localhost:3000", + video_path="/path/to/video.mp4" +) + +# Start synchronization +player.start_sync() + +# Play video +player.play() + +# Clean up when done +player.cleanup() +``` + +### Advanced Usage + +```python +from vlchan import VLChanPlayer + +def on_state_change(state): + print(f"Synchan state: playing={state.playing}, time={state.currentTime:.2f}s") + +# Create player +player = VLChanPlayer("http://localhost:3000", "video.mp4") + +# Set up state change callback +player.on_state_change = on_state_change + +# Start synchronization +player.start_sync() + +# Control playback +player.play() +player.pause() +player.seek(120.5) # Seek to 2 minutes 0.5 seconds + +# Adjust settings +player.set_volume(80) +player.set_fullscreen(True) + +# Get current state +print(f"Position: {player.get_position():.2f}s") +print(f"Duration: {player.get_duration():.2f}s") +print(f"Playing: {player.is_playing()}") +``` + +### Command Line Usage + +```bash +# Play video with synchan sync +poetry run python -m vlchan.player video.mp4 + +# Use custom synchan server +poetry run python -m vlchan.player video.mp4 http://192.168.1.100:3000 +``` + +## API Reference + +### VLChanPlayer + +#### Constructor +- `VLChanPlayer(synchan_url: str, video_path: Optional[str])` + - `synchan_url`: URL of the synchan server (default: "http://localhost:3000") + - `video_path`: Path to video file to load + +#### Methods +- `load_video(video_path: str)`: Load a video file +- `start_sync()`: Start synchronization with synchan server +- `stop_sync()`: Stop synchronization +- `play()`: Start playback +- `pause()`: Pause playback +- `stop()`: Stop playback +- `seek(time_seconds: float)`: Seek to specific time +- `set_volume(volume: int)`: Set volume (0-100) +- `get_volume() -> int`: Get current volume +- `set_fullscreen(fullscreen: bool)`: Toggle fullscreen +- `get_position() -> float`: Get current position in seconds +- `get_duration() -> float`: Get video duration in seconds +- `is_playing() -> bool`: Check if playing +- `cleanup()`: Clean up resources + +#### Properties +- `on_state_change`: Callback for synchan state changes + +### SynchanState + +Data class containing synchronization state: +- `playing: bool`: Whether video should be playing +- `currentTime: float`: Current time position in seconds +- `duration: float`: Total duration in seconds +- `loop: bool`: Whether video should loop +- `latency: float`: Network latency in seconds + +## Synchronization + +The player automatically synchronizes with the synchan server: + +1. **Playback State**: Play/pause commands are synchronized +2. **Time Position**: Video seeks to match synchan timecode +3. **Threshold**: Only seeks if time difference exceeds 100ms +4. **Reconnection**: Automatically attempts to reconnect on connection loss + +## Requirements + +- Python 3.11+ +- VLC media player +- Synchan server running + +## Dependencies + +- `python-vlc`: VLC Python bindings +- `reactivex`: Reactive programming +- `python-socketio`: WebSocket client for synchan +- `requests`: HTTP client for synchan API + +## Development + +```bash +# Install development dependencies +poetry install --with dev + +# Run linting +poetry run ruff check . + +# Run type checking +poetry run mypy vlchan/ +``` diff --git a/example.py b/example.py new file mode 100644 index 0000000..924fdf1 --- /dev/null +++ b/example.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +"""Example usage of VLChan video player with synchan synchronization.""" + +import time +import sys +from vlchan import VLChanPlayer + + +def main(): + """Example demonstrating VLChan usage.""" + + # Check command line arguments + if len(sys.argv) < 2: + print("Usage: python example.py [synchan_url]") + print("Example: python example.py /path/to/video.mp4 http://localhost:3000") + sys.exit(1) + + video_path = sys.argv[1] + synchan_url = sys.argv[2] if len(sys.argv) > 2 else "http://localhost:3000" + + print(f"VLChan Example") + print(f"Video: {video_path}") + print(f"Synchan: {synchan_url}") + print("-" * 40) + + # Create player + player = VLChanPlayer(synchan_url, video_path) + + # Set up state change callback + def on_state_change(state): + print(f"Synchan update: playing={state.playing}, " + f"time={state.currentTime:.2f}s, " + f"duration={state.duration:.2f}s") + + player.on_state_change = on_state_change + + try: + # Start synchronization + print("Starting synchan synchronization...") + player.start_sync() + + # Wait a moment for connection + time.sleep(1) + + # Start playback + print("Starting video playback...") + player.play() + + # Display controls + print("\nControls:") + print("- Press 'p' to pause/play") + print("- Press 's' to stop") + print("- Press 'f' to toggle fullscreen") + print("- Press 'v' to change volume") + print("- Press 'q' to quit") + print("- Press 'j' to seek forward 10s") + print("- Press 'k' to seek backward 10s") + print("\nPress 'q' to quit...") + + # Interactive control loop + while True: + try: + # Check for keyboard input (non-blocking) + import select + import tty + import termios + + if sys.stdin in select.select([sys.stdin], [], [], 0)[0]: + key = sys.stdin.read(1) + + if key == 'q': + break + elif key == 'p': + if player.is_playing(): + player.pause() + else: + player.play() + elif key == 's': + player.stop() + elif key == 'f': + player.set_fullscreen(not player.player.get_fullscreen()) + elif key == 'v': + current_vol = player.get_volume() + new_vol = min(100, current_vol + 10) + player.set_volume(new_vol) + print(f"Volume: {new_vol}") + elif key == 'j': + current_pos = player.get_position() + player.seek(current_pos + 10) + print(f"Seeked forward to {current_pos + 10:.1f}s") + elif key == 'k': + current_pos = player.get_position() + player.seek(max(0, current_pos - 10)) + print(f"Seeked backward to {max(0, current_pos - 10):.1f}s") + + # Display current status + if player.is_playing(): + pos = player.get_position() + dur = player.get_duration() + print(f"\rPlaying: {pos:.1f}s / {dur:.1f}s", end="", flush=True) + + time.sleep(0.1) + + except KeyboardInterrupt: + break + except Exception as e: + print(f"\nError: {e}") + break + + except Exception as e: + print(f"Error: {e}") + + finally: + print("\nCleaning up...") + player.cleanup() + print("Done!") + + +if __name__ == "__main__": + main() diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..0a94a38 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,413 @@ +# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand. + +[[package]] +name = "bidict" +version = "0.23.1" +description = "The bidirectional mapping library for Python." +optional = false +python-versions = ">=3.8" +files = [ + {file = "bidict-0.23.1-py3-none-any.whl", hash = "sha256:5dae8d4d79b552a71cbabc7deb25dfe8ce710b17ff41711e13010ead2abfc3e5"}, + {file = "bidict-0.23.1.tar.gz", hash = "sha256:03069d763bc387bbd20e7d49914e75fc4132a41937fa3405417e1a5a2d006d71"}, +] + +[[package]] +name = "certifi" +version = "2025.10.5" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.7" +files = [ + {file = "certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de"}, + {file = "certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.3" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7" +files = [ + {file = "charset_normalizer-3.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-win32.whl", hash = "sha256:d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-win32.whl", hash = "sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-win32.whl", hash = "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0f2be7e0cf7754b9a30eb01f4295cc3d4358a479843b31f328afd210e2c7598c"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c60e092517a73c632ec38e290eba714e9627abe9d301c8c8a12ec32c314a2a4b"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:252098c8c7a873e17dd696ed98bbe91dbacd571da4b87df3736768efa7a792e4"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3653fad4fe3ed447a596ae8638b437f827234f01a8cd801842e43f3d0a6b281b"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8999f965f922ae054125286faf9f11bc6932184b93011d138925a1773830bbe9"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d95bfb53c211b57198bb91c46dd5a2d8018b3af446583aab40074bf7988401cb"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:5b413b0b1bfd94dbf4023ad6945889f374cd24e3f62de58d6bb102c4d9ae534a"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:b5e3b2d152e74e100a9e9573837aba24aab611d39428ded46f4e4022ea7d1942"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:a2d08ac246bb48479170408d6c19f6385fa743e7157d716e144cad849b2dd94b"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-win32.whl", hash = "sha256:ec557499516fc90fd374bf2e32349a2887a876fbf162c160e3c01b6849eaf557"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:5d8d01eac18c423815ed4f4a2ec3b439d654e55ee4ad610e153cf02faf67ea40"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:70bfc5f2c318afece2f5838ea5e4c3febada0be750fcf4775641052bbba14d05"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:23b6b24d74478dc833444cbd927c338349d6ae852ba53a0d02a2de1fce45b96e"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:34a7f768e3f985abdb42841e20e17b330ad3aaf4bb7e7aeeb73db2e70f077b99"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb731e5deb0c7ef82d698b0f4c5bb724633ee2a489401594c5c88b02e6cb15f7"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:257f26fed7d7ff59921b78244f3cd93ed2af1800ff048c33f624c87475819dd7"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1ef99f0456d3d46a50945c98de1774da86f8e992ab5c77865ea8b8195341fc19"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:2c322db9c8c89009a990ef07c3bcc9f011a3269bc06782f916cd3d9eed7c9312"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:511729f456829ef86ac41ca78c63a5cb55240ed23b4b737faca0eb1abb1c41bc"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:88ab34806dea0671532d3f82d82b85e8fc23d7b2dd12fa837978dad9bb392a34"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-win32.whl", hash = "sha256:16a8770207946ac75703458e2c743631c79c59c5890c80011d536248f8eaa432"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:d22dbedd33326a4a5190dd4fe9e9e693ef12160c77382d9e87919bce54f3d4ca"}, + {file = "charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a"}, + {file = "charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14"}, +] + +[[package]] +name = "h11" +version = "0.16.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.8" +files = [ + {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, + {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, +] + +[[package]] +name = "idna" +version = "3.10" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.6" +files = [ + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, +] + +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + +[[package]] +name = "mypy" +version = "1.18.2" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "mypy-1.18.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c1eab0cf6294dafe397c261a75f96dc2c31bffe3b944faa24db5def4e2b0f77c"}, + {file = "mypy-1.18.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7a780ca61fc239e4865968ebc5240bb3bf610ef59ac398de9a7421b54e4a207e"}, + {file = "mypy-1.18.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:448acd386266989ef11662ce3c8011fd2a7b632e0ec7d61a98edd8e27472225b"}, + {file = "mypy-1.18.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f9e171c465ad3901dc652643ee4bffa8e9fef4d7d0eece23b428908c77a76a66"}, + {file = "mypy-1.18.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:592ec214750bc00741af1f80cbf96b5013d81486b7bb24cb052382c19e40b428"}, + {file = "mypy-1.18.2-cp310-cp310-win_amd64.whl", hash = "sha256:7fb95f97199ea11769ebe3638c29b550b5221e997c63b14ef93d2e971606ebed"}, + {file = "mypy-1.18.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:807d9315ab9d464125aa9fcf6d84fde6e1dc67da0b6f80e7405506b8ac72bc7f"}, + {file = "mypy-1.18.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:776bb00de1778caf4db739c6e83919c1d85a448f71979b6a0edd774ea8399341"}, + {file = "mypy-1.18.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1379451880512ffce14505493bd9fe469e0697543717298242574882cf8cdb8d"}, + {file = "mypy-1.18.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1331eb7fd110d60c24999893320967594ff84c38ac6d19e0a76c5fd809a84c86"}, + {file = "mypy-1.18.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ca30b50a51e7ba93b00422e486cbb124f1c56a535e20eff7b2d6ab72b3b2e37"}, + {file = "mypy-1.18.2-cp311-cp311-win_amd64.whl", hash = "sha256:664dc726e67fa54e14536f6e1224bcfce1d9e5ac02426d2326e2bb4e081d1ce8"}, + {file = "mypy-1.18.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:33eca32dd124b29400c31d7cf784e795b050ace0e1f91b8dc035672725617e34"}, + {file = "mypy-1.18.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a3c47adf30d65e89b2dcd2fa32f3aeb5e94ca970d2c15fcb25e297871c8e4764"}, + {file = "mypy-1.18.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d6c838e831a062f5f29d11c9057c6009f60cb294fea33a98422688181fe2893"}, + {file = "mypy-1.18.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01199871b6110a2ce984bde85acd481232d17413868c9807e95c1b0739a58914"}, + {file = "mypy-1.18.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a2afc0fa0b0e91b4599ddfe0f91e2c26c2b5a5ab263737e998d6817874c5f7c8"}, + {file = "mypy-1.18.2-cp312-cp312-win_amd64.whl", hash = "sha256:d8068d0afe682c7c4897c0f7ce84ea77f6de953262b12d07038f4d296d547074"}, + {file = "mypy-1.18.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:07b8b0f580ca6d289e69209ec9d3911b4a26e5abfde32228a288eb79df129fcc"}, + {file = "mypy-1.18.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ed4482847168439651d3feee5833ccedbf6657e964572706a2adb1f7fa4dfe2e"}, + {file = "mypy-1.18.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3ad2afadd1e9fea5cf99a45a822346971ede8685cc581ed9cd4d42eaf940986"}, + {file = "mypy-1.18.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a431a6f1ef14cf8c144c6b14793a23ec4eae3db28277c358136e79d7d062f62d"}, + {file = "mypy-1.18.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7ab28cc197f1dd77a67e1c6f35cd1f8e8b73ed2217e4fc005f9e6a504e46e7ba"}, + {file = "mypy-1.18.2-cp313-cp313-win_amd64.whl", hash = "sha256:0e2785a84b34a72ba55fb5daf079a1003a34c05b22238da94fcae2bbe46f3544"}, + {file = "mypy-1.18.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:62f0e1e988ad41c2a110edde6c398383a889d95b36b3e60bcf155f5164c4fdce"}, + {file = "mypy-1.18.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8795a039bab805ff0c1dfdb8cd3344642c2b99b8e439d057aba30850b8d3423d"}, + {file = "mypy-1.18.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6ca1e64b24a700ab5ce10133f7ccd956a04715463d30498e64ea8715236f9c9c"}, + {file = "mypy-1.18.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d924eef3795cc89fecf6bedc6ed32b33ac13e8321344f6ddbf8ee89f706c05cb"}, + {file = "mypy-1.18.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20c02215a080e3a2be3aa50506c67242df1c151eaba0dcbc1e4e557922a26075"}, + {file = "mypy-1.18.2-cp314-cp314-win_amd64.whl", hash = "sha256:749b5f83198f1ca64345603118a6f01a4e99ad4bf9d103ddc5a3200cc4614adf"}, + {file = "mypy-1.18.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:25a9c8fb67b00599f839cf472713f54249a62efd53a54b565eb61956a7e3296b"}, + {file = "mypy-1.18.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c2b9c7e284ee20e7598d6f42e13ca40b4928e6957ed6813d1ab6348aa3f47133"}, + {file = "mypy-1.18.2-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d6985ed057513e344e43a26cc1cd815c7a94602fb6a3130a34798625bc2f07b6"}, + {file = "mypy-1.18.2-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22f27105f1525ec024b5c630c0b9f36d5c1cc4d447d61fe51ff4bd60633f47ac"}, + {file = "mypy-1.18.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:030c52d0ea8144e721e49b1f68391e39553d7451f0c3f8a7565b59e19fcb608b"}, + {file = "mypy-1.18.2-cp39-cp39-win_amd64.whl", hash = "sha256:aa5e07ac1a60a253445797e42b8b2963c9675563a94f11291ab40718b016a7a0"}, + {file = "mypy-1.18.2-py3-none-any.whl", hash = "sha256:22a1748707dd62b58d2ae53562ffc4d7f8bcc727e8ac7cbc69c053ddc874d47e"}, + {file = "mypy-1.18.2.tar.gz", hash = "sha256:06a398102a5f203d7477b2923dda3634c36727fa5c237d8f859ef90c42a9924b"}, +] + +[package.dependencies] +mypy_extensions = ">=1.0.0" +pathspec = ">=0.9.0" +typing_extensions = ">=4.6.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +faster-cache = ["orjson"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.8" +files = [ + {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, + {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, +] + +[[package]] +name = "python-engineio" +version = "4.12.3" +description = "Engine.IO server and client for Python" +optional = false +python-versions = ">=3.6" +files = [ + {file = "python_engineio-4.12.3-py3-none-any.whl", hash = "sha256:7c099abb2a27ea7ab429c04da86ab2d82698cdd6c52406cb73766fe454feb7e1"}, + {file = "python_engineio-4.12.3.tar.gz", hash = "sha256:35633e55ec30915e7fc8f7e34ca8d73ee0c080cec8a8cd04faf2d7396f0a7a7a"}, +] + +[package.dependencies] +simple-websocket = ">=0.10.0" + +[package.extras] +asyncio-client = ["aiohttp (>=3.4)"] +client = ["requests (>=2.21.0)", "websocket-client (>=0.54.0)"] +docs = ["sphinx"] + +[[package]] +name = "python-socketio" +version = "5.14.1" +description = "Socket.IO server and client for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "python_socketio-5.14.1-py3-none-any.whl", hash = "sha256:3419f5917f0e3942317836a77146cb4caa23ad804c8fd1a7e3f44a6657a8406e"}, + {file = "python_socketio-5.14.1.tar.gz", hash = "sha256:bf49657073b90ee09e4cbd6651044b46bb526694276621e807a1b8fcc0c1b25b"}, +] + +[package.dependencies] +bidict = ">=0.21.0" +python-engineio = ">=4.11.0" +requests = {version = ">=2.21.0", optional = true, markers = "extra == \"client\""} +websocket-client = {version = ">=0.54.0", optional = true, markers = "extra == \"client\""} + +[package.extras] +asyncio-client = ["aiohttp (>=3.4)"] +client = ["requests (>=2.21.0)", "websocket-client (>=0.54.0)"] +docs = ["sphinx"] + +[[package]] +name = "python-vlc" +version = "3.0.21203" +description = "VLC bindings for python." +optional = false +python-versions = "*" +files = [ + {file = "python_vlc-3.0.21203-py3-none-any.whl", hash = "sha256:1613451a31b692ec276296ceeae0c0ba82bfc2d094dabf9aceb70f58944a6320"}, + {file = "python_vlc-3.0.21203.tar.gz", hash = "sha256:52d0544b276b11e58b6c0b748c3e0518f94f74b1b4cd328c83a59eacabead1ec"}, +] + +[[package]] +name = "reactivex" +version = "4.0.4" +description = "ReactiveX (Rx) for Python" +optional = false +python-versions = ">=3.7,<4.0" +files = [ + {file = "reactivex-4.0.4-py3-none-any.whl", hash = "sha256:0004796c420bd9e68aad8e65627d85a8e13f293de76656165dffbcb3a0e3fb6a"}, + {file = "reactivex-4.0.4.tar.gz", hash = "sha256:e912e6591022ab9176df8348a653fe8c8fa7a301f26f9931c9d8c78a650e04e8"}, +] + +[package.dependencies] +typing-extensions = ">=4.1.1,<5.0.0" + +[[package]] +name = "requests" +version = "2.32.5" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.9" +files = [ + {file = "requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"}, + {file = "requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset_normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "ruff" +version = "0.12.12" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruff-0.12.12-py3-none-linux_armv6l.whl", hash = "sha256:de1c4b916d98ab289818e55ce481e2cacfaad7710b01d1f990c497edf217dafc"}, + {file = "ruff-0.12.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7acd6045e87fac75a0b0cdedacf9ab3e1ad9d929d149785903cff9bb69ad9727"}, + {file = "ruff-0.12.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:abf4073688d7d6da16611f2f126be86523a8ec4343d15d276c614bda8ec44edb"}, + {file = "ruff-0.12.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:968e77094b1d7a576992ac078557d1439df678a34c6fe02fd979f973af167577"}, + {file = "ruff-0.12.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42a67d16e5b1ffc6d21c5f67851e0e769517fb57a8ebad1d0781b30888aa704e"}, + {file = "ruff-0.12.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b216ec0a0674e4b1214dcc998a5088e54eaf39417327b19ffefba1c4a1e4971e"}, + {file = "ruff-0.12.12-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:59f909c0fdd8f1dcdbfed0b9569b8bf428cf144bec87d9de298dcd4723f5bee8"}, + {file = "ruff-0.12.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ac93d87047e765336f0c18eacad51dad0c1c33c9df7484c40f98e1d773876f5"}, + {file = "ruff-0.12.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:01543c137fd3650d322922e8b14cc133b8ea734617c4891c5a9fccf4bfc9aa92"}, + {file = "ruff-0.12.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2afc2fa864197634e549d87fb1e7b6feb01df0a80fd510d6489e1ce8c0b1cc45"}, + {file = "ruff-0.12.12-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:0c0945246f5ad776cb8925e36af2438e66188d2b57d9cf2eed2c382c58b371e5"}, + {file = "ruff-0.12.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a0fbafe8c58e37aae28b84a80ba1817f2ea552e9450156018a478bf1fa80f4e4"}, + {file = "ruff-0.12.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b9c456fb2fc8e1282affa932c9e40f5ec31ec9cbb66751a316bd131273b57c23"}, + {file = "ruff-0.12.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5f12856123b0ad0147d90b3961f5c90e7427f9acd4b40050705499c98983f489"}, + {file = "ruff-0.12.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:26a1b5a2bf7dd2c47e3b46d077cd9c0fc3b93e6c6cc9ed750bd312ae9dc302ee"}, + {file = "ruff-0.12.12-py3-none-win32.whl", hash = "sha256:173be2bfc142af07a01e3a759aba6f7791aa47acf3604f610b1c36db888df7b1"}, + {file = "ruff-0.12.12-py3-none-win_amd64.whl", hash = "sha256:e99620bf01884e5f38611934c09dd194eb665b0109104acae3ba6102b600fd0d"}, + {file = "ruff-0.12.12-py3-none-win_arm64.whl", hash = "sha256:2a8199cab4ce4d72d158319b63370abf60991495fb733db96cd923a34c52d093"}, + {file = "ruff-0.12.12.tar.gz", hash = "sha256:b86cd3415dbe31b3b46a71c598f4c4b2f550346d1ccf6326b347cc0c8fd063d6"}, +] + +[[package]] +name = "simple-websocket" +version = "1.1.0" +description = "Simple WebSocket server and client for Python" +optional = false +python-versions = ">=3.6" +files = [ + {file = "simple_websocket-1.1.0-py3-none-any.whl", hash = "sha256:4af6069630a38ed6c561010f0e11a5bc0d4ca569b36306eb257cd9a192497c8c"}, + {file = "simple_websocket-1.1.0.tar.gz", hash = "sha256:7939234e7aa067c534abdab3a9ed933ec9ce4691b0713c78acb195560aa52ae4"}, +] + +[package.dependencies] +wsproto = "*" + +[package.extras] +dev = ["flake8", "pytest", "pytest-cov", "tox"] +docs = ["sphinx"] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +description = "Backported and Experimental Type Hints for Python 3.9+" +optional = false +python-versions = ">=3.9" +files = [ + {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, + {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, +] + +[[package]] +name = "urllib3" +version = "2.5.0" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.9" +files = [ + {file = "urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"}, + {file = "urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "websocket-client" +version = "1.9.0" +description = "WebSocket client for Python with low level API options" +optional = false +python-versions = ">=3.9" +files = [ + {file = "websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef"}, + {file = "websocket_client-1.9.0.tar.gz", hash = "sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98"}, +] + +[package.extras] +docs = ["Sphinx (>=6.0)", "myst-parser (>=2.0.0)", "sphinx_rtd_theme (>=1.1.0)"] +optional = ["python-socks", "wsaccel"] +test = ["pytest", "websockets"] + +[[package]] +name = "wsproto" +version = "1.2.0" +description = "WebSockets state-machine based protocol implementation" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "wsproto-1.2.0-py3-none-any.whl", hash = "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736"}, + {file = "wsproto-1.2.0.tar.gz", hash = "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065"}, +] + +[package.dependencies] +h11 = ">=0.9.0,<1" + +[metadata] +lock-version = "2.0" +python-versions = ">=3.11,<4.0" +content-hash = "a80df18ad34d1fdfb77fe9a5cdaeeace2a61248f735f55fe3506825aef6c8e0d" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..dccb0fc --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,24 @@ +[tool.poetry] +name = "vlchan" +version = "0.1.0" +description = "VLC video player with synchan timecode synchronization" +authors = ["Justin Lin "] +readme = "README.md" +packages = [ + { include = "vlchan" } +] + +[tool.poetry.dependencies] +python = ">=3.11,<4.0" +python-vlc = "^3.0.20123" +reactivex = "^4.0.4" +python-socketio = {extras = ["client"], version = "^5.14.1"} +requests = "^2.32.5" + +[tool.poetry.group.dev.dependencies] +mypy = "^1.17.1" +ruff = "^0.12.12" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..cafb977 --- /dev/null +++ b/setup.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +"""Setup script for VLChan.""" + +from setuptools import setup, find_packages + +with open("README.md", "r", encoding="utf-8") as fh: + long_description = fh.read() + +setup( + name="vlchan", + version="0.1.0", + author="Justin Lin", + author_email="wancat@wancat.cc", + description="VLC video player with synchan timecode synchronization", + long_description=long_description, + long_description_content_type="text/markdown", + packages=find_packages(), + classifiers=[ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + ], + python_requires=">=3.11", + install_requires=[ + "python-vlc>=3.0.20123", + "reactivex>=4.0.4", + "python-socketio[client]>=5.14.1", + "requests>=2.32.5", + ], + extras_require={ + "dev": [ + "mypy>=1.17.1", + "ruff>=0.12.12", + ], + }, + entry_points={ + "console_scripts": [ + "vlchan=vlchan.player:main", + ], + }, +) diff --git a/test_vlchan.py b/test_vlchan.py new file mode 100644 index 0000000..7b8cab4 --- /dev/null +++ b/test_vlchan.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 +"""Test script to verify VLChan installation and basic functionality.""" + +import sys +import time +from vlchan import VLChanPlayer, SynchanController, SynchanState + + +def test_imports(): + """Test that all modules can be imported.""" + print("Testing imports...") + try: + from vlchan import VLChanPlayer, SynchanController, SynchanState + print("✓ All imports successful") + return True + except ImportError as e: + print(f"✗ Import error: {e}") + return False + + +def test_vlc_availability(): + """Test that VLC is available.""" + print("Testing VLC availability...") + try: + import vlc + instance = vlc.Instance() + print("✓ VLC is available") + instance.release() + return True + except Exception as e: + print(f"✗ VLC error: {e}") + print(" Make sure VLC is installed on your system") + return False + + +def test_synchan_controller(): + """Test synchan controller creation.""" + print("Testing synchan controller...") + try: + controller = SynchanController("http://localhost:3000") + print("✓ Synchan controller created") + return True + except Exception as e: + print(f"✗ Synchan controller error: {e}") + return False + + +def test_player_creation(): + """Test player creation without video file.""" + print("Testing player creation...") + try: + player = VLChanPlayer("http://localhost:3000") + print("✓ Player created successfully") + player.cleanup() + return True + except Exception as e: + print(f"✗ Player creation error: {e}") + return False + + +def test_synchan_state(): + """Test synchan state dataclass.""" + print("Testing synchan state...") + try: + state = SynchanState( + playing=True, + currentTime=10.5, + duration=120.0, + loop=False, + latency=0.05 + ) + print(f"✓ Synchan state created: {state}") + return True + except Exception as e: + print(f"✗ Synchan state error: {e}") + return False + + +def main(): + """Run all tests.""" + print("VLChan Test Suite") + print("=" * 40) + + tests = [ + test_imports, + test_vlc_availability, + test_synchan_controller, + test_player_creation, + test_synchan_state, + ] + + passed = 0 + total = len(tests) + + for test in tests: + if test(): + passed += 1 + print() + + print("=" * 40) + print(f"Tests passed: {passed}/{total}") + + if passed == total: + print("✓ All tests passed! VLChan is ready to use.") + return 0 + else: + print("✗ Some tests failed. Please check the errors above.") + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/vlchan/__init__.py b/vlchan/__init__.py new file mode 100644 index 0000000..ad5dafe --- /dev/null +++ b/vlchan/__init__.py @@ -0,0 +1,6 @@ +"""VLC video player with synchan timecode synchronization.""" + +from .player import VLChanPlayer +from .synchan import SynchanController, SynchanState + +__all__ = ["VLChanPlayer", "SynchanController", "SynchanState"] diff --git a/vlchan/__pycache__/__init__.cpython-313.pyc b/vlchan/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2c89cee76d5fceb48fe4751274bdd64929116572 GIT binary patch literal 370 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 literal 0 HcmV?d00001 diff --git a/vlchan/__pycache__/synchan.cpython-313.pyc b/vlchan/__pycache__/synchan.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..723b9386143faa6ede402b8d8105379622f1a926 GIT binary patch literal 5988 zcmc&2OK=p&wdc1x`_*4aLZA@_;x+cJu)qlzQ#P_)E(v6pMv5WYcs&};N(;L)OV2EZ z%>|dMY>dkVt`rsJ6b?DS&8M6l=aQpj$t8I#s^W?h7q=x6Whc4hy`Gufl@LyD>Dqn$ zrr+z|`}OPINF<^J%Ei~dn~x<3`73?|CFCO;Jscsoh)O6?Ih7ydMtRCBhznx^6<94& z5o&Qv8V%79qe)}oQJKm(P@{462bE<&=q_K{$QiD7@LrYhTa6r_PgA#=iV79MznHWh!}#Do&wf=+>;E*vLmMvruKk^nDyA#krAL&C&FtX4~WN6@Puk z8|ai~8}J?VzO|VmesxUFQ$w45!?KF1K5xuciUx(|_D1t7mkqt*SX9T+gCh9I#%Vxq zku0Gc=#y7DDjXmxPsOa14XMIUfSL^hCe|^DVMq={ z%3U&^inwwvXKE!Qmvf`JT**QS03OffE>^UnPie{J@&#%;#e!*=7Ldca+^nSoBF=#1 zT%c>FX~BX)Nj5Y~`J$ycZdWd6W22ta9ETQWDvkkEihJ6CKXFr;d8=e(-qB25b22v7 zGna~}`=DYf*35MI3XK3i1K8Uh0Pq3%MjTp8w7qv`t*v`m>|RTDy!ZNAH2L1>TB7~^ zaoQL3*jaTi19_|JP;qKZ6|&M1UKM?F4yjT$4BsJU__7-AgJqx#(Wn}svAJj}?zZ_V z8?j7>T1Bv-`#5>^=|%%HU4J`)W$RX{R51&%br4(HjrrtUg%)++J7E+;5J!vK*azSi zIon!<+AHAQ{17)qioO>SGDlLvcr}hQg8@7uDqHPrOtkuT(6ou|a4ls{`S#Ut7eUEH19nkgwr`-kMDp}ioa7FG}>p8gigA&7!x8eZ!7!A6qyKuYZEOA zjyTK%O>S!R(dk&wHpK<=sKPP9I+Z zJGLvrP!3a!$0@*}wyCnZp{2zmeq;HZt_R073;K8*`*_oGU~cAV12*maeBD<^Ko z7EUc3SxhP`^5A;B^TwI$XVzLeR$B&^S_YtXQ5jl{A71P1`OWE1POrCh-kkXLt_NW; z5xvH3MoA=jO}ml*F#mCa=~=Jq5k|*zgXuU62ze~ zvC1Fg@!&{_)fR}6)KM@3&S?6D^j1-0^IeV!aQv=FQ!p)k3IVzSYX&L`)5i7twl89@ zokWQ3hmaWqH;S)r#%Sfb$VKd|OS%4Hz>Sjqu zjKkgas5LPH_W3bc9A1n+>)TI57QW>(4UFIAs$KJrQ$C)_6fIpV&V$V#A08SSO2ug# zuBaVB2ZBxn7;Gr2fIb1h6~JX^8b6O9_$h*31bA3@2LVPiih+s&49{%E?~|{@=N^c> zbmI2NLjqMzmZk2S_9lVqADxmEeV7E`je*@=+42(#wsIZqJ1BK~{|v0?E%KK9-)%lf zj!N>Ryp#2-TwgtbGKHOarpOUE2H<>>gV9X!ahy{!oMplmUg>eXFe;vcjZs6>7`0hZ zr73=El-^5V96Y7Bl_@wo)iC-g(Ccxx!4#|+{5Ud5rt2{`OtfQ!nBk^5<*D%2IVZ_P zOJk$}n+i9gG;pB(Y%7g92Ot$T!!L#BB3_;6^5g|!faJMUw0dX+l0yeFO-sLEIErqW zrlGS$6k@r5TKk82T5p~LjRK8z1MO_<;nr9hoYeeyb+7MJ&7(uyhy1i)LqeM3=#wzU zHbZv9{#x9bcpZ?F{4EfP%Ap)gZjzs9J{F-3%XD3gz{w*g z)Pi4&el7WR2y1X7sY%SjPGVMNNNN1z6VpG8A2r!ZnuABm(rbl=N5HWY{kCPT zU%9_+Uv*v+v!+O^n{JcQq1h#yCPSdVpX5SVCPPAs;j?_9j2 zZx2d?D{_Bbk*o5+l05M5S{S*chk5w|{@;(&6}SZsrZ< zcKKgrcnr$b-IZeHV(pJG%~!61g7vC>GO8a720(HsH|fZJUcOIn-J)Af8GPb;^xL2OtI zMI&%Knn+~>-rOu|yAdluDP~vAOm#^yZFrZ`3FL`*@!)*YSP(9xnQT0I0plr8s5b^qu~t z{^x37{L11R>L%6?&T=s>bX%wqs6Kn@&)KC@zj%l^L{2e&t-37qej~Ooi=C_D-X(GG QLgh>GKuscIAJgZ50ICZm`~Uy| literal 0 HcmV?d00001 diff --git a/vlchan/player.py b/vlchan/player.py new file mode 100644 index 0000000..b157347 --- /dev/null +++ b/vlchan/player.py @@ -0,0 +1,207 @@ +"""VLC video player with synchan timecode synchronization.""" + +import vlc +import time +import threading +from typing import Optional, Callable +from reactivex import Observable +from .synchan import SynchanState, SynchanController, create_synchan + + +class VLChanPlayer: + """VLC video player synchronized with synchan server.""" + + def __init__(self, synchan_url: str = "http://localhost:3000", + video_path: Optional[str] = None): + """Initialize the VLC player with synchan synchronization. + + Args: + synchan_url: URL of the synchan server + video_path: Path to video file to play + """ + self.synchan_url = synchan_url + self.video_path = video_path + + # Initialize VLC + self.instance = vlc.Instance() + self.player = self.instance.media_player_new() + + # Initialize synchan + self.synchan_controller = SynchanController(synchan_url) + self.synchan_observable: Optional[Observable[SynchanState]] = None + self.synchan_subscription = None + + # State tracking + self._is_playing = False + self.last_sync_time = 0 + self.sync_threshold = 0.1 # 100ms threshold for sync + + # Callbacks + self.on_state_change: Optional[Callable[[SynchanState], None]] = None + + # Load video if provided + if video_path: + self.load_video(video_path) + + def load_video(self, video_path: str): + """Load a video file.""" + self.video_path = video_path + media = self.instance.media_new(video_path) + self.player.set_media(media) + print(f"Loaded video: {video_path}") + + def start_sync(self): + """Start synchronization with synchan server.""" + if self.synchan_observable is not None: + print("Sync already started") + return + + self.synchan_observable = create_synchan(self.synchan_url) + self.synchan_subscription = self.synchan_observable.subscribe( + on_next=self._handle_synchan_state, + on_error=self._handle_synchan_error, + on_completed=self._handle_synchan_completed + ) + print("Started synchan synchronization") + + def stop_sync(self): + """Stop synchronization with synchan server.""" + if self.synchan_subscription: + self.synchan_subscription.dispose() + self.synchan_subscription = None + self.synchan_observable = None + print("Stopped synchan synchronization") + + def _handle_synchan_state(self, state: SynchanState): + """Handle state updates from synchan server.""" + if self.on_state_change: + self.on_state_change(state) + + # Sync playback state + if state.playing != self._is_playing: + if state.playing: + self.play() + else: + self.pause() + self._is_playing = state.playing + + # Sync time position + current_time = self.player.get_time() / 1000.0 # Convert to seconds + time_diff = abs(current_time - state.currentTime) + + if time_diff > self.sync_threshold: + # Seek to match synchan time + seek_time = int(state.currentTime * 1000) # Convert to milliseconds + self.player.set_time(seek_time) + self.last_sync_time = time.time() + print(f"Synced to time: {state.currentTime:.2f}s (diff: {time_diff:.2f}s)") + + def _handle_synchan_error(self, error): + """Handle synchan connection errors.""" + print(f"Synchan error: {error}") + # Optionally attempt to reconnect + threading.Timer(5.0, self.start_sync).start() + + def _handle_synchan_completed(self): + """Handle synchan connection completion.""" + print("Synchan connection completed") + + def play(self): + """Start video playback.""" + if self.player.get_media(): + self.player.play() + self._is_playing = True + print("Started playback") + else: + print("No media loaded") + + def pause(self): + """Pause video playback.""" + self.player.pause() + self._is_playing = False + print("Paused playback") + + def stop(self): + """Stop video playback.""" + self.player.stop() + self._is_playing = False + print("Stopped playback") + + def seek(self, time_seconds: float): + """Seek to a specific time position.""" + time_ms = int(time_seconds * 1000) + self.player.set_time(time_ms) + print(f"Seeked to {time_seconds:.2f}s") + + def get_position(self) -> float: + """Get current playback position in seconds.""" + return self.player.get_time() / 1000.0 + + def get_duration(self) -> float: + """Get video duration in seconds.""" + return self.player.get_length() / 1000.0 + + def is_playing(self) -> bool: + """Check if video is currently playing.""" + return self.player.is_playing() + + def set_volume(self, volume: int): + """Set volume (0-100).""" + self.player.audio_set_volume(int(volume)) + + def get_volume(self) -> int: + """Get current volume (0-100).""" + return self.player.audio_get_volume() + + def set_fullscreen(self, fullscreen: bool): + """Set fullscreen mode.""" + self.player.set_fullscreen(fullscreen) + + def cleanup(self): + """Clean up resources.""" + self.stop_sync() + self.stop() + self.player.release() + self.instance.release() + print("Cleaned up VLC player") + + +def main(): + """Example usage of VLChanPlayer.""" + import sys + + if len(sys.argv) < 2: + print("Usage: python -m vlchan.player [synchan_url]") + sys.exit(1) + + video_path = sys.argv[1] + synchan_url = sys.argv[2] if len(sys.argv) > 2 else "http://localhost:3000" + + # Create player + player = VLChanPlayer(synchan_url, video_path) + + # Set up state change callback + def on_state_change(state: SynchanState): + print(f"Synchan state: playing={state.playing}, time={state.currentTime:.2f}s") + + player.on_state_change = on_state_change + + # Start synchronization + player.start_sync() + + try: + # Start playback + player.play() + + # Keep running until interrupted + while True: + time.sleep(1) + + except KeyboardInterrupt: + print("\nShutting down...") + finally: + player.cleanup() + + +if __name__ == "__main__": + main() diff --git a/vlchan/synchan.py b/vlchan/synchan.py new file mode 100644 index 0000000..513cea8 --- /dev/null +++ b/vlchan/synchan.py @@ -0,0 +1,118 @@ +"""Synchan server integration for timecode synchronization.""" + +from dataclasses import dataclass +import multiprocessing +import reactivex +from reactivex.abc.observer import ObserverBase +from reactivex.observable import Observable +import reactivex.operators as ops +from reactivex.scheduler import ThreadPoolScheduler +import requests +import socketio +import threading +from concurrent.futures import ThreadPoolExecutor + + +@dataclass +class SynchanState: + """State information from synchan server.""" + playing: bool + currentTime: float + duration: float + loop: bool + latency: float + + +class SynchanController: + """Controller for synchan server communication.""" + + def __init__(self, synchan_url: str = "http://localhost:3000"): + self.synchan_url = synchan_url + self.headers = {"Content-Type": "application/json"} + + def seek(self, to: int): + """Seek to a specific time position.""" + print(f"Seeking to {to}") + r = requests.post( + f"{self.synchan_url}/trpc/admin.seek", + headers=self.headers, + data=str(to) + ) + print(r.text) + + def play(self): + """Start playback.""" + requests.post( + f"{self.synchan_url}/trpc/admin.play", + headers=self.headers + ) + + def pause(self): + """Pause playback.""" + requests.post( + f"{self.synchan_url}/trpc/admin.pause", + headers=self.headers + ) + + +def create_socket(observer: ObserverBase[SynchanState], scheduler, synchan_url: str): + """Create socket connection to synchan server.""" + sio = socketio.Client() + + @sio.event + def connect(): + print("Synchan connection established") + + @sio.event + def connect_error(data): + print(f"Synchan connection failed: {data}") + observer.on_error(data) + observer.on_completed() + + @sio.event + def control(data): + """Handle control events from synchan server.""" + nonce = data["nonce"] + observer.on_next( + SynchanState( + playing=data["playing"], + currentTime=data["currentTime"], + duration=data["duration"], + loop=data["loop"], + latency=data["latency"], + ) + ) + sio.emit("ping", nonce) + + @sio.event + def disconnect(): + print("Disconnected from synchan server") + observer.on_completed() + + print(f"Connecting to synchan at {synchan_url}") + try: + sio.connect(synchan_url, wait_timeout=5) + sio.wait() + except Exception as e: + print(f"Failed to connect to synchan: {e}") + observer.on_error(e) + observer.on_completed() + + +def create_synchan(synchan_url: str = "http://localhost:3000") -> Observable[SynchanState]: + """Create an observable stream of synchan state updates.""" + # Create a thread pool for socket operations + thread_count = multiprocessing.cpu_count() + thread_pool_scheduler = ThreadPoolScheduler(thread_count) + + return reactivex.create( + lambda observer, scheduler: create_socket(observer, scheduler, synchan_url) + ).pipe( + ops.share(), + ops.subscribe_on(thread_pool_scheduler) + ) + + +if __name__ == "__main__": + # Test the synchan connection + create_synchan().subscribe(print)