WebRTC: Imlement MediaEndpointPeerConnection::createAnswer()
authoradam.bergkvist@ericsson.com <adam.bergkvist@ericsson.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Fri, 10 Jun 2016 09:15:07 +0000 (09:15 +0000)
committeradam.bergkvist@ericsson.com <adam.bergkvist@ericsson.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Fri, 10 Jun 2016 09:15:07 +0000 (09:15 +0000)
https://bugs.webkit.org/show_bug.cgi?id=158566

Reviewed by Eric Carlson.

Source/WebCore:

Add the MediaEndpointPeerConnection implementation of RTCPeerConnection.createAnswer [1].
createAnswer() creates a 'reply' to an remote offer set with setRemoteDescription(),
completes the offer/answer dialog and brings the RTCPeerConnection back to the 'stable'
signaling state.

[1] https://w3c.github.io/webrtc-pc/archives/20160513/webrtc.html#dom-rtcpeerconnection-createanswer

Test: fast/mediastream/RTCPeerConnection-inspect-answer.html

* Modules/mediastream/MediaEndpointPeerConnection.cpp:
(WebCore::MediaEndpointPeerConnection::createOfferTask):
Align creation of RTCSessionDescription with createAnswerTask.
(WebCore::MediaEndpointPeerConnection::createAnswer):
(WebCore::MediaEndpointPeerConnection::createAnswerTask):
Add Implementation.
* Modules/mediastream/MediaEndpointPeerConnection.h:

LayoutTests:

Add test for RTCPeerConnection.createAnswer.

* fast/mediastream/RTCPeerConnection-inspect-answer-expected.txt: Added.
* fast/mediastream/RTCPeerConnection-inspect-answer.html: Added.
Generate two answers, one with audio only and a second with audio and video, and inspect
the result.
* platform/mac/TestExpectations:
Skip tests for mac that require building with WEB_RTC enabled.

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@201920 268f45cc-cd09-0410-ab3c-d52691b4dbfc

LayoutTests/ChangeLog
LayoutTests/fast/mediastream/RTCPeerConnection-inspect-answer-expected.txt [new file with mode: 0644]
LayoutTests/fast/mediastream/RTCPeerConnection-inspect-answer.html [new file with mode: 0644]
LayoutTests/platform/mac/TestExpectations
Source/WebCore/ChangeLog
Source/WebCore/Modules/mediastream/MediaEndpointPeerConnection.cpp
Source/WebCore/Modules/mediastream/MediaEndpointPeerConnection.h

index 64ce61e..dfced84 100644 (file)
@@ -1,3 +1,19 @@
+2016-06-10  Adam Bergkvist  <adam.bergkvist@ericsson.com>
+
+        WebRTC: Imlement MediaEndpointPeerConnection::createAnswer()
+        https://bugs.webkit.org/show_bug.cgi?id=158566
+
+        Reviewed by Eric Carlson.
+
+        Add test for RTCPeerConnection.createAnswer.
+
+        * fast/mediastream/RTCPeerConnection-inspect-answer-expected.txt: Added.
+        * fast/mediastream/RTCPeerConnection-inspect-answer.html: Added.
+        Generate two answers, one with audio only and a second with audio and video, and inspect
+        the result.
+        * platform/mac/TestExpectations:
+        Skip tests for mac that require building with WEB_RTC enabled.
+
 2016-06-08  Sergio Villar Senin  <svillar@igalia.com>
 
         [css-grid] CRASH when getting the computed style of a grid with only absolutely positioned children
diff --git a/LayoutTests/fast/mediastream/RTCPeerConnection-inspect-answer-expected.txt b/LayoutTests/fast/mediastream/RTCPeerConnection-inspect-answer-expected.txt
new file mode 100644 (file)
index 0000000..bad2ce7
--- /dev/null
@@ -0,0 +1,73 @@
+Test RTCPeerConnection.setRemoteDescription called with an RTCSessionDescription of type 'offer'
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+
+PASS Answer with audio created
+=== RTCSessionDescription ===
+type: answer, sdp:
+v=0
+o=- {session-id:OK} 0 IN IP4 127.0.0.1
+s=-
+t=0 0
+m=audio 9 UDP/TLS/RTP/SAVPF 111 8 0
+c=IN IP4 0.0.0.0
+a=rtcp-mux
+a=recvonly
+a=mid:{mid:OK}
+a=rtpmap:111 OPUS/48000/2
+a=rtpmap:8 PCMA/8000
+a=rtpmap:0 PCMU/8000
+a=ssrc:{ssrc:OK} cname:{cname:OK}
+a=ice-ufrag:{ice-ufrag:OK}
+a=ice-pwd:{ice-password:OK}
+a=fingerprint:sha-256 8B:87:09:8A:5D:C2:F3:33:EF:C5:B1:F6:84:3A:3D:D6:A3:E2:9C:17:4C:E7:46:3B:1B:CE:84:98:DD:8E:AF:7B
+a=setup:active
+===
+
+PASS Answer with audio and video created
+=== RTCSessionDescription ===
+type: answer, sdp:
+v=0
+o=- {session-id:OK} 1 IN IP4 127.0.0.1
+s=-
+t=0 0
+m=audio 9 UDP/TLS/RTP/SAVPF 111 8 0
+c=IN IP4 0.0.0.0
+a=rtcp-mux
+a=recvonly
+a=mid:{mid:OK}
+a=rtpmap:111 OPUS/48000/2
+a=rtpmap:8 PCMA/8000
+a=rtpmap:0 PCMU/8000
+a=ssrc:{ssrc:OK} cname:{cname:OK}
+a=ice-ufrag:{ice-ufrag:OK}
+a=ice-pwd:{ice-password:OK}
+a=fingerprint:sha-256 8B:87:09:8A:5D:C2:F3:33:EF:C5:B1:F6:84:3A:3D:D6:A3:E2:9C:17:4C:E7:46:3B:1B:CE:84:98:DD:8E:AF:7B
+a=setup:active
+m=video 9 UDP/TLS/RTP/SAVPF 103 100 120
+c=IN IP4 0.0.0.0
+a=rtcp-mux
+a=recvonly
+a=mid:{mid:OK}
+a=rtpmap:103 H264/90000
+a=rtpmap:100 VP8/90000
+a=rtpmap:120 RTX/90000
+a=fmtp:103 packetization-mode=1
+a=fmtp:120 apt=100;rtx-time=200
+a=rtcp-fb:100 nack
+a=rtcp-fb:103 nack pli
+a=rtcp-fb:100 nack pli
+a=rtcp-fb:103 ccm fir
+a=rtcp-fb:100 ccm fir
+a=ssrc:{ssrc:OK} cname:{cname:OK}
+a=ice-ufrag:{ice-ufrag:OK}
+a=ice-pwd:{ice-password:OK}
+a=fingerprint:sha-256 8B:87:09:8A:5D:C2:F3:33:EF:C5:B1:F6:84:3A:3D:D6:A3:E2:9C:17:4C:E7:46:3B:1B:CE:84:98:DD:8E:AF:7B
+a=setup:active
+===
+
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/LayoutTests/fast/mediastream/RTCPeerConnection-inspect-answer.html b/LayoutTests/fast/mediastream/RTCPeerConnection-inspect-answer.html
new file mode 100644 (file)
index 0000000..0fd0038
--- /dev/null
@@ -0,0 +1,82 @@
+<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
+<html>
+    <head>
+        <script src="../../resources/js-test-pre.js"></script>
+        <script src="./resources/sdp-utils.js"></script>
+    </head>
+    <body>
+        <script>
+            let remoteStream;
+            let remotePcAudioTrack;
+            let remotePcVideoTrack;
+            const mediaDescriptionVariables = [];
+
+            description("Test RTCPeerConnection.setRemoteDescription called with an RTCSessionDescription of type 'offer'");
+
+            if (window.testRunner)
+                testRunner.setUserMediaPermission(true);
+            else {
+                debug("This test can not be run without the testRunner");
+                finishJSTest();
+            }
+
+            const pc = new webkitRTCPeerConnection({iceServers:[{urls:'stun:foo.com'}]});
+            const remotePc = new webkitRTCPeerConnection({iceServers:[{urls:'stun:foo.com'}]});
+
+            navigator.mediaDevices.getUserMedia({ "audio": true, "video": true })
+            .then(function (s) {
+                remoteStream = s;
+
+                remotePcAudioTrack = remoteStream.getAudioTracks()[0];
+                remotePcVideoTrack = remoteStream.getVideoTracks()[0];
+
+                remotePc.addTrack(remotePcAudioTrack, remoteStream);
+                return remotePc.createOffer();
+            })
+            .then(function (remoteOffer) {
+                return pc.setRemoteDescription(remoteOffer);
+            })
+            .then(function () {
+                return pc.createAnswer();
+            })
+            .then(function (answer) {
+                testPassed("Answer with audio created");
+
+                mediaDescriptionVariables.push({
+                    "trackId": remotePcAudioTrack.id,
+                    "streamId": remoteStream.id
+                });
+                printComparableSessionDescription(answer, mediaDescriptionVariables);
+
+                remotePc.addTrack(remotePcVideoTrack, remoteStream);
+                return remotePc.createOffer();
+            })
+            .then(function (remoteOffer) {
+                return pc.setRemoteDescription(remoteOffer);
+            })
+            .then(function () {
+                return pc.createAnswer();
+            })
+            .then(function (answer) {
+                testPassed("Answer with audio and video created");
+
+                mediaDescriptionVariables.push({
+                    "trackId": remotePcVideoTrack.id,
+                    "streamId": remoteStream.id
+                });
+                printComparableSessionDescription(answer, mediaDescriptionVariables);
+
+                finishJSTest();
+            })
+            .catch(function (error) {
+                testFailed("Error in promise chain: " + error);
+                finishJSTest();
+            });
+
+            window.jsTestIsAsync = true;
+            window.successfullyParsed = true;
+
+        </script>
+        <script src="../../resources/js-test-post.js"></script>
+    </body>
+</html>
index 2847b03..52a2072 100644 (file)
@@ -193,6 +193,7 @@ fast/mediastream/RTCSessionDescription.html
 fast/mediastream/RTCPeerConnection-setLocalDescription-offer.html
 fast/mediastream/RTCPeerConnection-setRemoteDescription-offer.html
 fast/mediastream/RTCTrackEvent-constructor.html
+fast/mediastream/RTCPeerConnection-inspect-answer.html
 
 # Asserts in debug.
 [ Debug ] fast/images/large-size-image-crash.html [ Skip ]
index e150b70..7ddfbeb 100644 (file)
@@ -1,3 +1,27 @@
+2016-06-10  Adam Bergkvist  <adam.bergkvist@ericsson.com>
+
+        WebRTC: Imlement MediaEndpointPeerConnection::createAnswer()
+        https://bugs.webkit.org/show_bug.cgi?id=158566
+
+        Reviewed by Eric Carlson.
+
+        Add the MediaEndpointPeerConnection implementation of RTCPeerConnection.createAnswer [1].
+        createAnswer() creates a 'reply' to an remote offer set with setRemoteDescription(),
+        completes the offer/answer dialog and brings the RTCPeerConnection back to the 'stable'
+        signaling state.
+
+        [1] https://w3c.github.io/webrtc-pc/archives/20160513/webrtc.html#dom-rtcpeerconnection-createanswer
+
+        Test: fast/mediastream/RTCPeerConnection-inspect-answer.html
+
+        * Modules/mediastream/MediaEndpointPeerConnection.cpp:
+        (WebCore::MediaEndpointPeerConnection::createOfferTask):
+        Align creation of RTCSessionDescription with createAnswerTask.
+        (WebCore::MediaEndpointPeerConnection::createAnswer):
+        (WebCore::MediaEndpointPeerConnection::createAnswerTask):
+        Add Implementation.
+        * Modules/mediastream/MediaEndpointPeerConnection.h:
+
 2016-06-08  Sergio Villar Senin  <svillar@igalia.com>
 
         [css-grid] CRASH when getting the computed style of a grid with only absolutely positioned children
index 0bfbfe5..00431b6 100644 (file)
@@ -191,23 +191,91 @@ void MediaEndpointPeerConnection::createOfferTask(RTCOfferOptions&, SessionDescr
         configurationSnapshot->addMediaDescription(WTFMove(mediaDescription));
     }
 
-    String sdpString;
-    SDPProcessor::Result result = m_sdpProcessor->generate(*configurationSnapshot, sdpString);
-    if (result != SDPProcessor::Result::Success) {
-        LOG_ERROR("SDPProcessor internal error");
-        return;
-    }
-
-    promise.resolve(RTCSessionDescription::create(RTCSessionDescription::SdpType::Offer, sdpString));
+    auto description = MediaEndpointSessionDescription::create(RTCSessionDescription::SdpType::Offer, WTFMove(configurationSnapshot));
+    promise.resolve(*description->toRTCSessionDescription(*m_sdpProcessor));
 }
 
 void MediaEndpointPeerConnection::createAnswer(RTCAnswerOptions& options, SessionDescriptionPromise&& promise)
 {
-    UNUSED_PARAM(options);
+    runTask([this, protectedOptions = RefPtr<RTCAnswerOptions>(&options), protectedPromise = WTFMove(promise)]() mutable {
+        createAnswerTask(*protectedOptions, protectedPromise);
+    });
+}
 
-    notImplemented();
+void MediaEndpointPeerConnection::createAnswerTask(RTCAnswerOptions&, SessionDescriptionPromise& promise)
+{
+    ASSERT(!m_dtlsFingerprint.isEmpty());
 
-    promise.reject(NOT_SUPPORTED_ERR);
+    if (m_client->internalSignalingState() == SignalingState::Closed)
+        return;
+
+    if (!internalRemoteDescription()) {
+        promise.reject(INVALID_STATE_ERR, "No remote description set");
+        return;
+    }
+
+    MediaEndpointSessionDescription* localDescription = internalLocalDescription();
+    RefPtr<MediaEndpointSessionConfiguration> configurationSnapshot = localDescription ?
+        localDescription->configuration()->clone() : MediaEndpointSessionConfiguration::create();
+
+    configurationSnapshot->setSessionVersion(m_sdpAnswerSessionVersion++);
+
+    auto transceivers = RtpTransceiverVector(m_client->getTransceivers());
+    const MediaDescriptionVector& remoteMediaDescriptions = internalRemoteDescription()->configuration()->mediaDescriptions();
+
+    for (unsigned i = 0; i < remoteMediaDescriptions.size(); ++i) {
+        PeerMediaDescription& remoteMediaDescription = *remoteMediaDescriptions[i];
+
+        RTCRtpTransceiver* transceiver = matchTransceiverByMid(transceivers, remoteMediaDescription.mid());
+        if (!transceiver) {
+            LOG_ERROR("Could not find a matching transceiver for remote description while creating answer");
+            continue;
+        }
+
+        if (i >= configurationSnapshot->mediaDescriptions().size()) {
+            auto newMediaDescription = PeerMediaDescription::create();
+
+            RTCRtpSender& sender = *transceiver->sender();
+            if (sender.track()) {
+                if (sender.mediaStreamIds().size())
+                    newMediaDescription->setMediaStreamId(sender.mediaStreamIds()[0]);
+                newMediaDescription->setMediaStreamTrackId(sender.trackId());
+                newMediaDescription->addSsrc(cryptographicallyRandomNumber());
+            }
+
+            newMediaDescription->setMode(transceiver->directionString());
+            newMediaDescription->setType(remoteMediaDescription.type());
+            newMediaDescription->setMid(remoteMediaDescription.mid());
+            newMediaDescription->setDtlsSetup(remoteMediaDescription.dtlsSetup() == "active" ? "passive" : "active");
+            newMediaDescription->setDtlsFingerprintHashFunction(m_dtlsFingerprintFunction);
+            newMediaDescription->setDtlsFingerprint(m_dtlsFingerprint);
+            newMediaDescription->setCname(m_cname);
+            newMediaDescription->setIceUfrag(m_iceUfrag);
+            newMediaDescription->setIcePassword(m_icePassword);
+
+            configurationSnapshot->addMediaDescription(WTFMove(newMediaDescription));
+        }
+
+        PeerMediaDescription& localMediaDescription = *configurationSnapshot->mediaDescriptions()[i];
+
+        localMediaDescription.setPayloads(remoteMediaDescription.payloads());
+        localMediaDescription.setRtcpMux(remoteMediaDescription.rtcpMux());
+
+        if (!localMediaDescription.ssrcs().size())
+            localMediaDescription.addSsrc(cryptographicallyRandomNumber());
+
+        if (localMediaDescription.dtlsSetup() == "actpass")
+            localMediaDescription.setDtlsSetup("passive");
+
+        transceivers.removeFirst(transceiver);
+    }
+
+    // Unassociated (non-stopped) transceivers need to be negotiated in a follow-up offer.
+    if (hasUnassociatedTransceivers(transceivers))
+        markAsNeedingNegotiation();
+
+    auto description = MediaEndpointSessionDescription::create(RTCSessionDescription::SdpType::Answer, WTFMove(configurationSnapshot));
+    promise.resolve(*description->toRTCSessionDescription(*m_sdpProcessor));
 }
 
 static RealtimeMediaSourceMap createSourceMap(const MediaDescriptionVector& remoteMediaDescriptions, unsigned localMediaDescriptionCount, const RtpTransceiverVector& transceivers)
index 3169c56..f017b2e 100644 (file)
@@ -87,6 +87,7 @@ private:
     void startRunningTasks();
 
     void createOfferTask(RTCOfferOptions&, PeerConnection::SessionDescriptionPromise&);
+    void createAnswerTask(RTCAnswerOptions&, PeerConnection::SessionDescriptionPromise&);
 
     void setLocalDescriptionTask(RefPtr<RTCSessionDescription>&&, PeerConnection::VoidPromise&);
     void setRemoteDescriptionTask(RefPtr<RTCSessionDescription>&&, PeerConnection::VoidPromise&);
@@ -120,6 +121,7 @@ private:
     String m_dtlsFingerprint;
     String m_dtlsFingerprintFunction;
     unsigned m_sdpOfferSessionVersion { 0 };
+    unsigned m_sdpAnswerSessionVersion { 0 };
 
     RefPtr<MediaEndpointSessionDescription> m_currentLocalDescription;
     RefPtr<MediaEndpointSessionDescription> m_pendingLocalDescription;