def connect(self): if self.is_connected(): raise RuntimeError("Client.connect : already connected") try: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.socket = Socket(sock) self.socket.connect((self.host, self.port)) local_address = self.socket.getsockname() logger.info( "Connecting from local %s:%s to %s:%s", local_address[0], local_address[1], self.host, self.port, ) self.send_command(common.Command(common.MessageType.CLIENT_ID)) self.send_command(common.Command(common.MessageType.LIST_CLIENTS)) self.send_command(common.Command(common.MessageType.LIST_ROOMS)) except ConnectionRefusedError: self.socket = None except common.ClientDisconnectedException: self.handle_connection_lost() except Exception as e: logger.error("Connection error %s", e, exc_info=True) self.socket = None raise
def __init__(self, server: Server, room_name: str, creator: Connection): self.name = room_name self.keep_open = False # Should the room remain open when no more clients are inside ? self.byte_size = 0 self.joinable = False # A room becomes joinable when its first client has send all the initial content self.custom_attributes: Dict[str, Any] = { } # custom attributes are used between clients, but not by the server self._commands: List[common.Command] = [] self._commands_mutex: threading.RLock = threading.RLock() self._connections: List[Connection] = [creator] self.join_count: int = 0 # this is used to ensure a room cannot be deleted while clients are joining (creator is not considered to be joining) # Server is responsible of increasing / decreasing join_count, with mutex protection creator.room = self creator.send_command( common.Command(common.MessageType.JOIN_ROOM, common.encode_string(self.name))) creator.send_command( common.Command(common.MessageType.CONTENT) ) # self.joinable will be set to true by creator later
def send_animation_buffer(self, obj_name, animation_data, channel_name, channel_index=-1): if not animation_data: return action = animation_data.action if not action: buffer = ( common.encode_string(obj_name) + common.encode_string(channel_name) + common.encode_int(channel_index) + common.int_to_bytes(0, 4) # send empty buffer ) self.add_command(common.Command(MessageType.ANIMATION, buffer, 0)) return for fcurve in action.fcurves: if fcurve.data_path == channel_name: if channel_index == -1 or fcurve.array_index == channel_index: key_count = len(fcurve.keyframe_points) times = [] values = [] for keyframe in fcurve.keyframe_points: times.append(int(keyframe.co[0])) values.append(keyframe.co[1]) buffer = ( common.encode_string(obj_name) + common.encode_string(channel_name) + common.encode_int(channel_index) + common.int_to_bytes(key_count, 4) + struct.pack(f"{len(times)}i", *times) + struct.pack(f"{len(values)}f", *values) ) self.add_command(common.Command(MessageType.ANIMATION, buffer, 0)) return
def broadcast_client_update(self, connection: Connection, attributes: Dict[str, Any]): if attributes == {}: return self.broadcast_to_all_clients( common.Command(common.MessageType.CLIENT_UPDATE, common.encode_json({connection.unique_id: attributes})) )
def send_material(self, material): if not material: return if material.grease_pencil: grease_pencil_api.send_grease_pencil_material(self, material) else: self.add_command(common.Command(MessageType.MATERIAL, material_api.get_material_buffer(self, material), 0))
def send_collection_instance(client: Client, obj): if not obj.instance_collection: return instance_name = obj.name_full instanciated_collection = obj.instance_collection.name_full buffer = common.encode_string(instance_name) + common.encode_string(instanciated_collection) client.add_command(common.Command(common.MessageType.INSTANCE_COLLECTION, buffer, 0))
def send_camera_attributes(self, obj): buffer = (common.encode_string(obj.name_full) + common.encode_float(obj.data.lens) + common.encode_float(obj.data.dof.aperture_fstop) + common.encode_float(obj.data.dof.focus_distance)) self.add_command( common.Command(MessageType.CAMERA_ATTRIBUTES, buffer, 0))
def send_group_begin(self): # The integer sent is for future use: the server might fill it with the group size once all messages # have been received, and give the opportunity to future clients to know how many messages they need to process # in the group (en probably show a progress bar to their user if their is a lot of message, e.g. initial scene # creation) self.add_command( common.Command(MessageType.GROUP_BEGIN, common.encode_int(0)))
def send_texture_data(self, path, data): name_buffer = common.encode_string(path) self.textures.add(path) self.add_command( common.Command(MessageType.TEXTURE, name_buffer + common.encode_int(len(data)) + data, 0))
def send_mesh(self, obj): logger.info("send_mesh %s", obj.name_full) mesh = obj.data mesh_name = self.get_mesh_name(mesh) path = self.get_object_path(obj) binary_buffer = common.encode_string(path) + common.encode_string(mesh_name) binary_buffer += mesh_api.encode_mesh( obj, get_mixer_prefs().send_base_meshes, get_mixer_prefs().send_baked_meshes ) # For now include material slots in the same message, but maybe it should be a separated message # like Transform material_link_dict = {"OBJECT": 0, "DATA": 1} material_links = [material_link_dict[slot.link] for slot in obj.material_slots] assert len(material_links) == len(obj.data.materials) binary_buffer += struct.pack(f"{len(material_links)}I", *material_links) for slot in obj.material_slots: if slot.link == "DATA": binary_buffer += common.encode_string("") else: binary_buffer += common.encode_string(slot.material.name if slot.material is not None else "") self.add_command(common.Command(MessageType.MESH, binary_buffer, 0))
def send_add_object_to_collection(client: Client, collection_name, obj_name): logger.info("send_add_object_to_collection %s <- %s", collection_name, obj_name) buffer = common.encode_string(collection_name) + common.encode_string( obj_name) client.add_command( common.Command(common.MessageType.ADD_OBJECT_TO_COLLECTION, buffer, 0))
def send_light_attributes(self, obj): buffer = ( common.encode_string(obj.name_full) + common.encode_float(obj.data.energy) + common.encode_color(obj.data.color) ) self.add_command(common.Command(MessageType.LIGHT_ATTRIBUTES, buffer, 0))
def _leave_room(command: common.Command): if self.room is None: _send_error("Received leave_room but no room is joined") return _ = command.data.decode() # todo remove room_name from protocol self._server.leave_room(self) self.send_command(common.Command(common.MessageType.LEAVE_ROOM))
def add_client(self, connection: Connection): logger.info(f"Add Client {connection.unique_id} to Room {self.name}") connection.send_command(common.Command(common.MessageType.CLEAR_CONTENT)) # todo temporary size stored here offset = 0 def _try_finish_sync(): connection.fetch_outgoing_commands() with self._commands_mutex: # from here no one can add commands anymore to self._commands (clients can still join and read previous commands) command_count = self.command_count() if command_count - offset > MAX_BROADCAST_COMMAND_COUNT: return False # while still more than MAX_BROADCAST_COMMAND_COUNT commands to broadcast, release the mutex # now is time to synchronize all room participants: broadcast remaining commands to new client for i in range(offset, command_count): command = self._commands[i] connection.add_command(command) # now he's part of the room, let him/her know self._connections.append(connection) connection.room = self connection.add_command(common.Command(common.MessageType.JOIN_ROOM, common.encode_string(self.name))) return True while True: if _try_finish_sync(): break # all done # broadcast commands that were added since last check command_count = self.command_count() for i in range(offset, command_count): command = self._commands[i] # atomic wrt. the GIL connection.add_command(command) offset = command_count
def send_data_updates(updates: List[BpyIDProxy]): if not share_data.use_experimental_sync(): return if not updates: return codec = Codec() for proxy in updates: # We send an ID, so we need to make sure that it includes a bp.data collection name # and the associated key try: collection_name, key = blenddata_path(proxy) except InvalidPath: logger.error("... update ignored") continue logger.info("send_data_update %s[%s]", collection_name, key) try: encoded_proxy = codec.encode(proxy) except InvalidPath: logger.error("send_update: Exception :") log_traceback(logger.error) logger.error( f"while processing bpy.data.{collection_name}[{key}]:") # For BpyIdProxy, the target is encoded in the proxy._blenddata_path buffer = common.encode_string(encoded_proxy) command = common.Command(common.MessageType.BLENDER_DATA_UPDATE, buffer, 0) share_data.client.add_command(command)
def set_client_attributes(self, attributes: dict): diff = update_attributes_and_get_diff(self.current_custom_attributes, attributes) if diff == {}: return True return self.send_command( common.Command(common.MessageType.SET_CLIENT_CUSTOM_ATTRIBUTES, common.encode_json(diff), 0) )
def get_list_rooms_command(self) -> common.Command: with self._mutex: result_dict = { room_name: value.attributes_dict() for room_name, value in self._rooms.items() } return common.Command(common.MessageType.LIST_ROOMS, common.encode_json(result_dict))
def get_list_clients_command(self) -> common.Command: with self._mutex: result_dict = { cid: c.client_attributes() for cid, c in self._connections.items() } return common.Command(common.MessageType.LIST_CLIENTS, common.encode_json(result_dict))
def send_remove_object_from_scene(client: Client, scene_name: str, object_name: str): logger.info("send_remove_object_from_scene %s <- %s", scene_name, object_name) buffer = common.encode_string(scene_name) + common.encode_string( object_name) client.add_command( common.Command(common.MessageType.REMOVE_OBJECT_FROM_SCENE, buffer, 0))
def send_add_collection_to_scene(client: Client, scene_name: str, collection_name: str): logger.info("send_add_collection_to_scene %s <- %s", scene_name, collection_name) buffer = common.encode_string(scene_name) + common.encode_string( collection_name) client.add_command( common.Command(common.MessageType.ADD_COLLECTION_TO_SCENE, buffer, 0))
def broadcast_room_update(self, room: Room, attributes: Dict[str, Any]): if attributes == {}: return self.broadcast_to_all_clients( common.Command( common.MessageType.ROOM_UPDATE, common.encode_json({room.name: attributes}), ))
def send_remove_object_from_collection(client: Client, collection_name, obj_name): logger.info("send_remove_object_from_collection %s <- %s", collection_name, obj_name) buffer = common.encode_string(collection_name) + common.encode_string( obj_name) client.add_command( common.Command(common.MessageType.REMOVE_OBJECT_FROM_COLLECTION, buffer, 0))
def send_object_visibility(client: Client, object_: bpy.types.Object): logger.debug("send_object_visibility %s", object_.name_full) buffer = (common.encode_string(object_.name_full) + common.encode_bool(object_.hide_viewport) + common.encode_bool(object_.hide_select) + common.encode_bool(object_.hide_render) + common.encode_bool(object_.hide_get())) client.add_command( common.Command(common.MessageType.OBJECT_VISIBILITY, buffer, 0))
def send_frame(): sm_props = get_shot_manager() if sm_props is None: return current_shot_index = shot_manager.get_current_shot_index(sm_props) if share_data.shot_manager.current_shot_index != current_shot_index: share_data.shot_manager.current_shot_index = current_shot_index buffer = common.encode_int(share_data.shot_manager.current_shot_index) share_data.client.add_command(common.Command(common.MessageType.SHOT_MANAGER_CURRENT_SHOT, buffer, 0))
def send_data_removals(removals: RemovalChangeset): if not share_data.use_experimental_sync(): return for uuid, debug_info in removals: logger.info("send_removal: %s (%s)", uuid, debug_info) buffer = common.encode_string(uuid) + common.encode_string(debug_info) command = common.Command(common.MessageType.BLENDER_DATA_REMOVE, buffer, 0) share_data.client.add_command(command)
def send_scene(): get_state() buffer = common.encode_int(len(share_data.shot_manager.shots)) for s in share_data.shot_manager.shots: buffer += (common.encode_string(s.name) + common.encode_string(s.camera_name) + common.encode_int(s.start) + common.encode_int(s.end) + common.encode_bool(s.enabled)) share_data.client.add_command( common.Command(common.MessageType.SHOT_MANAGER_CONTENT, buffer, 0))
def send_remove_collection_from_scene(client: Client, scene_name: str, collection_name: str): logger.info("send_remove_collection_from_scene %s <- %s", scene_name, collection_name) buffer = common.encode_string(scene_name) + common.encode_string( collection_name) client.add_command( common.Command(common.MessageType.REMOVE_COLLECTION_FROM_SCENE, buffer, 0))
def send_data_renames(renames: RenameChangeset): if not share_data.use_experimental_sync(): return for uuid, new_name, debug_info in renames: logger.info("send_rename: %s (%s) into %s", uuid, debug_info, new_name) buffer = common.encode_string(uuid) + common.encode_string( new_name) + common.encode_string(debug_info) command = common.Command(common.MessageType.BLENDER_DATA_RENAME, buffer, 0) share_data.client.add_command(command)
def send_data_removals(removals: List[Tuple[str, str]]): if not share_data.use_experimental_sync(): return for collection_name, key in removals: logger.info("send_removal: %s[%s]", collection_name, key) buffer = common.encode_string(collection_name) + common.encode_string( key) command = common.Command(common.MessageType.BLENDER_DATA_REMOVE, buffer, 0) share_data.client.add_command(command)
def add_command(self, command: common.Command): # A wrapped message is a message emitted from a frame change event. # Right now we wrap this kind of messages adding the client_id. # In the future we will probably always add the client_id to all messages. But the difference # between synced time messages and the other must remain. if self.synced_time_messages: command = common.Command( MessageType.CLIENT_ID_WRAPPER, common.encode_string(self.client_id) + common.encode_int(command.type.value) + command.data, 0, ) super().add_command(command)