<!doctype html> <meta charset=utf-8> <title>RTCPeerConnection BUNDLE</title> <script src="/resources/testharness.js"></script> <script src="/resources/testharnessreport.js"></script> <script src="../RTCPeerConnection-helper.js"></script> <script src="/webrtc/third_party/sdp/sdp.js"></script> <script> 'use strict'; promise_test(async t => { const caller = new RTCPeerConnection(); t.add_cleanup(() => caller.close()); const calleeAudio = new RTCPeerConnection(); t.add_cleanup(() => calleeAudio.close()); const calleeVideo = new RTCPeerConnection(); t.add_cleanup(() => calleeVideo.close()); const stream = await getNoiseStream({audio: true, video: true}); t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); stream.getTracks().forEach(track => caller.addTrack(track, stream)); let metadataToBeLoaded; calleeVideo.ontrack = (e) => { const stream = e.streams[0]; const v = document.createElement('video'); v.autoplay = true; v.srcObject = stream; v.id = stream.id metadataToBeLoaded = new Promise((resolve) => { v.addEventListener('loadedmetadata', () => { resolve(); }); }); }; caller.addEventListener('icecandidate', (e) => { // route depending on sdpMlineIndex if (e.candidate) { const target = e.candidate.sdpMLineIndex === 0 ? calleeAudio : calleeVideo; target.addIceCandidate({sdpMid: e.candidate.sdpMid, candidate: e.candidate.candidate}); } else { calleeAudio.addIceCandidate(); calleeVideo.addIceCandidate(); } }); calleeAudio.addEventListener('icecandidate', (e) => { if (e.candidate) { caller.addIceCandidate({sdpMid: e.candidate.sdpMid, candidate: e.candidate.candidate}); } // Note: caller.addIceCandidate is only called for video to avoid calling it twice. }); calleeVideo.addEventListener('icecandidate', (e) => { if (e.candidate) { caller.addIceCandidate({sdpMid: e.candidate.sdpMid, candidate: e.candidate.candidate}); } else { caller.addIceCandidate(); } }); const offer = await caller.createOffer(); const sections = SDPUtils.splitSections(offer.sdp); // Remove the a=group:BUNDLE from the SDP when signaling. const bundle = SDPUtils.matchPrefix(sections[0], 'a=group:BUNDLE')[0]; sections[0] = sections[0].replace(bundle + '\r\n', ''); const audioSdp = sections[0] + sections[1]; const videoSdp = sections[0] + sections[2]; await calleeAudio.setRemoteDescription({type: 'offer', sdp: audioSdp}); await calleeVideo.setRemoteDescription({type: 'offer', sdp: videoSdp}); await caller.setLocalDescription(offer); const answerAudio = await calleeAudio.createAnswer(); const answerVideo = await calleeVideo.createAnswer(); const audioSections = SDPUtils.splitSections(answerAudio.sdp); const videoSections = SDPUtils.splitSections(answerVideo.sdp); // Remove the fingerprint from the session part of the SDP if present // and move it to the media section. SDPUtils.matchPrefix(audioSections[0], 'a=fingerprint:').forEach(line => { audioSections[0] = audioSections[0].replace(line + '\r\n', ''); audioSections[1] += line + '\r\n'; }); SDPUtils.matchPrefix(videoSections[0], 'a=fingerprint:').forEach(line => { videoSections[0] = videoSections[0].replace(line + '\r\n', ''); videoSections[1] += line + '\r\n'; }); const sdp = audioSections[0] + audioSections[1] + videoSections[1]; await caller.setRemoteDescription({type: 'answer', sdp}); await calleeAudio.setLocalDescription(answerAudio); await calleeVideo.setLocalDescription(answerVideo); await metadataToBeLoaded; assert_equals(calleeAudio.connectionState, 'connected'); assert_equals(calleeVideo.connectionState, 'connected'); }, 'Connect audio and video to two independent PeerConnections'); </script>