Mercurial > libervia-web
comparison libervia/web/pages/calls/_browser/webrtc.py @ 1564:bd3c880f4a47
browser (calls): add camera switching:
- devices are listed on startup, and a `has_multiple_cameras` boolean is set
- based on this data, switch camera button is hidden or not
- a click/touch on the button will switch to the next available camera, and loop back to
first one at the end
author | Goffi <goffi@goffi.org> |
---|---|
date | Fri, 18 Aug 2023 17:43:01 +0200 |
parents | e3449beac8d8 |
children | d282dbdd5ffd |
comparison
equal
deleted
inserted
replaced
1563:e3449beac8d8 | 1564:bd3c880f4a47 |
---|---|
13 bridge = Bridge() | 13 bridge = Bridge() |
14 GATHER_TIMEOUT = 10000 | 14 GATHER_TIMEOUT = 10000 |
15 | 15 |
16 | 16 |
17 class WebRTC: | 17 class WebRTC: |
18 def __init__(self): | 18 def __init__( |
19 self, | |
20 screen_sharing_cb=None, | |
21 on_connection_established_cb=None, | |
22 on_reconnect_cb=None, | |
23 on_connection_lost_cb=None, | |
24 on_video_devices=None, | |
25 ): | |
19 self.reset_instance() | 26 self.reset_instance() |
27 | |
28 # ICE events | |
20 bridge.register_signal("ice_candidates_new", self._on_ice_candidates_new) | 29 bridge.register_signal("ice_candidates_new", self._on_ice_candidates_new) |
21 bridge.register_signal("ice_restart", self._on_ice_restart) | 30 bridge.register_signal("ice_restart", self._on_ice_restart) |
22 self.on_connection_established_cb = None | 31 |
23 self.on_reconnect_cb = None | 32 # connection events callbacks |
24 self.on_connection_lost_cb = None | 33 self.on_connection_established_cb = on_connection_established_cb |
34 self.on_reconnect_cb = on_reconnect_cb | |
35 self.on_connection_lost_cb = on_connection_lost_cb | |
36 | |
37 # video devices | |
38 self.on_video_devices = on_video_devices | |
39 self.video_devices = [] | |
40 self.has_multiple_cameras = False | |
41 self.current_camera = None | |
42 | |
43 # Initially populate the video devices list | |
44 aio.run(self._populate_video_devices()) | |
45 | |
46 # muting | |
25 self.is_audio_muted = None | 47 self.is_audio_muted = None |
26 self.is_video_muted = None | 48 self.is_video_muted = None |
49 | |
50 # screen sharing | |
27 self._is_sharing_screen = False | 51 self._is_sharing_screen = False |
28 self.screen_sharing_cb = None | 52 self.screen_sharing_cb = screen_sharing_cb |
53 | |
54 # video elements | |
29 self.local_video_elt = document["local_video"] | 55 self.local_video_elt = document["local_video"] |
30 self.remote_video_elt = document["remote_video"] | 56 self.remote_video_elt = document["remote_video"] |
31 | 57 |
32 @property | 58 @property |
33 def is_sharing_screen(self) -> bool: | 59 def is_sharing_screen(self) -> bool: |
54 "video": {"candidates": []}, | 80 "video": {"candidates": []}, |
55 } | 81 } |
56 self.media_candidates = {} | 82 self.media_candidates = {} |
57 self.candidates_gathered = aio.Future() | 83 self.candidates_gathered = aio.Future() |
58 | 84 |
85 async def _populate_video_devices(self): | |
86 devices = await window.navigator.mediaDevices.enumerateDevices() | |
87 devices_ids = set() | |
88 self.video_devices.clear() | |
89 for device in devices: | |
90 if device.kind != "videoinput": | |
91 continue | |
92 # we can have multiple devices with same IDs in some corner cases (e.g. | |
93 # infrared camera) | |
94 device_id = device.deviceId | |
95 if device_id in devices_ids: | |
96 continue | |
97 devices_ids.add(device_id) | |
98 self.video_devices.append(device) | |
99 self.has_multiple_cameras = len(self.video_devices) > 1 | |
100 if self.on_video_devices is not None: | |
101 self.on_video_devices(self.has_multiple_cameras) | |
102 # Set the initial camera to the default (usually front on mobile) | |
103 if self.video_devices: | |
104 self.current_camera = self.video_devices[0].deviceId | |
105 log.debug( | |
106 f"devices populated: {self.video_devices=} {self.has_multiple_cameras=}" | |
107 ) | |
108 | |
59 @property | 109 @property |
60 def media_types(self): | 110 def media_types(self): |
61 if self._media_types is None: | 111 if self._media_types is None: |
62 raise Exception("self._media_types should not be None!") | 112 raise Exception("self._media_types should not be None!") |
63 return self._media_types | 113 return self._media_types |
316 else: | 366 else: |
317 if self.local_video_elt.srcObject: | 367 if self.local_video_elt.srcObject: |
318 for track in self.local_video_elt.srcObject.getTracks(): | 368 for track in self.local_video_elt.srcObject.getTracks(): |
319 if track.kind == "video": | 369 if track.kind == "video": |
320 track.stop() | 370 track.stop() |
321 media_constraints = {"video": True} | 371 |
372 media_constraints = { | |
373 "video": {"deviceId": self.current_camera} | |
374 if self.current_camera | |
375 else True | |
376 } | |
377 | |
322 new_stream = await window.navigator.mediaDevices.getUserMedia( | 378 new_stream = await window.navigator.mediaDevices.getUserMedia( |
323 media_constraints | 379 media_constraints |
324 ) | 380 ) |
325 | 381 |
326 if not new_stream: | 382 if not new_stream: |
366 new_video_tracks[0].bind("ended", on_track_ended) | 422 new_video_tracks[0].bind("ended", on_track_ended) |
367 | 423 |
368 self.is_sharing_screen = screen | 424 self.is_sharing_screen = screen |
369 | 425 |
370 return local_stream | 426 return local_stream |
427 | |
428 async def switch_camera(self) -> None: | |
429 """Switches to the next available camera. | |
430 | |
431 This method cycles through the list of available video devices, replaces the | |
432 current video track with the next one in the user's local video element, and then | |
433 updates the sender's track in the peer connection. If there's only one camera or | |
434 if an error occurs while switching, the method logs the error and does nothing. | |
435 """ | |
436 log.info("switching camera") | |
437 if not self.has_multiple_cameras: | |
438 log.error("No multiple cameras to switch.") | |
439 return | |
440 | |
441 current_camera_index = -1 | |
442 for i, device_info in enumerate(self.video_devices): | |
443 if device_info.deviceId == self.current_camera: | |
444 current_camera_index = i | |
445 break | |
446 | |
447 if current_camera_index == -1: | |
448 log.error("Current camera not found in available devices.") | |
449 return | |
450 | |
451 # Switch to the next camera in the list | |
452 next_camera_index = (current_camera_index + 1) % len(self.video_devices) | |
453 self.current_camera = self.video_devices[next_camera_index].deviceId | |
454 log.debug(f"{next_camera_index=} {self.current_camera=}") | |
455 | |
456 new_stream = await window.navigator.mediaDevices.getUserMedia( | |
457 {"video": {"deviceId": self.current_camera}} | |
458 ) | |
459 | |
460 new_video_tracks = [ | |
461 track for track in new_stream.getTracks() if track.kind == "video" | |
462 ] | |
463 | |
464 if not new_video_tracks: | |
465 log.error("Failed to retrieve the video track from the new stream.") | |
466 return | |
467 | |
468 # Update local video element's stream | |
469 local_stream = self.local_video_elt.srcObject | |
470 if local_stream: | |
471 local_video_tracks = [ | |
472 track for track in local_stream.getTracks() if track.kind == "video" | |
473 ] | |
474 if local_video_tracks: | |
475 local_video_tracks[0].stop() | |
476 local_stream.removeTrack(local_video_tracks[0]) | |
477 local_stream.addTrack(new_video_tracks[0]) | |
478 self.local_video_elt.srcObject = local_stream | |
479 | |
480 # update remove video stream | |
481 video_sender = next( | |
482 ( | |
483 sender | |
484 for sender in self._peer_connection.getSenders() | |
485 if sender.track and sender.track.kind == "video" | |
486 ), | |
487 None, | |
488 ) | |
489 | |
490 if video_sender: | |
491 await video_sender.replaceTrack(new_video_tracks[0]) | |
492 | |
371 | 493 |
372 async def _gather_ice_candidates(self, is_initiator: bool, remote_candidates=None): | 494 async def _gather_ice_candidates(self, is_initiator: bool, remote_candidates=None): |
373 """Get ICE candidates and wait to have them all before returning them | 495 """Get ICE candidates and wait to have them all before returning them |
374 | 496 |
375 @param is_initiator: Boolean indicating if the user is the initiator of the connection | 497 @param is_initiator: Boolean indicating if the user is the initiator of the connection |