From b5db8affc9a6f381c5dcba3767b3625717055a76 Mon Sep 17 00:00:00 2001 From: Brent Yi Date: Wed, 29 Nov 2023 17:29:42 -0800 Subject: [PATCH 1/7] Add API for accessing host, port, share URL after instantiation --- src/viser/_tunnel.py | 10 +++++- src/viser/_viser.py | 81 ++++++++++++++++++++++++++++++++++++++------ 2 files changed, 79 insertions(+), 12 deletions(-) diff --git a/src/viser/_tunnel.py b/src/viser/_tunnel.py index f762acc86..e4b4f02a6 100644 --- a/src/viser/_tunnel.py +++ b/src/viser/_tunnel.py @@ -3,7 +3,7 @@ import threading import time from multiprocessing.managers import DictProxy -from typing import Callable, Optional +from typing import Callable, Literal, Optional import requests @@ -46,6 +46,11 @@ def get_url(self) -> Optional[str]: """Get tunnel URL. None if not connected (or connection failed).""" return self._shared_state["url"] + def get_status( + self, + ) -> Literal["ready", "connecting", "failed", "connected", "closed"]: + return self._shared_state["status"] + def close(self) -> None: """Close the tunnel.""" if self._process is not None: @@ -104,6 +109,9 @@ async def _make_tunnel(local_port: int, shared_state: DictProxy) -> None: ] ) + shared_state["url"] = None + shared_state["status"] = "closed" + async def _simple_proxy( local_host: str, diff --git a/src/viser/_viser.py b/src/viser/_viser.py index dfe3c14ae..949b20852 100644 --- a/src/viser/_viser.py +++ b/src/viser/_viser.py @@ -446,25 +446,71 @@ def _() -> None: ) self.world_axes.visible = False + def get_host(self) -> str: + """Returns the host address of the Viser server. + + Returns: + Host address as string. + """ + return self._server._host + + def get_port(self) -> int: + """Returns the port of the Viser server. This could be different from the + originally requested one. + + Returns: + Port as integer. + """ + return self._server._port + + def get_share_url(self) -> Optional[str]: + """Returns a share URL for the Viser server. If one does not exist, a new one + will be requested and the function will block until it is ready. + + Returns: + Share URL as string, or None if connection fails or is closed. + """ + + if self._share_tunnel is not None: + # Tunnel already exists. + while self._share_tunnel.get_status() in ("ready", "connecting"): + time.sleep(0.05) + return self._share_tunnel.get_url() + else: + # Create a new tunnel!. + rich.print( + "[bold](viser)[/bold] Share URL requested! (expires in 24 hours)" + ) + self._share_tunnel = _ViserTunnel(self._server._port) + + connect_event = threading.Event() + + @self._share_tunnel.on_connect + def _() -> None: + assert self._share_tunnel is not None + share_url = self._share_tunnel.get_url() + if share_url is None: + rich.print("[bold](viser)[/bold] Could not generate share URL") + else: + rich.print(f"[bold](viser)[/bold] Generated share URL: {share_url}") + connect_event.set() + + connect_event.wait() + return self._share_tunnel.get_url() + def stop(self) -> None: """Stop the Viser server and associated threads and tunnels.""" self._server.stop() if self._share_tunnel is not None: self._share_tunnel.close() - @override - def _get_api(self) -> MessageApi: - """Message API to use.""" - return self - - @override - def _queue_unsafe(self, message: _messages.Message) -> None: - """Define how the message API should send messages.""" - self._server.broadcast(message) - def get_clients(self) -> Dict[int, ClientHandle]: """Creates and returns a copy of the mapping from connected client IDs to - handles.""" + handles. + + Returns: + Dictionary of clients. + """ with self._state.client_lock: return self._state.connected_clients.copy() @@ -502,6 +548,9 @@ def atomic(self) -> Generator[None, None, None]: This can be helpful for things like animations, or when we want position and orientation updates to happen synchronously. + + Returns: + Context manager. """ # Acquire the global atomic lock. # If called multiple times in the same thread, we ignore inner calls. @@ -530,3 +579,13 @@ def flush(self) -> None: """Flush the outgoing message buffer. Any buffered messages will immediately be sent. (by default they are windowed)""" self._server.flush() + + @override + def _get_api(self) -> MessageApi: + """Message API to use.""" + return self + + @override + def _queue_unsafe(self, message: _messages.Message) -> None: + """Define how the message API should send messages.""" + self._server.broadcast(message) From bc7e1548f8507bdbeaa2ea59e2bba50bd4a31bba Mon Sep 17 00:00:00 2001 From: Brent Yi Date: Wed, 29 Nov 2023 18:07:49 -0800 Subject: [PATCH 2/7] Deprecate old share URL api --- examples/08_smplx_visualizer.py | 5 ++- src/viser/_viser.py | 68 +++++++++++++++++---------------- 2 files changed, 40 insertions(+), 33 deletions(-) diff --git a/examples/08_smplx_visualizer.py b/examples/08_smplx_visualizer.py index 93b2dc028..72e73f8f4 100644 --- a/examples/08_smplx_visualizer.py +++ b/examples/08_smplx_visualizer.py @@ -36,7 +36,10 @@ def main( ext: Literal["npz", "pkl"] = "npz", share: bool = False, ) -> None: - server = viser.ViserServer(share=share) + server = viser.ViserServer() + if share: + server.get_share_url() + server.configure_theme(control_layout="collapsible") model = smplx.create( model_path=str(model_path), diff --git a/src/viser/_viser.py b/src/viser/_viser.py index 949b20852..3e9a265c6 100644 --- a/src/viser/_viser.py +++ b/src/viser/_viser.py @@ -302,15 +302,19 @@ class ViserServer(MessageApi, GuiApi): Args: host: Host to bind server to. port: Port to bind server to. - share: Experimental. If set to `True`, create and print a public, shareable URL - for this instance of viser. """ world_axes: FrameHandle """Handle for manipulating the world frame axes (/WorldAxes), which is instantiated and then hidden by default.""" - def __init__(self, host: str = "0.0.0.0", port: int = 8080, share: bool = False): + # Hide deprecated arguments from docstring and type checkers. + def __init__(self, host: str = "0.0.0.0", port: int = 8080): + ... + + def _actual_init( + self, host: str = "0.0.0.0", port: int = 8080, **_deprecated_kwargs + ): server = infra.Server( host=host, port=port, @@ -414,26 +418,15 @@ def _(conn: infra.ClientConnection) -> None: ) table.add_row("HTTP", http_url) table.add_row("Websocket", ws_url) + rich.print(Panel(table, title="[bold]viser[/bold]", expand=False)) - # Create share tunnel if requested. - if not share: - self._share_tunnel = None - rich.print(Panel(table, title="[bold]viser[/bold]", expand=False)) - else: - rich.print( - "[bold](viser)[/bold] Share URL requested! (expires in 24 hours)" - ) - self._share_tunnel = _ViserTunnel(port) + self._share_tunnel = None - @self._share_tunnel.on_connect - def _() -> None: - assert self._share_tunnel is not None - share_url = self._share_tunnel.get_url() - if share_url is None: - rich.print("[bold](viser)[/bold] Could not generate share URL") - else: - table.add_row("Share URL", share_url) - rich.print(Panel(table, title="[bold]viser[/bold]", expand=False)) + # Create share tunnel if requested. + # This is deprecated: we should use get_share_url() instead. + share = _deprecated_kwargs.get("share", False) + if share: + self.get_share_url() self.reset_scene() self.world_axes = FrameHandle( @@ -463,9 +456,13 @@ def get_port(self) -> int: """ return self._server._port - def get_share_url(self) -> Optional[str]: - """Returns a share URL for the Viser server. If one does not exist, a new one - will be requested and the function will block until it is ready. + def get_share_url(self, verbose: bool = True) -> Optional[str]: + """Request a share URL for the Viser server, which allows for public access. + On the first call, will block until a connecting with the share URL server is + established. Afterwards, the URL will be returned directly. + + This is an experimental feature that relies on an external server; it shouldn't + be relied on for critical applications. Returns: Share URL as string, or None if connection fails or is closed. @@ -478,9 +475,10 @@ def get_share_url(self) -> Optional[str]: return self._share_tunnel.get_url() else: # Create a new tunnel!. - rich.print( - "[bold](viser)[/bold] Share URL requested! (expires in 24 hours)" - ) + if verbose: + rich.print( + "[bold](viser)[/bold] Share URL requested! (expires in 24 hours)" + ) self._share_tunnel = _ViserTunnel(self._server._port) connect_event = threading.Event() @@ -488,11 +486,14 @@ def get_share_url(self) -> Optional[str]: @self._share_tunnel.on_connect def _() -> None: assert self._share_tunnel is not None - share_url = self._share_tunnel.get_url() - if share_url is None: - rich.print("[bold](viser)[/bold] Could not generate share URL") - else: - rich.print(f"[bold](viser)[/bold] Generated share URL: {share_url}") + if verbose: + share_url = self._share_tunnel.get_url() + if share_url is None: + rich.print("[bold](viser)[/bold] Could not generate share URL") + else: + rich.print( + f"[bold](viser)[/bold] Generated share URL: {share_url}" + ) connect_event.set() connect_event.wait() @@ -589,3 +590,6 @@ def _get_api(self) -> MessageApi: def _queue_unsafe(self, message: _messages.Message) -> None: """Define how the message API should send messages.""" self._server.broadcast(message) + + +ViserServer.__init__ = ViserServer._actual_init From 1938726cce9fa7fef659a8a39cc244eb3c773b70 Mon Sep 17 00:00:00 2001 From: Brent Yi Date: Wed, 29 Nov 2023 18:09:59 -0800 Subject: [PATCH 3/7] get_share_url() => request_share_url() --- examples/08_smplx_visualizer.py | 2 +- src/viser/_viser.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/08_smplx_visualizer.py b/examples/08_smplx_visualizer.py index 72e73f8f4..a6d965cab 100644 --- a/examples/08_smplx_visualizer.py +++ b/examples/08_smplx_visualizer.py @@ -38,7 +38,7 @@ def main( ) -> None: server = viser.ViserServer() if share: - server.get_share_url() + server.request_share_url() server.configure_theme(control_layout="collapsible") model = smplx.create( diff --git a/src/viser/_viser.py b/src/viser/_viser.py index 3e9a265c6..31ce0acfd 100644 --- a/src/viser/_viser.py +++ b/src/viser/_viser.py @@ -426,7 +426,7 @@ def _(conn: infra.ClientConnection) -> None: # This is deprecated: we should use get_share_url() instead. share = _deprecated_kwargs.get("share", False) if share: - self.get_share_url() + self.request_share_url() self.reset_scene() self.world_axes = FrameHandle( @@ -456,7 +456,7 @@ def get_port(self) -> int: """ return self._server._port - def get_share_url(self, verbose: bool = True) -> Optional[str]: + def request_share_url(self, verbose: bool = True) -> Optional[str]: """Request a share URL for the Viser server, which allows for public access. On the first call, will block until a connecting with the share URL server is established. Afterwards, the URL will be returned directly. From 53feb2135ea68081c95a603e18422b0bffca3da7 Mon Sep 17 00:00:00 2001 From: Brent Yi Date: Wed, 29 Nov 2023 18:12:26 -0800 Subject: [PATCH 4/7] Bump version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d0b4a2178..a25194732 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "viser" -version = "0.1.12" +version = "0.1.13" description = "3D visualization + Python" readme = "README.md" license = { text="MIT" } From dc4d44373f7dd048ee53aaa8719f1f4011508bd6 Mon Sep 17 00:00:00 2001 From: Brent Yi Date: Wed, 29 Nov 2023 18:45:54 -0800 Subject: [PATCH 5/7] Update Record3D example --- examples/07_record3d_visualizer.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/07_record3d_visualizer.py b/examples/07_record3d_visualizer.py index ebae783c4..9dcb1b6a9 100644 --- a/examples/07_record3d_visualizer.py +++ b/examples/07_record3d_visualizer.py @@ -22,7 +22,9 @@ def main( max_frames: int = 100, share: bool = False, ) -> None: - server = viser.ViserServer(share=share) + server = viser.ViserServer() + if share: + server.request_share_url() print("Loading frames!") loader = viser.extras.Record3dLoader(data_path) From 9f9cae14a87ceebe114aca5bc86eafd54f5f11d9 Mon Sep 17 00:00:00 2001 From: Brent Yi Date: Wed, 29 Nov 2023 19:10:00 -0800 Subject: [PATCH 6/7] Types --- src/viser/_viser.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/viser/_viser.py b/src/viser/_viser.py index 31ce0acfd..2e3d36de9 100644 --- a/src/viser/_viser.py +++ b/src/viser/_viser.py @@ -420,7 +420,7 @@ def _(conn: infra.ClientConnection) -> None: table.add_row("Websocket", ws_url) rich.print(Panel(table, title="[bold]viser[/bold]", expand=False)) - self._share_tunnel = None + self._share_tunnel: Optional[_ViserTunnel] = None # Create share tunnel if requested. # This is deprecated: we should use get_share_url() instead. @@ -479,10 +479,11 @@ def request_share_url(self, verbose: bool = True) -> Optional[str]: rich.print( "[bold](viser)[/bold] Share URL requested! (expires in 24 hours)" ) - self._share_tunnel = _ViserTunnel(self._server._port) connect_event = threading.Event() + self._share_tunnel = _ViserTunnel(self._server._port) + @self._share_tunnel.on_connect def _() -> None: assert self._share_tunnel is not None From bddadf2a34d3fd8e6d91a2494ba8c755c0ce5fcb Mon Sep 17 00:00:00 2001 From: Brent Yi Date: Wed, 29 Nov 2023 20:08:50 -0800 Subject: [PATCH 7/7] Types --- src/viser/_viser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/viser/_viser.py b/src/viser/_viser.py index 2e3d36de9..d48dc6b28 100644 --- a/src/viser/_viser.py +++ b/src/viser/_viser.py @@ -593,4 +593,4 @@ def _queue_unsafe(self, message: _messages.Message) -> None: self._server.broadcast(message) -ViserServer.__init__ = ViserServer._actual_init +ViserServer.__init__ = ViserServer._actual_init # type: ignore