···308308 ScriptToConstellationMessage::MediaSessionEvent(pipeline_id, event) => {
309309 // Unlikely at this point, but we may receive events coming from
310310 // different media sessions, so we set the active media session based
311311-@@ -2057,7 +2185,333 @@
311311+@@ -2057,9 +2185,358 @@
312312 let _ = event_loop.send(ScriptThreadMessage::TriggerGarbageCollection);
313313 }
314314 },
···463463+ peer_id,
464464+ local_port_id,
465465+ remote_port_id,
466466++ target_url,
466467+ callback,
467468+ ) => {
469469++ debug!("CreatePeerStream: peer_id={peer_id}, target_url={target_url}");
468470+ // Register the virtual remote port that only exists in the constellation,
469471+ // not in any GlobalScope.
470472+ self.message_ports.insert(
···494496+ &P2pMessage::PortOffer {
495497+ stream_id,
496498+ port_id: port_id_bytes,
497497-+ from_peer: peer_id.clone(),
499499++ target_url,
498500+ },
499501+ );
500502+ // TODO: wait for PortOfferAccepted before resolving.
···520522+ .send_message(&from_peer, &P2pMessage::PortOfferDenied { stream_id });
521523+ }
522524+ },
523523-+ }
524524-+ }
525525-+
525525+ }
526526+ }
527527+526528+ fn handle_pairing_event(&mut self, event: constellation_traits::PairingEvent) {
527529+ if let constellation_traits::PairingEvent::MessageReceived { ref from, ref data } = event {
530530++ debug!("P2P message received from {from}, {} bytes", data.len());
528531+ if let Some((from, message)) = self.pairing.handle_incoming_message(from, data) {
532532++ debug!("Decoded P2P message: {message:?}");
529533+ match message {
530534+ P2pMessage::BroadcastChannelMessage {
531535+ ref origin,
···551555+ P2pMessage::PortOffer {
552556+ ref stream_id,
553557+ ref port_id,
554554-+ ..
558558++ ref target_url,
555559+ } => {
560560++ debug!(
561561++ "PortOffer received: stream_id={stream_id}, target_url={target_url}, from={from}"
562562++ );
556563+ let Ok(remote_port_id) =
557564+ postcard::from_bytes::<base::id::MessagePortId>(port_id)
558565+ else {
···568575+ entangled_with: None,
569576+ },
570577+ );
571571-+ // Notify script threads to create a MessagePort and fire peerstream event.
572572-+ // The script thread will report back via PeerStreamResponse whether
573573-+ // the event was accepted or denied.
578578++ // Dispatch only to the pipeline whose URL matches the target URL.
574579+ let remote_port_id_bytes = port_id.clone();
575575-+ for event_loop in self.event_loops() {
576576-+ let _ = event_loop.send(ScriptThreadMessage::DispatchPeerStream(
577577-+ from.clone(),
578578-+ remote_port_id_bytes.clone(),
579579-+ stream_id.clone(),
580580-+ from.clone(),
581581-+ ));
580580++ let mut dispatched = false;
581581++ for (_pipeline_id, pipeline) in &self.pipelines {
582582++ let pipeline_url = pipeline.load_data.url.as_str();
583583++ debug!(
584584++ "Checking pipeline {:?} url={} against target={}",
585585++ _pipeline_id, pipeline_url, target_url
586586++ );
587587++ if pipeline_url == target_url {
588588++ let _ = pipeline.event_loop.send(
589589++ ScriptThreadMessage::DispatchPeerStream(
590590++ from.clone(),
591591++ remote_port_id_bytes.clone(),
592592++ stream_id.clone(),
593593++ from.clone(),
594594++ target_url.clone(),
595595++ ),
596596++ );
597597++ dispatched = true;
598598++ // Send to the first matching pipeline only — avoid entanglement
599599++ // conflicts from multiple ports being created for the same offer.
600600++ break;
601601++ }
602602++ }
603603++ if !dispatched {
604604++ warn!("No pipeline matches target_url {target_url} for PortOffer");
582605+ }
583606+ },
584607+ P2pMessage::PortOfferAccepted { .. } => {
···632655+ // Handle peer disconnect: clean up remote channel state.
633656+ if let constellation_traits::PairingEvent::PeerExpired { ref id } = event {
634657+ self.pairing.clear_remote_peer(id);
635635- }
658658++ }
636659+
637660+ for event_loop in self.event_loops() {
638661+ if self.embedder_error_listeners.contains(&event_loop.id()) {
639662+ let _ = event_loop.send(ScriptThreadMessage::DispatchPairingEvent(event.clone()));
640663+ }
641664+ }
642642- }
643643-665665++ }
666666++
644667 /// Check the origin of a message against that of the pipeline it came from.
645645-@@ -2376,6 +2830,29 @@
668668+ /// Note: this is still limited as a security check,
669669+ /// see <https://github.com/servo/servo/issues/11722>
670670+@@ -2376,6 +2853,29 @@
646671 TransferState::TransferInProgress(queue) => queue.push_back(task),
647672 TransferState::CompletionFailed(queue) => queue.push_back(task),
648673 TransferState::CompletionRequested(_, queue) => queue.push_back(task),
···672697 }
673698 }
674699675675-@@ -3246,6 +3723,13 @@
700700+@@ -3246,6 +3746,13 @@
676701 /// <https://html.spec.whatwg.org/multipage/#destroy-a-top-level-traversable>
677702 fn handle_close_top_level_browsing_context(&mut self, webview_id: WebViewId) {
678703 debug!("{webview_id}: Closing");
···686711 let browsing_context_id = BrowsingContextId::from(webview_id);
687712 // Step 5. Remove traversable from the user agent's top-level traversable set.
688713 let browsing_context =
689689-@@ -3522,8 +4006,27 @@
714714+@@ -3522,8 +4029,27 @@
690715 opener_webview_id,
691716 opener_pipeline_id,
692717 response_sender,
···714739 let Some((webview_id_sender, webview_id_receiver)) = generic_channel::channel() else {
715740 warn!("Failed to create channel");
716741 let _ = response_sender.send(None);
717717-@@ -3622,6 +4125,361 @@
742742+@@ -3622,6 +4148,361 @@
718743 });
719744 }
720745···10761101 #[servo_tracing::instrument(skip_all)]
10771102 fn handle_refresh_cursor(&self, pipeline_id: PipelineId) {
10781103 let Some(pipeline) = self.pipelines.get(&pipeline_id) else {
10791079-@@ -4747,7 +5605,7 @@
11041104+@@ -4747,7 +5628,7 @@
10801105 }
1081110610821107 #[servo_tracing::instrument(skip_all)]
···10851110 // Send a flat projection of the history to embedder.
10861111 // The final vector is a concatenation of the URLs of the past
10871112 // entries, the current entry and the future entries.
10881088-@@ -4850,9 +5708,23 @@
11131113+@@ -4850,9 +5731,23 @@
10891114 );
10901115 self.embedder_proxy.send(EmbedderMsg::HistoryChanged(
10911116 webview_id,
+2-2
patches/components/constellation/pairing.rs.patch
···4242+ stream_id: String,
4343+ /// The serialized MessagePortId of the port on the offering side.
4444+ port_id: Vec<u8>,
4545-+ /// The peer that is offering the port.
4646-+ from_peer: String,
4545++ /// The URL of the page that should receive the peer stream event on the remote side.
4646++ target_url: String,
4747+ },
4848+ /// Accept a port offer — the remote side created its port.
4949+ PortOfferAccepted {
···119119 /// Mark a new document as active
120120 ActivateDocument,
121121 /// Set the document state for a pipeline (used by screenshot / reftests)
122122-@@ -726,6 +775,72 @@
122122+@@ -726,6 +775,73 @@
123123 RespondToScreenshotReadinessRequest(ScreenshotReadinessResponse),
124124 /// Request the constellation to force garbage collection in all `ScriptThread`'s.
125125 TriggerGarbageCollection,
···179179+ PairingRejectPairing(String, GenericCallback<Result<(), String>>),
180180+ /// Create a peer stream: create a virtual remote port entangled with a local port,
181181+ /// and send the offer to a remote peer.
182182-+ /// Args: peer_id, local_port_id, remote_port_id, callback.
182182++ /// Args: peer_id, local_port_id, remote_port_id, target_url, callback.
183183+ CreatePeerStream(
184184+ String,
185185+ base::id::MessagePortId,
186186+ base::id::MessagePortId,
187187++ String,
187188+ GenericCallback<Result<(), String>>,
188189+ ),
189190+ /// Response to a DispatchPeerStream — whether the peer stream was accepted or denied.
+2-2
patches/components/shared/script/lib.rs.patch
···5555+ /// Dispatch a pairing event to all `navigator.embedder.pairing` instances in this script thread.
5656+ DispatchPairingEvent(constellation_traits::PairingEvent),
5757+ /// Dispatch a peer stream event — a remote peer is offering a MessagePort.
5858-+ /// Contains (peer_id, serialized remote port_id bytes, stream_id, from_peer_id).
5959-+ DispatchPeerStream(String, Vec<u8>, String, String),
5858++ /// Contains (peer_id, serialized remote port_id bytes, stream_id, from_peer_id, target_url).
5959++ DispatchPeerStream(String, Vec<u8>, String, String, String),
6060 }
61616262 impl fmt::Debug for ScriptThreadMessage {