comparison libervia/frontends/tools/webrtc.py @ 4270:0d7bb4df2343

Reformatted code base using black.
author Goffi <goffi@goffi.org>
date Wed, 19 Jun 2024 18:44:57 +0200
parents 79c8a70e1813
children 2992f9d1e039
comparison
equal deleted inserted replaced
4269:64a85ce8be70 4270:0d7bb4df2343
80 80
81 def __init__( 81 def __init__(
82 self, 82 self,
83 bridge, 83 bridge,
84 profile: str, 84 profile: str,
85 sources_data: SourcesData|None = None, 85 sources_data: SourcesData | None = None,
86 sinks_data: SinksData|None = None, 86 sinks_data: SinksData | None = None,
87 reset_cb: Callable | None = None, 87 reset_cb: Callable | None = None,
88 merge_pip: bool | None = None, 88 merge_pip: bool | None = None,
89 target_size: tuple[int, int] | None = None, 89 target_size: tuple[int, int] | None = None,
90 call_start_cb: Callable[[str, dict, str], Awaitable[str]] | None = None, 90 call_start_cb: Callable[[str, dict, str], Awaitable[str]] | None = None,
91 dc_data_list: list[SourcesDataChannel|SinksDataChannel]|None = None 91 dc_data_list: list[SourcesDataChannel | SinksDataChannel] | None = None,
92 ) -> None: 92 ) -> None:
93 """Initializes a new WebRTC instance. 93 """Initializes a new WebRTC instance.
94 94
95 @param bridge: An instance of backend bridge. 95 @param bridge: An instance of backend bridge.
96 @param profile: Libervia profile. 96 @param profile: Libervia profile.
384 """Retrieves ICE password and user fragment for SDP offer. 384 """Retrieves ICE password and user fragment for SDP offer.
385 385
386 @param sdp: The Session Description Protocol offer string. 386 @param sdp: The Session Description Protocol offer string.
387 """ 387 """
388 lines = sdp.splitlines() 388 lines = sdp.splitlines()
389 media = '' 389 media = ""
390 mid_media_map = {} 390 mid_media_map = {}
391 bundle_media = set() 391 bundle_media = set()
392 bundle_ufrag = '' 392 bundle_ufrag = ""
393 bundle_pwd = '' 393 bundle_pwd = ""
394 in_bundle = False 394 in_bundle = False
395 395
396 for line in lines: 396 for line in lines:
397 if line.startswith('m='): 397 if line.startswith("m="):
398 media = line.split('=')[1].split()[0] 398 media = line.split("=")[1].split()[0]
399 elif line.startswith('a=mid:'): 399 elif line.startswith("a=mid:"):
400 mid = line.split(':')[1].strip() 400 mid = line.split(":")[1].strip()
401 mid_media_map[mid] = media 401 mid_media_map[mid] = media
402 elif line.startswith('a=group:BUNDLE'): 402 elif line.startswith("a=group:BUNDLE"):
403 in_bundle = True 403 in_bundle = True
404 bundle_media = set(line.split(':')[1].strip().split()) 404 bundle_media = set(line.split(":")[1].strip().split())
405 elif line.startswith('a=ice-ufrag:'): 405 elif line.startswith("a=ice-ufrag:"):
406 if in_bundle: 406 if in_bundle:
407 bundle_ufrag = line.split(':')[1].strip() 407 bundle_ufrag = line.split(":")[1].strip()
408 else: 408 else:
409 self.ufrag[media] = line.split(':')[1].strip() 409 self.ufrag[media] = line.split(":")[1].strip()
410 elif line.startswith('a=ice-pwd:'): 410 elif line.startswith("a=ice-pwd:"):
411 if in_bundle: 411 if in_bundle:
412 bundle_pwd = line.split(':')[1].strip() 412 bundle_pwd = line.split(":")[1].strip()
413 else: 413 else:
414 self.pwd[media] = line.split(':')[1].strip() 414 self.pwd[media] = line.split(":")[1].strip()
415 else: 415 else:
416 in_bundle = False 416 in_bundle = False
417 417
418 if bundle_ufrag and bundle_pwd: 418 if bundle_ufrag and bundle_pwd:
419 for mid in bundle_media: 419 for mid in bundle_media:
485 @raises AssertionError: If the role is not 'initiator' or 'responder'. 485 @raises AssertionError: If the role is not 'initiator' or 'responder'.
486 """ 486 """
487 assert role in ("initiator", "responder") 487 assert role in ("initiator", "responder")
488 self.role = role 488 self.role = role
489 489
490
491 if isinstance(self.sources_data, SourcesPipeline): 490 if isinstance(self.sources_data, SourcesPipeline):
492 if self.sources_data.video_pipeline!= "" and video_pt is None: 491 if self.sources_data.video_pipeline != "" and video_pt is None:
493 raise NotImplementedError(NONE_NOT_IMPLEMENTED_MSG) 492 raise NotImplementedError(NONE_NOT_IMPLEMENTED_MSG)
494 if self.sources_data.audio_pipeline!= "" and audio_pt is None: 493 if self.sources_data.audio_pipeline != "" and audio_pt is None:
495 raise NotImplementedError(NONE_NOT_IMPLEMENTED_MSG) 494 raise NotImplementedError(NONE_NOT_IMPLEMENTED_MSG)
496 elif isinstance(self.sources_data, SourcesNone): 495 elif isinstance(self.sources_data, SourcesNone):
497 pass 496 pass
498 else: 497 else:
499 if audio_pt is None or video_pt is None: 498 if audio_pt is None or video_pt is None:
557 local_video_sink_elt = "compositor.sink_1" 556 local_video_sink_elt = "compositor.sink_1"
558 557
559 if video_source_elt: 558 if video_source_elt:
560 # Video source with an input-selector to switch between normal and video mute 559 # Video source with an input-selector to switch between normal and video mute
561 # (or desktop sharing). 560 # (or desktop sharing).
562 gst_pipe_elements.append(f""" 561 gst_pipe_elements.append(
562 f"""
563 input-selector name=video_selector 563 input-selector name=video_selector
564 ! videorate drop-only=1 max-rate=30 564 ! videorate drop-only=1 max-rate=30
565 ! video/x-raw,framerate=30/1 565 ! video/x-raw,framerate=30/1
566 ! tee name=t 566 ! tee name=t
567 567
579 ! videoconvert 579 ! videoconvert
580 ! vp8enc deadline=1 keyframe-max-dist=30 580 ! vp8enc deadline=1 keyframe-max-dist=30
581 ! rtpvp8pay picture-id-mode=15-bit 581 ! rtpvp8pay picture-id-mode=15-bit
582 ! application/x-rtp,media=video,encoding-name=VP8,payload={video_pt} 582 ! application/x-rtp,media=video,encoding-name=VP8,payload={video_pt}
583 ! sendrecv. 583 ! sendrecv.
584 """) 584 """
585 )
585 586
586 if local_video_sink_elt: 587 if local_video_sink_elt:
587 # Local video feedback. 588 # Local video feedback.
588 gst_pipe_elements.append(f""" 589 gst_pipe_elements.append(
590 f"""
589 t. 591 t.
590 ! queue max-size-buffers=1 max-size-time=0 max-size-bytes=0 leaky=downstream 592 ! queue max-size-buffers=1 max-size-time=0 max-size-bytes=0 leaky=downstream
591 ! videoconvert 593 ! videoconvert
592 ! {local_video_sink_elt} 594 ! {local_video_sink_elt}
593 """) 595 """
596 )
594 597
595 if audio_source_elt: 598 if audio_source_elt:
596 # Audio with a valve for muting. 599 # Audio with a valve for muting.
597 gst_pipe_elements.append(r""" 600 gst_pipe_elements.append(
601 r"""
598 {audio_source_elt} name=audio_src 602 {audio_source_elt} name=audio_src
599 ! valve 603 ! valve
600 ! queue max-size-buffers=10 max-size-time=0 max-size-bytes=0 leaky=downstream 604 ! queue max-size-buffers=10 max-size-time=0 max-size-bytes=0 leaky=downstream
601 ! audioconvert 605 ! audioconvert
602 ! audioresample 606 ! audioresample
603 ! opusenc audio-type=voice 607 ! opusenc audio-type=voice
604 ! rtpopuspay 608 ! rtpopuspay
605 ! application/x-rtp,media=audio,encoding-name=OPUS,payload={audio_pt} 609 ! application/x-rtp,media=audio,encoding-name=OPUS,payload={audio_pt}
606 ! sendrecv. 610 ! sendrecv.
607 """) 611 """
612 )
608 613
609 self.gst_pipe_desc = "\n\n".join(gst_pipe_elements) 614 self.gst_pipe_desc = "\n\n".join(gst_pipe_elements)
610 615
611 log.debug(f"Gstreamer pipeline: {self.gst_pipe_desc}") 616 log.debug(f"Gstreamer pipeline: {self.gst_pipe_desc}")
612 617
706 ) 711 )
707 712
708 for dc_data in self.dc_data_list: 713 for dc_data in self.dc_data_list:
709 self.create_data_channel(dc_data) 714 self.create_data_channel(dc_data)
710 715
711 def create_data_channel(self, dc_data: SourcesDataChannel|SinksDataChannel) -> None: 716 def create_data_channel(self, dc_data: SourcesDataChannel | SinksDataChannel) -> None:
712 """Create a Data Channel and connect relevant callbacks.""" 717 """Create a Data Channel and connect relevant callbacks."""
713 assert self.pipeline is not None 718 assert self.pipeline is not None
714 if isinstance(dc_data, SourcesDataChannel): 719 if isinstance(dc_data, SourcesDataChannel):
715 # Data channel configuration for compatibility with browser defaults 720 # Data channel configuration for compatibility with browser defaults
716 data_channel_options = Gst.Structure.new_empty("data-channel-options") 721 data_channel_options = Gst.Structure.new_empty("data-channel-options")
727 return 732 return
728 data_channel.connect("on-open", dc_data.dc_open_cb) 733 data_channel.connect("on-open", dc_data.dc_open_cb)
729 elif isinstance(dc_data, SinksDataChannel): 734 elif isinstance(dc_data, SinksDataChannel):
730 self.webrtcbin.connect("on-data-channel", dc_data.dc_on_data_channel) 735 self.webrtcbin.connect("on-data-channel", dc_data.dc_on_data_channel)
731 else: 736 else:
732 raise ValueError( 737 raise ValueError("Only SourcesDataChannel or SinksDataChannel are allowed.")
733 "Only SourcesDataChannel or SinksDataChannel are allowed."
734 )
735 738
736 def start_pipeline(self) -> None: 739 def start_pipeline(self) -> None:
737 """Starts the GStreamer pipeline.""" 740 """Starts the GStreamer pipeline."""
738 log.debug("starting the pipeline") 741 log.debug("starting the pipeline")
739 self.pipeline.set_state(Gst.State.PLAYING) 742 self.pipeline.set_state(Gst.State.PLAYING)
1098 assert sdp is not None 1101 assert sdp is not None
1099 self.extract_ufrag_pwd(sdp) 1102 self.extract_ufrag_pwd(sdp)
1100 ice_data = { 1103 ice_data = {
1101 "ufrag": self.ufrag[media_type], 1104 "ufrag": self.ufrag[media_type],
1102 "pwd": self.pwd[media_type], 1105 "pwd": self.pwd[media_type],
1103 "candidates": [parsed_candidate] 1106 "candidates": [parsed_candidate],
1104 } 1107 }
1105 self._a_call( 1108 self._a_call(
1106 self.bridge.ice_candidates_add, 1109 self.bridge.ice_candidates_add,
1107 self.sid, 1110 self.sid,
1108 data_format.serialise({media_type: ice_data}), 1111 data_format.serialise({media_type: ice_data}),