summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/webrtc
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
commit6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch)
treea68f146d7fa01f0134297619fbe7e33db084e0aa /testing/web-platform/tests/webrtc
parentInitial commit. (diff)
downloadthunderbird-upstream.tar.xz
thunderbird-upstream.zip
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--testing/web-platform/tests/webrtc-encoded-transform/META.yml1
-rw-r--r--testing/web-platform/tests/webrtc-encoded-transform/RTCEncodedAudioFrame-clone.https.html50
-rw-r--r--testing/web-platform/tests/webrtc-encoded-transform/RTCEncodedAudioFrame-serviceworker-failure.https.html60
-rw-r--r--testing/web-platform/tests/webrtc-encoded-transform/RTCEncodedVideoFrame-clone.https.html61
-rw-r--r--testing/web-platform/tests/webrtc-encoded-transform/RTCEncodedVideoFrame-serviceworker-failure.https.html60
-rw-r--r--testing/web-platform/tests/webrtc-encoded-transform/RTCPeerConnection-insertable-streams-audio.https.html212
-rw-r--r--testing/web-platform/tests/webrtc-encoded-transform/RTCPeerConnection-insertable-streams-errors.https.html87
-rw-r--r--testing/web-platform/tests/webrtc-encoded-transform/RTCPeerConnection-insertable-streams-simulcast.https.html89
-rw-r--r--testing/web-platform/tests/webrtc-encoded-transform/RTCPeerConnection-insertable-streams-video-frames.https.html80
-rw-r--r--testing/web-platform/tests/webrtc-encoded-transform/RTCPeerConnection-insertable-streams-video.https.html149
-rw-r--r--testing/web-platform/tests/webrtc-encoded-transform/RTCPeerConnection-insertable-streams-worker.https.html196
-rw-r--r--testing/web-platform/tests/webrtc-encoded-transform/RTCPeerConnection-insertable-streams.js242
-rw-r--r--testing/web-platform/tests/webrtc-encoded-transform/RTCPeerConnection-sender-worker-single-frame.js19
-rw-r--r--testing/web-platform/tests/webrtc-encoded-transform/RTCPeerConnection-worker-transform.js22
-rw-r--r--testing/web-platform/tests/webrtc-encoded-transform/codec-specific-metadata.https.html45
-rw-r--r--testing/web-platform/tests/webrtc-encoded-transform/helper.js26
-rw-r--r--testing/web-platform/tests/webrtc-encoded-transform/idlharness.https.window.js18
-rw-r--r--testing/web-platform/tests/webrtc-encoded-transform/resources/blank.html2
-rw-r--r--testing/web-platform/tests/webrtc-encoded-transform/resources/serviceworker-failure.js30
-rw-r--r--testing/web-platform/tests/webrtc-encoded-transform/routines.js32
-rw-r--r--testing/web-platform/tests/webrtc-encoded-transform/script-audio-transform-worker.js30
-rw-r--r--testing/web-platform/tests/webrtc-encoded-transform/script-audio-transform.https.html69
-rw-r--r--testing/web-platform/tests/webrtc-encoded-transform/script-change-transform-worker.js39
-rw-r--r--testing/web-platform/tests/webrtc-encoded-transform/script-change-transform.https.html61
-rw-r--r--testing/web-platform/tests/webrtc-encoded-transform/script-late-transform.https.html94
-rw-r--r--testing/web-platform/tests/webrtc-encoded-transform/script-metadata-transform-worker.js24
-rw-r--r--testing/web-platform/tests/webrtc-encoded-transform/script-metadata-transform.https.html91
-rw-r--r--testing/web-platform/tests/webrtc-encoded-transform/script-transform-worker.js25
-rw-r--r--testing/web-platform/tests/webrtc-encoded-transform/script-transform.https.html153
-rw-r--r--testing/web-platform/tests/webrtc-encoded-transform/script-write-twice-transform-worker.js22
-rw-r--r--testing/web-platform/tests/webrtc-encoded-transform/script-write-twice-transform.https.html62
-rw-r--r--testing/web-platform/tests/webrtc-encoded-transform/set-metadata.https.html83
-rw-r--r--testing/web-platform/tests/webrtc-encoded-transform/sframe-keys.https.html69
-rw-r--r--testing/web-platform/tests/webrtc-encoded-transform/sframe-transform-buffer-source.html50
-rw-r--r--testing/web-platform/tests/webrtc-encoded-transform/sframe-transform-in-worker.https.html61
-rw-r--r--testing/web-platform/tests/webrtc-encoded-transform/sframe-transform-readable.html19
-rw-r--r--testing/web-platform/tests/webrtc-encoded-transform/sframe-transform-worker.js7
-rw-r--r--testing/web-platform/tests/webrtc-encoded-transform/sframe-transform.html141
-rw-r--r--testing/web-platform/tests/webrtc-extensions/META.yml3
-rw-r--r--testing/web-platform/tests/webrtc-extensions/RTCOAuthCredential.html44
-rw-r--r--testing/web-platform/tests/webrtc-extensions/RTCRtpParameters-adaptivePtime.html42
-rw-r--r--testing/web-platform/tests/webrtc-extensions/RTCRtpParameters-codec.html422
-rw-r--r--testing/web-platform/tests/webrtc-extensions/RTCRtpParameters-maxFramerate.html101
-rw-r--r--testing/web-platform/tests/webrtc-extensions/RTCRtpReceiver-jitterBufferTarget-stats.html96
-rw-r--r--testing/web-platform/tests/webrtc-extensions/RTCRtpReceiver-jitterBufferTarget.html130
-rw-r--r--testing/web-platform/tests/webrtc-extensions/RTCRtpSynchronizationSource-captureTimestamp.html94
-rw-r--r--testing/web-platform/tests/webrtc-extensions/RTCRtpSynchronizationSource-helper.js140
-rw-r--r--testing/web-platform/tests/webrtc-extensions/RTCRtpSynchronizationSource-senderCaptureTimeOffset.html92
-rw-r--r--testing/web-platform/tests/webrtc-extensions/RTCRtpTransceiver-headerExtensionControl.html295
-rw-r--r--testing/web-platform/tests/webrtc-extensions/transfer-datachannel-service-worker.https.html83
-rw-r--r--testing/web-platform/tests/webrtc-extensions/transfer-datachannel-service-worker.js15
-rw-r--r--testing/web-platform/tests/webrtc-extensions/transfer-datachannel-worker.js19
-rw-r--r--testing/web-platform/tests/webrtc-extensions/transfer-datachannel.html165
-rw-r--r--testing/web-platform/tests/webrtc-ice/META.yml3
-rw-r--r--testing/web-platform/tests/webrtc-ice/RTCIceTransport-extension-helper.js42
-rw-r--r--testing/web-platform/tests/webrtc-ice/RTCIceTransport-extension.https.html362
-rw-r--r--testing/web-platform/tests/webrtc-identity/META.yml4
-rw-r--r--testing/web-platform/tests/webrtc-identity/RTCPeerConnection-constructor.html11
-rw-r--r--testing/web-platform/tests/webrtc-identity/RTCPeerConnection-getIdentityAssertion.sub.https.html397
-rw-r--r--testing/web-platform/tests/webrtc-identity/RTCPeerConnection-peerIdentity.https.html328
-rw-r--r--testing/web-platform/tests/webrtc-identity/identity-helper.sub.js70
-rw-r--r--testing/web-platform/tests/webrtc-identity/idlharness.https.window.js24
-rw-r--r--testing/web-platform/tests/webrtc-priority/META.yml1
-rw-r--r--testing/web-platform/tests/webrtc-priority/RTCPeerConnection-ondatachannel.html66
-rw-r--r--testing/web-platform/tests/webrtc-priority/RTCRtpParameters-encodings.html44
-rw-r--r--testing/web-platform/tests/webrtc-stats/META.yml5
-rw-r--r--testing/web-platform/tests/webrtc-stats/README.md7
-rw-r--r--testing/web-platform/tests/webrtc-stats/getStats-remote-candidate-address.html81
-rw-r--r--testing/web-platform/tests/webrtc-stats/hardware-capability-stats.https.html107
-rw-r--r--testing/web-platform/tests/webrtc-stats/idlharness.window.js14
-rw-r--r--testing/web-platform/tests/webrtc-stats/outbound-rtp.https.html49
-rw-r--r--testing/web-platform/tests/webrtc-stats/rtp-stats-creation.html110
-rw-r--r--testing/web-platform/tests/webrtc-stats/supported-stats.https.html212
-rw-r--r--testing/web-platform/tests/webrtc-svc/META.yml1
-rw-r--r--testing/web-platform/tests/webrtc-svc/RTCRtpParameters-scalability-av1.html43
-rw-r--r--testing/web-platform/tests/webrtc-svc/RTCRtpParameters-scalability-h264.html18
-rw-r--r--testing/web-platform/tests/webrtc-svc/RTCRtpParameters-scalability-vp8.html18
-rw-r--r--testing/web-platform/tests/webrtc-svc/RTCRtpParameters-scalability-vp9.html43
-rw-r--r--testing/web-platform/tests/webrtc-svc/RTCRtpParameters-scalability.html93
-rw-r--r--testing/web-platform/tests/webrtc-svc/svc-helper.js50
-rw-r--r--testing/web-platform/tests/webrtc/META.yml9
-rw-r--r--testing/web-platform/tests/webrtc/README.md12
-rw-r--r--testing/web-platform/tests/webrtc/RTCCertificate-postMessage.html78
-rw-r--r--testing/web-platform/tests/webrtc/RTCCertificate.html283
-rw-r--r--testing/web-platform/tests/webrtc/RTCConfiguration-bundlePolicy.html128
-rw-r--r--testing/web-platform/tests/webrtc/RTCConfiguration-helper.js24
-rw-r--r--testing/web-platform/tests/webrtc/RTCConfiguration-iceCandidatePoolSize.html117
-rw-r--r--testing/web-platform/tests/webrtc/RTCConfiguration-iceServers.html330
-rw-r--r--testing/web-platform/tests/webrtc/RTCConfiguration-iceTransportPolicy.html306
-rw-r--r--testing/web-platform/tests/webrtc/RTCConfiguration-rtcpMuxPolicy.html196
-rw-r--r--testing/web-platform/tests/webrtc/RTCDTMFSender-helper.js149
-rw-r--r--testing/web-platform/tests/webrtc/RTCDTMFSender-insertDTMF.https.html176
-rw-r--r--testing/web-platform/tests/webrtc/RTCDTMFSender-ontonechange-long.https.html50
-rw-r--r--testing/web-platform/tests/webrtc/RTCDTMFSender-ontonechange.https.html294
-rw-r--r--testing/web-platform/tests/webrtc/RTCDataChannel-binaryType.window.js27
-rw-r--r--testing/web-platform/tests/webrtc/RTCDataChannel-bufferedAmount.html287
-rw-r--r--testing/web-platform/tests/webrtc/RTCDataChannel-close.html180
-rw-r--r--testing/web-platform/tests/webrtc/RTCDataChannel-iceRestart.html76
-rw-r--r--testing/web-platform/tests/webrtc/RTCDataChannel-id.html345
-rw-r--r--testing/web-platform/tests/webrtc/RTCDataChannel-send-blob-order.html31
-rw-r--r--testing/web-platform/tests/webrtc/RTCDataChannel-send.html336
-rw-r--r--testing/web-platform/tests/webrtc/RTCDataChannelEvent-constructor.html41
-rw-r--r--testing/web-platform/tests/webrtc/RTCDtlsTransport-getRemoteCertificates.html97
-rw-r--r--testing/web-platform/tests/webrtc/RTCDtlsTransport-state.html142
-rw-r--r--testing/web-platform/tests/webrtc/RTCError.html89
-rw-r--r--testing/web-platform/tests/webrtc/RTCIceCandidate-constructor.html234
-rw-r--r--testing/web-platform/tests/webrtc/RTCIceConnectionState-candidate-pair.https.html33
-rw-r--r--testing/web-platform/tests/webrtc/RTCIceTransport.html193
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-GC.https.html92
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-SLD-SRD-timing.https.html24
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-add-track-no-deadlock.https.html31
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-addIceCandidate-connectionSetup.html92
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-addIceCandidate-timing.https.html149
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-addIceCandidate.html631
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-addTrack.https.html394
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-addTransceiver.https.html441
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-canTrickleIceCandidates.html62
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-candidate-in-sdp.https.html26
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-capture-video.https.html72
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-connectionState.https.html291
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-constructor.html76
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-createAnswer.html41
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-createDataChannel.html758
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-createOffer.html134
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-description-attributes-timing.https.html81
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-explicit-rollback-iceGatheringState.html53
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-generateCertificate.html138
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-getStats.https.html422
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-getTransceivers.html39
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-helper-test.html21
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-helper.js722
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-iceConnectionState-disconnected.https.html30
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-iceConnectionState.https.html396
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-iceGatheringState.html244
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-mandatory-getStats.https.html277
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-ondatachannel.html374
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-onicecandidateerror.https.html38
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-onnegotiationneeded.html627
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-onsignalingstatechanged.https.html71
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-ontrack.https.html258
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-operations.https.html425
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-perfect-negotiation-helper.js153
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-perfect-negotiation-stress-glare-linear.https.html23
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-perfect-negotiation-stress-glare.https.html23
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-perfect-negotiation.https.html22
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-plan-b-is-not-supported.html28
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-relay-canvas.https.html84
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-remote-track-mute.https.html132
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-removeTrack.https.html338
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-restartIce-onnegotiationneeded.https.html29
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-restartIce.https.html482
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-setDescription-transceiver.html295
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-setLocalDescription-answer.html230
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-setLocalDescription-offer.html229
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-setLocalDescription-parameterless.https.html170
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-setLocalDescription-pranswer.html166
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-setLocalDescription-rollback.html167
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-setLocalDescription.html152
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-setRemoteDescription-answer.html123
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-setRemoteDescription-nomsid.html40
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-setRemoteDescription-offer.html356
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-setRemoteDescription-pranswer.html166
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-setRemoteDescription-replaceTrack.https.html115
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-setRemoteDescription-rollback.html602
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-setRemoteDescription-simulcast.https.html50
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-setRemoteDescription-tracks.https.html385
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-setRemoteDescription.html171
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-transceivers.https.html509
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-transport-stats.https.html46
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-videoDetectorTest.html84
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnectionIceErrorEvent.html26
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnectionIceEvent-constructor.html126
-rw-r--r--testing/web-platform/tests/webrtc/RTCRtpCapabilities-helper.js52
-rw-r--r--testing/web-platform/tests/webrtc/RTCRtpParameters-codecs.html206
-rw-r--r--testing/web-platform/tests/webrtc/RTCRtpParameters-encodings.html543
-rw-r--r--testing/web-platform/tests/webrtc/RTCRtpParameters-headerExtensions.html74
-rw-r--r--testing/web-platform/tests/webrtc/RTCRtpParameters-helper.js259
-rw-r--r--testing/web-platform/tests/webrtc/RTCRtpParameters-rtcp.html104
-rw-r--r--testing/web-platform/tests/webrtc/RTCRtpParameters-transactionId.html190
-rw-r--r--testing/web-platform/tests/webrtc/RTCRtpReceiver-getCapabilities.html39
-rw-r--r--testing/web-platform/tests/webrtc/RTCRtpReceiver-getContributingSources.https.html35
-rw-r--r--testing/web-platform/tests/webrtc/RTCRtpReceiver-getParameters.html73
-rw-r--r--testing/web-platform/tests/webrtc/RTCRtpReceiver-getStats.https.html145
-rw-r--r--testing/web-platform/tests/webrtc/RTCRtpReceiver-getSynchronizationSources.https.html105
-rw-r--r--testing/web-platform/tests/webrtc/RTCRtpSender-encode-same-track-twice.https.html66
-rw-r--r--testing/web-platform/tests/webrtc/RTCRtpSender-getCapabilities.html45
-rw-r--r--testing/web-platform/tests/webrtc/RTCRtpSender-getStats.https.html97
-rw-r--r--testing/web-platform/tests/webrtc/RTCRtpSender-replaceTrack.https.html338
-rw-r--r--testing/web-platform/tests/webrtc/RTCRtpSender-setParameters.html52
-rw-r--r--testing/web-platform/tests/webrtc/RTCRtpSender-setStreams.https.html127
-rw-r--r--testing/web-platform/tests/webrtc/RTCRtpSender-transport.https.html152
-rw-r--r--testing/web-platform/tests/webrtc/RTCRtpSender.https.html20
-rw-r--r--testing/web-platform/tests/webrtc/RTCRtpTransceiver-direction.html94
-rw-r--r--testing/web-platform/tests/webrtc/RTCRtpTransceiver-setCodecPreferences.html322
-rw-r--r--testing/web-platform/tests/webrtc/RTCRtpTransceiver-stop.html155
-rw-r--r--testing/web-platform/tests/webrtc/RTCRtpTransceiver-stopping.https.html217
-rw-r--r--testing/web-platform/tests/webrtc/RTCRtpTransceiver.https.html2297
-rw-r--r--testing/web-platform/tests/webrtc/RTCSctpTransport-constructor.html125
-rw-r--r--testing/web-platform/tests/webrtc/RTCSctpTransport-events.html55
-rw-r--r--testing/web-platform/tests/webrtc/RTCSctpTransport-maxChannels.html49
-rw-r--r--testing/web-platform/tests/webrtc/RTCSctpTransport-maxMessageSize.html206
-rw-r--r--testing/web-platform/tests/webrtc/RTCStats-helper.js973
-rw-r--r--testing/web-platform/tests/webrtc/RTCTrackEvent-constructor.html159
-rw-r--r--testing/web-platform/tests/webrtc/RTCTrackEvent-fire.html168
-rw-r--r--testing/web-platform/tests/webrtc/RollbackEvents.https.html231
-rw-r--r--testing/web-platform/tests/webrtc/coverage/RTCDTMFSender.txt122
-rw-r--r--testing/web-platform/tests/webrtc/coverage/identity.txt220
-rw-r--r--testing/web-platform/tests/webrtc/coverage/set-session-description.txt240
-rw-r--r--testing/web-platform/tests/webrtc/dictionary-helper.js101
-rw-r--r--testing/web-platform/tests/webrtc/getstats.html130
-rw-r--r--testing/web-platform/tests/webrtc/historical.html51
-rw-r--r--testing/web-platform/tests/webrtc/idlharness.https.window.js146
-rw-r--r--testing/web-platform/tests/webrtc/legacy/README.txt2
-rw-r--r--testing/web-platform/tests/webrtc/legacy/RTCPeerConnection-createOffer-offerToReceive.html274
-rw-r--r--testing/web-platform/tests/webrtc/legacy/RTCRtpTransceiver-with-OfferToReceive-options.https.html172
-rw-r--r--testing/web-platform/tests/webrtc/legacy/onaddstream.https.html157
-rw-r--r--testing/web-platform/tests/webrtc/no-media-call.html100
-rw-r--r--testing/web-platform/tests/webrtc/promises-call.html113
-rw-r--r--testing/web-platform/tests/webrtc/protocol/README.txt22
-rw-r--r--testing/web-platform/tests/webrtc/protocol/RTCPeerConnection-payloadTypes.html49
-rw-r--r--testing/web-platform/tests/webrtc/protocol/bundle.https.html150
-rw-r--r--testing/web-platform/tests/webrtc/protocol/candidate-exchange.https.html218
-rw-r--r--testing/web-platform/tests/webrtc/protocol/crypto-suite.https.html85
-rw-r--r--testing/web-platform/tests/webrtc/protocol/dtls-certificates.html42
-rw-r--r--testing/web-platform/tests/webrtc/protocol/dtls-fingerprint-validation.html37
-rw-r--r--testing/web-platform/tests/webrtc/protocol/dtls-setup.https.html135
-rw-r--r--testing/web-platform/tests/webrtc/protocol/h264-profile-levels.https.html115
-rw-r--r--testing/web-platform/tests/webrtc/protocol/handover-datachannel.html61
-rw-r--r--testing/web-platform/tests/webrtc/protocol/handover.html72
-rw-r--r--testing/web-platform/tests/webrtc/protocol/ice-state.https.html130
-rw-r--r--testing/web-platform/tests/webrtc/protocol/ice-ufragpwd.html55
-rw-r--r--testing/web-platform/tests/webrtc/protocol/jsep-initial-offer.https.html41
-rw-r--r--testing/web-platform/tests/webrtc/protocol/missing-fields.html47
-rw-r--r--testing/web-platform/tests/webrtc/protocol/msid-generate.html160
-rw-r--r--testing/web-platform/tests/webrtc/protocol/msid-parse.html83
-rw-r--r--testing/web-platform/tests/webrtc/protocol/rtp-clockrate.html40
-rw-r--r--testing/web-platform/tests/webrtc/protocol/rtp-demuxing.html109
-rw-r--r--testing/web-platform/tests/webrtc/protocol/rtp-extension-support.html78
-rw-r--r--testing/web-platform/tests/webrtc/protocol/rtp-headerextensions.html101
-rw-r--r--testing/web-platform/tests/webrtc/protocol/rtp-payloadtypes.html61
-rw-r--r--testing/web-platform/tests/webrtc/protocol/rtx-codecs.https.html153
-rw-r--r--testing/web-platform/tests/webrtc/protocol/sctp-format.html25
-rw-r--r--testing/web-platform/tests/webrtc/protocol/sdes-dont-dont-dont.html55
-rw-r--r--testing/web-platform/tests/webrtc/protocol/simulcast-answer.html101
-rw-r--r--testing/web-platform/tests/webrtc/protocol/simulcast-offer.html33
-rw-r--r--testing/web-platform/tests/webrtc/protocol/split.https.html98
-rw-r--r--testing/web-platform/tests/webrtc/protocol/unknown-mediatypes.html34
-rw-r--r--testing/web-platform/tests/webrtc/protocol/video-codecs.https.html95
-rw-r--r--testing/web-platform/tests/webrtc/protocol/vp8-fmtp.html44
-rw-r--r--testing/web-platform/tests/webrtc/receiver-track-live.https.html72
-rw-r--r--testing/web-platform/tests/webrtc/recvonly-transceiver-can-become-sendrecv.https.html50
-rw-r--r--testing/web-platform/tests/webrtc/resources/RTCCertificate-postMessage-iframe.html9
-rw-r--r--testing/web-platform/tests/webrtc/simplecall-no-ssrcs.https.html118
-rw-r--r--testing/web-platform/tests/webrtc/simplecall.https.html109
-rw-r--r--testing/web-platform/tests/webrtc/simulcast/basic.https.html23
-rw-r--r--testing/web-platform/tests/webrtc/simulcast/getStats.https.html34
-rw-r--r--testing/web-platform/tests/webrtc/simulcast/h264.https.html31
-rw-r--r--testing/web-platform/tests/webrtc/simulcast/negotiation-encodings.https.html534
-rw-r--r--testing/web-platform/tests/webrtc/simulcast/rid-manipulation.html39
-rw-r--r--testing/web-platform/tests/webrtc/simulcast/setParameters-active.https.html104
-rw-r--r--testing/web-platform/tests/webrtc/simulcast/setParameters-encodings.https.html462
-rw-r--r--testing/web-platform/tests/webrtc/simulcast/simulcast.js254
-rw-r--r--testing/web-platform/tests/webrtc/simulcast/vp8.https.html26
-rw-r--r--testing/web-platform/tests/webrtc/simulcast/vp9-scalability-mode.https.html35
-rw-r--r--testing/web-platform/tests/webrtc/simulcast/vp9.https.html26
-rw-r--r--testing/web-platform/tests/webrtc/third_party/README.md5
-rw-r--r--testing/web-platform/tests/webrtc/third_party/sdp/LICENSE19
-rw-r--r--testing/web-platform/tests/webrtc/third_party/sdp/sdp.js825
-rw-r--r--testing/web-platform/tests/webrtc/toJSON.html48
-rw-r--r--testing/web-platform/tests/webrtc/tools/.eslintrc.js154
-rw-r--r--testing/web-platform/tests/webrtc/tools/README.md14
-rw-r--r--testing/web-platform/tests/webrtc/tools/codemod-peerconnection-addcleanup58
-rw-r--r--testing/web-platform/tests/webrtc/tools/html-codemod.js34
-rw-r--r--testing/web-platform/tests/webrtc/tools/package.json16
274 files changed, 39069 insertions, 0 deletions
diff --git a/testing/web-platform/tests/webrtc-encoded-transform/META.yml b/testing/web-platform/tests/webrtc-encoded-transform/META.yml
new file mode 100644
index 0000000000..6365c8d16a
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-encoded-transform/META.yml
@@ -0,0 +1 @@
+spec: https://w3c.github.io/webrtc-encoded-transform/
diff --git a/testing/web-platform/tests/webrtc-encoded-transform/RTCEncodedAudioFrame-clone.https.html b/testing/web-platform/tests/webrtc-encoded-transform/RTCEncodedAudioFrame-clone.https.html
new file mode 100644
index 0000000000..ec99338772
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-encoded-transform/RTCEncodedAudioFrame-clone.https.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>RTCEncodedAudioFrame can be cloned and distributed</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src=/resources/testdriver.js></script>
+<script src=/resources/testdriver-vendor.js></script>
+<script src='../mediacapture-streams/permission-helper.js'></script>
+<script src="../webrtc/RTCPeerConnection-helper.js"></script>
+<script src="../service-workers/service-worker/resources/test-helpers.sub.js"></script>
+
+<script>
+"use strict";
+promise_test(async t => {
+ const caller1 = new RTCPeerConnection();
+ t.add_cleanup(() => caller1.close());
+ const callee1 = new RTCPeerConnection({encodedInsertableStreams:true});
+ t.add_cleanup(() => callee1.close());
+ await setMediaPermission("granted", ["microphone"]);
+ const inputStream = await navigator.mediaDevices.getUserMedia({audio:true});
+ const inputTrack = inputStream.getAudioTracks()[0];
+ t.add_cleanup(() => inputTrack.stop());
+ caller1.addTrack(inputTrack)
+ exchangeIceCandidates(caller1, callee1);
+
+ const caller2 = new RTCPeerConnection({encodedInsertableStreams:true});
+ t.add_cleanup(() => caller2.close());
+ const sender2 = caller2.addTransceiver("audio").sender;
+ const writer2 = sender2.createEncodedStreams().writable.getWriter();
+ sender2.replaceTrack(new MediaStreamTrackGenerator({ kind: 'audio' }));
+
+ const framesReceivedCorrectly = new Promise((resolve, reject) => {
+ callee1.ontrack = async e => {
+ const receiverStreams = e.receiver.createEncodedStreams();
+ const receiverReader = receiverStreams.readable.getReader();
+ const result = await receiverReader.read();
+ const original = result.value;
+ let clone = original.clone();
+ assert_equals(original.timestamp, clone.timestamp);
+ assert_array_equals(Array.from(original.data), Array.from(clone.data));
+ await writer2.write(clone);
+ resolve();
+ }
+ });
+
+ await exchangeOfferAnswer(caller1, callee1);
+
+ return framesReceivedCorrectly;
+}, "Cloning before sending works");
+</script>
diff --git a/testing/web-platform/tests/webrtc-encoded-transform/RTCEncodedAudioFrame-serviceworker-failure.https.html b/testing/web-platform/tests/webrtc-encoded-transform/RTCEncodedAudioFrame-serviceworker-failure.https.html
new file mode 100644
index 0000000000..d6d8578dbd
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-encoded-transform/RTCEncodedAudioFrame-serviceworker-failure.https.html
@@ -0,0 +1,60 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<!-- Based on similar tests in html/infrastructure/safe-passing-of-structured-data/shared-array-buffers/ -->
+<title>RTCEncodedVideoFrame cannot cross agent clusters, service worker edition</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src=/resources/testdriver.js></script>
+<script src=/resources/testdriver-vendor.js></script>
+<script src='../mediacapture-streams/permission-helper.js'></script>
+<script src="../webrtc/RTCPeerConnection-helper.js"></script>
+<script src="../service-workers/service-worker/resources/test-helpers.sub.js"></script>
+
+<script>
+"use strict";
+promise_test(async t => {
+ const caller = new RTCPeerConnection({encodedInsertableStreams:true});
+ t.add_cleanup(() => caller.close());
+ const callee = new RTCPeerConnection();
+ t.add_cleanup(() => callee.close());
+
+ await setMediaPermission("granted", ["microphone"]);
+ const stream = await navigator.mediaDevices.getUserMedia({audio:true});
+ const track = stream.getTracks()[0];
+ t.add_cleanup(() => track.stop());
+
+ const sender = caller.addTrack(track)
+ const streams = sender.createEncodedStreams();
+ const reader = streams.readable.getReader();
+ const writer = streams.writable.getWriter();
+
+ exchangeIceCandidates(caller, callee);
+ await exchangeOfferAnswer(caller, callee);
+
+ const result = await reader.read();
+ const scope = "resources/blank.html";
+ let reg = await service_worker_unregister_and_register(t, "resources/serviceworker-failure.js", scope);
+ t.add_cleanup(() => service_worker_unregister(t, scope));
+ await wait_for_state(t, reg.installing, "activated");
+ let iframe = await with_iframe(scope);
+ t.add_cleanup(() => iframe.remove());
+ const sw = iframe.contentWindow.navigator.serviceWorker;
+ let state = "start in window";
+ return new Promise(resolve => {
+ sw.onmessage = t.step_func(e => {
+ if (e.data === "start in worker") {
+ assert_equals(state, "start in window");
+ sw.controller.postMessage(result.value);
+ state = "we are expecting confirmation of an onmessageerror in the worker";
+ } else if (e.data === "onmessageerror was received in worker") {
+ assert_equals(state, "we are expecting confirmation of an onmessageerror in the worker");
+ resolve();
+ } else {
+ assert_unreached("Got an unexpected message from the service worker: " + e.data);
+ }
+ });
+
+ sw.controller.postMessage(state);
+ });
+});
+</script>
diff --git a/testing/web-platform/tests/webrtc-encoded-transform/RTCEncodedVideoFrame-clone.https.html b/testing/web-platform/tests/webrtc-encoded-transform/RTCEncodedVideoFrame-clone.https.html
new file mode 100644
index 0000000000..29330729dc
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-encoded-transform/RTCEncodedVideoFrame-clone.https.html
@@ -0,0 +1,61 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>RTCEncodedVideoFrame can be cloned and distributed</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src=/resources/testdriver.js></script>
+<script src=/resources/testdriver-vendor.js></script>
+<script src='../mediacapture-streams/permission-helper.js'></script>
+<script src="../webrtc/RTCPeerConnection-helper.js"></script>
+<script src="../service-workers/service-worker/resources/test-helpers.sub.js"></script>
+
+<script>
+"use strict";
+promise_test(async t => {
+ const caller = new RTCPeerConnection({encodedInsertableStreams:true});
+ t.add_cleanup(() => caller.close());
+ const callee = new RTCPeerConnection({encodedInsertableStreams:true});
+ t.add_cleanup(() => callee.close());
+
+ await setMediaPermission("granted", ["camera"]);
+ const stream = await navigator.mediaDevices.getUserMedia({video:true});
+ const videoTrack = stream.getVideoTracks()[0];
+ t.add_cleanup(() => videoTrack.stop());
+
+ const videoSender = caller.addTrack(videoTrack)
+ const senderStreams = videoSender.createEncodedStreams();
+ const senderReader = senderStreams.readable.getReader();
+ const senderWriter = senderStreams.writable.getWriter();
+
+ exchangeIceCandidates(caller, callee);
+
+ // Send 10 frames and stop
+ const numFramesToSend = 10;
+
+ const framesReceivedCorrectly = new Promise((resolve, reject) => {
+ callee.ontrack = async e => {
+ const receiverStreams = e.receiver.createEncodedStreams();
+ const receiverReader = receiverStreams.readable.getReader();
+ const receiverWriter = receiverStreams.writable.getWriter();
+
+ // This should all be able to happen in 5 seconds.
+ // For fast failures, uncomment this line.
+ // t.step_timeout(reject, 5000);
+ for (let i = 0; i < numFramesToSend; i++) {
+ const result = await receiverReader.read();
+ // Write upstream, purely to avoid "no frame received" error messages
+ receiverWriter.write(result.value);
+ }
+ resolve();
+ }
+ });
+
+ await exchangeOfferAnswer(caller, callee);
+
+ for (let i = 0; i < numFramesToSend; i++) {
+ const result = await senderReader.read();
+ senderWriter.write(result.value.clone());
+ }
+ return framesReceivedCorrectly;
+}, "Cloning before sending works");
+</script>
diff --git a/testing/web-platform/tests/webrtc-encoded-transform/RTCEncodedVideoFrame-serviceworker-failure.https.html b/testing/web-platform/tests/webrtc-encoded-transform/RTCEncodedVideoFrame-serviceworker-failure.https.html
new file mode 100644
index 0000000000..b95c673f41
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-encoded-transform/RTCEncodedVideoFrame-serviceworker-failure.https.html
@@ -0,0 +1,60 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<!-- Based on similar tests in html/infrastructure/safe-passing-of-structured-data/shared-array-buffers/ -->
+<title>RTCEncodedVideoFrame cannot cross agent clusters, service worker edition</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src=/resources/testdriver.js></script>
+<script src=/resources/testdriver-vendor.js></script>
+<script src='../mediacapture-streams/permission-helper.js'></script>
+<script src="../webrtc/RTCPeerConnection-helper.js"></script>
+<script src="../service-workers/service-worker/resources/test-helpers.sub.js"></script>
+
+<script>
+"use strict";
+promise_test(async t => {
+ const caller = new RTCPeerConnection({encodedInsertableStreams:true});
+ t.add_cleanup(() => caller.close());
+ const callee = new RTCPeerConnection();
+ t.add_cleanup(() => callee.close());
+
+ await setMediaPermission("granted", ["camera"]);
+ const stream = await navigator.mediaDevices.getUserMedia({video:true});
+ const videoTrack = stream.getVideoTracks()[0];
+ t.add_cleanup(() => videoTrack.stop());
+
+ const videoSender = caller.addTrack(videoTrack)
+ const senderStreams = videoSender.createEncodedStreams();
+ const senderReader = senderStreams.readable.getReader();
+ const senderWriter = senderStreams.writable.getWriter();
+
+ exchangeIceCandidates(caller, callee);
+ await exchangeOfferAnswer(caller, callee);
+
+ const result = await senderReader.read();
+ const scope = "resources/blank.html";
+ const reg = await service_worker_unregister_and_register(t, "resources/serviceworker-failure.js", scope)
+ t.add_cleanup(() => service_worker_unregister(t, scope));
+ await wait_for_state(t, reg.installing, "activated");
+ const iframe = await with_iframe(scope);
+ t.add_cleanup(() => iframe.remove());
+ const sw = iframe.contentWindow.navigator.serviceWorker;
+ let state = "start in window";
+ return new Promise(resolve => {
+ sw.onmessage = t.step_func(e => {
+ if (e.data === "start in worker") {
+ assert_equals(state, "start in window");
+ sw.controller.postMessage(result.value);
+ state = "we are expecting confirmation of an onmessageerror in the worker";
+ } else if (e.data === "onmessageerror was received in worker") {
+ assert_equals(state, "we are expecting confirmation of an onmessageerror in the worker");
+ resolve();
+ } else {
+ assert_unreached("Got an unexpected message from the service worker: " + e.data);
+ }
+ });
+
+ sw.controller.postMessage(state);
+ });
+});
+</script>
diff --git a/testing/web-platform/tests/webrtc-encoded-transform/RTCPeerConnection-insertable-streams-audio.https.html b/testing/web-platform/tests/webrtc-encoded-transform/RTCPeerConnection-insertable-streams-audio.https.html
new file mode 100644
index 0000000000..d4b6b72a32
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-encoded-transform/RTCPeerConnection-insertable-streams-audio.https.html
@@ -0,0 +1,212 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>RTCPeerConnection Insertable Streams Audio</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src=/resources/testdriver.js></script>
+<script src=/resources/testdriver-vendor.js></script>
+<script src='../mediacapture-streams/permission-helper.js'></script>
+<script src="../webrtc/RTCPeerConnection-helper.js"></script>
+<script src="./RTCPeerConnection-insertable-streams.js"></script>
+</head>
+<body>
+<script>
+async function testAudioFlow(t, negotiationFunction) {
+ const caller = new RTCPeerConnection({encodedInsertableStreams:true});
+ t.add_cleanup(() => caller.close());
+ const callee = new RTCPeerConnection({encodedInsertableStreams:true});
+ t.add_cleanup(() => callee.close());
+
+ await setMediaPermission("granted", ["microphone"]);
+ const stream = await navigator.mediaDevices.getUserMedia({audio:true});
+ const audioTrack = stream.getAudioTracks()[0];
+ t.add_cleanup(() => audioTrack.stop());
+
+ const audioSender = caller.addTrack(audioTrack)
+ const senderStreams = audioSender.createEncodedStreams();
+ const senderReader = senderStreams.readable.getReader();
+ const senderWriter = senderStreams.writable.getWriter();
+
+ const frameInfos = [];
+ const numFramesPassthrough = 5;
+ const numFramesReplaceData = 5;
+ const numFramesModifyData = 5;
+ const numFramesToSend = numFramesPassthrough + numFramesReplaceData + numFramesModifyData;
+
+ const ontrackPromise = new Promise(resolve => {
+ callee.ontrack = t.step_func(() => {
+ const audioReceiver = callee.getReceivers().find(r => r.track.kind === 'audio');
+ assert_not_equals(audioReceiver, undefined);
+
+ const receiverStreams =
+ audioReceiver.createEncodedStreams();
+ const receiverReader = receiverStreams.readable.getReader();
+ const receiverWriter = receiverStreams.writable.getWriter();
+
+ const maxFramesToReceive = numFramesToSend;
+ let numVerifiedFrames = 0;
+ for (let i = 0; i < maxFramesToReceive; i++) {
+ receiverReader.read().then(t.step_func(result => {
+ if (frameInfos[numVerifiedFrames] &&
+ areFrameInfosEqual(result.value, frameInfos[numVerifiedFrames])) {
+ numVerifiedFrames++;
+ } else {
+ // Receiving unexpected frames is an indication that
+ // frames are not passed correctly between sender and receiver.
+ assert_unreached("Incorrect frame received");
+ }
+ assert_not_equals(result.value.getMetadata().sequenceNumber, undefined);
+
+ if (numVerifiedFrames == numFramesToSend)
+ resolve();
+ }));
+ }
+ });
+ });
+
+ exchangeIceCandidates(caller, callee);
+ await negotiationFunction(caller, callee);
+
+ // Pass frames as they come from the encoder.
+ for (let i = 0; i < numFramesPassthrough; i++) {
+ const result = await senderReader.read()
+ frameInfos.push({
+ data: result.value.data,
+ timestamp: result.value.timestamp,
+ type: result.value.type,
+ metadata: result.value.getMetadata(),
+ getMetadata() { return this.metadata; }
+ });
+ senderWriter.write(result.value);
+ }
+
+ // Replace frame data with arbitrary buffers.
+ for (let i = 0; i < numFramesReplaceData; i++) {
+ const result = await senderReader.read()
+
+ const buffer = new ArrayBuffer(100);
+ const int8View = new Int8Array(buffer);
+ int8View.fill(i);
+
+ result.value.data = buffer;
+ frameInfos.push({
+ data: result.value.data,
+ timestamp: result.value.timestamp,
+ type: result.value.type,
+ metadata: result.value.getMetadata(),
+ getMetadata() { return this.metadata; }
+ });
+ senderWriter.write(result.value);
+ }
+
+ // Modify frame data.
+ for (let i = 0; i < numFramesReplaceData; i++) {
+ const result = await senderReader.read()
+ const int8View = new Int8Array(result.value.data);
+ int8View.fill(i);
+
+ frameInfos.push({
+ data: result.value.data,
+ timestamp: result.value.timestamp,
+ type: result.value.type,
+ metadata: result.value.getMetadata(),
+ getMetadata() { return this.metadata; }
+ });
+ senderWriter.write(result.value);
+ }
+
+ return ontrackPromise;
+}
+
+promise_test(async t => {
+ return testAudioFlow(t, exchangeOfferAnswer);
+}, 'Frames flow correctly using insertable streams');
+
+promise_test(async t => {
+ return testAudioFlow(t, exchangeOfferAnswerReverse);
+}, 'Frames flow correctly using insertable streams when receiver starts negotiation');
+
+promise_test(async t => {
+ const caller = new RTCPeerConnection();
+ t.add_cleanup(() => caller.close());
+ const callee = new RTCPeerConnection();
+ t.add_cleanup(() => callee.close());
+ const stream = await navigator.mediaDevices.getUserMedia({audio:true});
+ const audioTrack = stream.getAudioTracks()[0];
+ t.add_cleanup(() => audioTrack.stop());
+
+ exchangeIceCandidates(caller, callee);
+ await exchangeOfferAnswer(caller, callee);
+
+ const audioSender = caller.addTrack(audioTrack);
+ assert_throws_dom("InvalidStateError", () => audioSender.createEncodedStreams());
+}, 'RTCRtpSender.createEncodedStream() throws if not requested in PC configuration');
+
+promise_test(async t => {
+ const caller = new RTCPeerConnection();
+ t.add_cleanup(() => caller.close());
+ const callee = new RTCPeerConnection();
+ t.add_cleanup(() => callee.close());
+ const stream = await navigator.mediaDevices.getUserMedia({audio:true});
+ const audioTrack = stream.getAudioTracks()[0];
+ t.add_cleanup(() => audioTrack.stop());
+
+ const audioSender = caller.addTrack(audioTrack);
+ const ontrackPromise = new Promise(resolve => {
+ callee.ontrack = t.step_func(() => {
+ const audioReceiver = callee.getReceivers().find(r => r.track.kind === 'audio');
+ assert_not_equals(audioReceiver, undefined);
+ assert_throws_dom("InvalidStateError", () => audioReceiver.createEncodedStreams());
+ resolve();
+ });
+ });
+
+ exchangeIceCandidates(caller, callee);
+ await exchangeOfferAnswer(caller, callee);
+ return ontrackPromise;
+}, 'RTCRtpReceiver.createEncodedStream() throws if not requested in PC configuration');
+
+promise_test(async t => {
+ const caller = new RTCPeerConnection({encodedInsertableStreams:true});
+ t.add_cleanup(() => caller.close());
+ const callee = new RTCPeerConnection();
+ t.add_cleanup(() => callee.close());
+
+ const stream = await navigator.mediaDevices.getUserMedia({audio:true});
+ const track = stream.getTracks()[0];
+ t.add_cleanup(() => track.stop());
+
+ const sender = caller.addTrack(track)
+ const streams = sender.createEncodedStreams();
+ const transformer = new TransformStream({
+ transform(frame, controller) {
+ // Inserting the same frame twice will result in failure since the frame
+ // will be neutered after the first insertion is processed.
+ controller.enqueue(frame);
+ controller.enqueue(frame);
+ }
+ });
+
+ exchangeIceCandidates(caller, callee);
+ await exchangeOfferAnswer(caller, callee);
+
+ await promise_rejects_dom(
+ t, 'OperationError',
+ streams.readable.pipeThrough(transformer).pipeTo(streams.writable));
+}, 'Enqueuing the same frame twice fails');
+
+promise_test(async t => {
+ const caller = new RTCPeerConnection({encodedInsertableStreams:true});
+ t.add_cleanup(() => caller.close());
+ const stream = await navigator.mediaDevices.getUserMedia({audio:true});
+ const track = stream.getTracks()[0];
+ t.add_cleanup(() => track.stop());
+ const sender = caller.addTrack(track)
+ sender.createEncodedStreams();
+ assert_throws_dom("InvalidStateError", () => sender.createEncodedStreams());
+}, 'Creating streams twice throws');
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/webrtc-encoded-transform/RTCPeerConnection-insertable-streams-errors.https.html b/testing/web-platform/tests/webrtc-encoded-transform/RTCPeerConnection-insertable-streams-errors.https.html
new file mode 100644
index 0000000000..56ba3ee972
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-encoded-transform/RTCPeerConnection-insertable-streams-errors.https.html
@@ -0,0 +1,87 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>RTCPeerConnection Insertable Streams - Errors</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src=/resources/testdriver.js></script>
+<script src=/resources/testdriver-vendor.js></script>
+<script src='../mediacapture-streams/permission-helper.js'></script>
+<script src="../webrtc/RTCPeerConnection-helper.js"></script>
+<script src="./RTCPeerConnection-insertable-streams.js"></script>
+</head>
+<body>
+<script>
+promise_test(async t => {
+ const caller = new RTCPeerConnection();
+ t.add_cleanup(() => caller.close());
+ const callee = new RTCPeerConnection();
+ t.add_cleanup(() => callee.close());
+ await setMediaPermission("granted", ["camera"]);
+ const stream = await navigator.mediaDevices.getUserMedia({video:true});
+ const videoTrack = stream.getVideoTracks()[0];
+ t.add_cleanup(() => videoTrack.stop());
+
+ exchangeIceCandidates(caller, callee);
+ await exchangeOfferAnswer(caller, callee);
+
+ const videoSender = caller.addTrack(videoTrack);
+ assert_throws_dom("InvalidStateError", () => videoSender.createEncodedStreams());
+}, 'RTCRtpSender.createEncodedStream() throws if not requested in PC configuration');
+
+promise_test(async t => {
+ const caller = new RTCPeerConnection();
+ t.add_cleanup(() => caller.close());
+ const callee = new RTCPeerConnection();
+ t.add_cleanup(() => callee.close());
+ const stream = await navigator.mediaDevices.getUserMedia({video:true});
+ const videoTrack = stream.getVideoTracks()[0];
+ t.add_cleanup(() => videoTrack.stop());
+
+ const videoSender = caller.addTrack(videoTrack);
+ const ontrackPromise = new Promise(resolve => {
+ callee.ontrack = t.step_func(() => {
+ const videoReceiver = callee.getReceivers().find(r => r.track.kind === 'video');
+ assert_not_equals(videoReceiver, undefined);
+ assert_throws_dom("InvalidStateError", () => videoReceiver.createEncodedStreams());
+ resolve();
+ });
+ });
+
+ exchangeIceCandidates(caller, callee);
+ await exchangeOfferAnswer(caller, callee);
+ return ontrackPromise;
+}, 'RTCRtpReceiver.createEncodedStream() throws if not requested in PC configuration');
+
+promise_test(async t => {
+ const caller = new RTCPeerConnection({encodedInsertableStreams:true});
+ t.add_cleanup(() => caller.close());
+ const callee = new RTCPeerConnection();
+ t.add_cleanup(() => callee.close());
+
+ const stream = await navigator.mediaDevices.getUserMedia({video:true});
+ const track = stream.getTracks()[0];
+ t.add_cleanup(() => track.stop());
+
+ const sender = caller.addTrack(track)
+ const streams = sender.createEncodedStreams();
+ const transformer = new TransformStream({
+ transform(frame, controller) {
+ // Inserting the same frame twice will result in failure since the frame
+ // will be neutered after the first insertion is processed.
+ controller.enqueue(frame);
+ controller.enqueue(frame);
+ }
+ });
+
+ exchangeIceCandidates(caller, callee);
+ await exchangeOfferAnswer(caller, callee);
+
+ await promise_rejects_dom(
+ t, 'OperationError',
+ streams.readable.pipeThrough(transformer).pipeTo(streams.writable));
+}, 'Enqueuing the same frame twice fails');
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/webrtc-encoded-transform/RTCPeerConnection-insertable-streams-simulcast.https.html b/testing/web-platform/tests/webrtc-encoded-transform/RTCPeerConnection-insertable-streams-simulcast.https.html
new file mode 100644
index 0000000000..cb33e458d1
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-encoded-transform/RTCPeerConnection-insertable-streams-simulcast.https.html
@@ -0,0 +1,89 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerConnection Insertable Streams Simulcast</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src=/resources/testdriver.js></script>
+<script src=/resources/testdriver-vendor.js></script>
+<script src='../mediacapture-streams/permission-helper.js'></script>
+<script src="../webrtc/RTCPeerConnection-helper.js"></script>
+<script src="../webrtc/third_party/sdp/sdp.js"></script>
+<script src="../webrtc/simulcast/simulcast.js"></script>
+<script>
+// Test based on wpt/webrtc/simulcast/basic.https.html
+promise_test(async t => {
+ const rids = [0, 1, 2];
+ const pc1 = new RTCPeerConnection({encodedInsertableStreams:true});
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection({encodedInsertableStreams:true});
+ t.add_cleanup(() => pc2.close());
+
+ exchangeIceCandidates(pc1, pc2);
+
+ const metadataToBeLoaded = [];
+ let receiverSSRCs = []
+ pc2.ontrack = t.step_func(e => {
+ const receiverTransformer = new TransformStream({
+ async transform(encodedFrame, controller) {
+ let ssrc = encodedFrame.getMetadata().synchronizationSource;
+ if (receiverSSRCs.indexOf(ssrc) == -1)
+ receiverSSRCs.push(ssrc);
+ controller.enqueue(encodedFrame);
+ }
+ });
+ const receiverStreams = e.receiver.createEncodedStreams();
+ receiverStreams.readable
+ .pipeThrough(receiverTransformer)
+ .pipeTo(receiverStreams.writable);
+
+ const stream = e.streams[0];
+ const v = document.createElement('video');
+ v.autoplay = true;
+ v.srcObject = stream;
+ v.id = stream.id
+ metadataToBeLoaded.push(new Promise((resolve) => {
+ v.addEventListener('loadedmetadata', () => {
+ resolve();
+ });
+ }));
+ });
+
+ await setMediaPermission("granted", ["camera"]);
+ const stream = await navigator.mediaDevices.getUserMedia({video: {width: 1280, height: 720}});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const transceiver = pc1.addTransceiver(stream.getVideoTracks()[0], {
+ streams: [stream],
+ sendEncodings: rids.map(rid => {rid}),
+ });
+ const senderStreams = transceiver.sender.createEncodedStreams();
+ let senderSSRCs = [];
+ const senderTransformer = new TransformStream({
+ async transform(encodedFrame, controller) {
+ if (senderSSRCs.indexOf(encodedFrame.getMetadata().synchronizationSource) == -1)
+ senderSSRCs.push(encodedFrame.getMetadata().synchronizationSource);
+ controller.enqueue(encodedFrame);
+ }
+ });
+ senderStreams.readable
+ .pipeThrough(senderTransformer)
+ .pipeTo(senderStreams.writable);
+
+ const offer = await pc1.createOffer();
+ await pc1.setLocalDescription(offer),
+ await pc2.setRemoteDescription({
+ type: 'offer',
+ sdp: swapRidAndMidExtensionsInSimulcastOffer(offer, rids),
+ });
+ const answer = await pc2.createAnswer();
+ await pc2.setLocalDescription(answer);
+ await pc1.setRemoteDescription({
+ type: 'answer',
+ sdp: swapRidAndMidExtensionsInSimulcastAnswer(answer, pc1.localDescription, rids),
+ });
+ assert_equals(metadataToBeLoaded.length, 3);
+ await Promise.all(metadataToBeLoaded);
+ // Ensure that frames from the 3 simulcast layers are exposed.
+ assert_equals(senderSSRCs.length, 3);
+ assert_equals(receiverSSRCs.length, 3);
+}, 'Basic simulcast setup with three spatial layers');
+</script>
diff --git a/testing/web-platform/tests/webrtc-encoded-transform/RTCPeerConnection-insertable-streams-video-frames.https.html b/testing/web-platform/tests/webrtc-encoded-transform/RTCPeerConnection-insertable-streams-video-frames.https.html
new file mode 100644
index 0000000000..d7fb088846
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-encoded-transform/RTCPeerConnection-insertable-streams-video-frames.https.html
@@ -0,0 +1,80 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>RTCPeerConnection Insertable Streams - Video Frames</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src=/resources/testdriver.js></script>
+<script src=/resources/testdriver-vendor.js></script>
+<script src='../mediacapture-streams/permission-helper.js'></script>
+<script src="../webrtc/RTCPeerConnection-helper.js"></script>
+<script src="./RTCPeerConnection-insertable-streams.js"></script>
+</head>
+<body>
+<script>
+promise_test(async t => {
+ const caller = new RTCPeerConnection({encodedInsertableStreams:true});
+ t.add_cleanup(() => caller.close());
+ const callee = new RTCPeerConnection({encodedInsertableStreams:true});
+ t.add_cleanup(() => callee.close());
+
+ await setMediaPermission("granted", ["camera"]);
+ const stream = await navigator.mediaDevices.getUserMedia({video:true});
+ const track = stream.getTracks()[0];
+ t.add_cleanup(() => track.stop());
+
+ const sender = caller.addTrack(track)
+ const senderStreams = sender.createEncodedStreams();
+ const senderReader = senderStreams.readable.getReader();
+ const senderWriter = senderStreams.writable.getWriter();
+ const numFramesToSend = 20;
+
+ const ontrackPromise = new Promise((resolve, reject) => {
+ callee.ontrack = async e => {
+ const receiverStreams = e.receiver.createEncodedStreams();
+ const receiverReader = receiverStreams.readable.getReader();
+
+ let numReceivedKeyFrames = 0;
+ let numReceivedDeltaFrames = 0;
+ for (let i = 0; i < numFramesToSend; i++) {
+ const result = await receiverReader.read();
+ if (result.value.type == 'key')
+ numReceivedKeyFrames++;
+ else if (result.value.type == 'delta')
+ numReceivedDeltaFrames++;
+
+ if (numReceivedKeyFrames > 0 && numReceivedDeltaFrames > 0)
+ resolve();
+ else if (numReceivedKeyFrames + numReceivedDeltaFrames >= numFramesToSend)
+ reject();
+ }
+ }
+ });
+
+ exchangeIceCandidates(caller, callee);
+ await exchangeOfferAnswer(caller, callee);
+
+ let numSentKeyFrames = 0;
+ let numSentDeltaFrames = 0;
+ // Pass frames as they come from the encoder.
+ for (let i = 0; i < numFramesToSend; i++) {
+ const result = await senderReader.read();
+ verifyNonstandardAdditionalDataIfPresent(result.value);
+ if (result.value.type == 'key') {
+ numSentKeyFrames++;
+ } else {
+ numSentDeltaFrames++;
+ }
+
+ senderWriter.write(result.value);
+ }
+
+ assert_greater_than(numSentKeyFrames, 0);
+ assert_greater_than(numSentDeltaFrames, 0);
+
+ return ontrackPromise;
+}, 'Key and Delta frames are sent and received');
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/webrtc-encoded-transform/RTCPeerConnection-insertable-streams-video.https.html b/testing/web-platform/tests/webrtc-encoded-transform/RTCPeerConnection-insertable-streams-video.https.html
new file mode 100644
index 0000000000..378520c693
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-encoded-transform/RTCPeerConnection-insertable-streams-video.https.html
@@ -0,0 +1,149 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>RTCPeerConnection Insertable Streams - Video</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src=/resources/testdriver.js></script>
+<script src=/resources/testdriver-vendor.js></script>
+<script src='../mediacapture-streams/permission-helper.js'></script>
+<script src="../webrtc/RTCPeerConnection-helper.js"></script>
+<script src="./RTCPeerConnection-insertable-streams.js"></script>
+</head>
+<body>
+<script>
+async function testVideoFlow(t, negotiationFunction) {
+ const caller = new RTCPeerConnection({encodedInsertableStreams:true});
+ t.add_cleanup(() => caller.close());
+ const callee = new RTCPeerConnection({encodedInsertableStreams:true});
+ t.add_cleanup(() => callee.close());
+
+ await setMediaPermission("granted", ["camera"]);
+ const stream = await navigator.mediaDevices.getUserMedia({video:true});
+ const videoTrack = stream.getVideoTracks()[0];
+ t.add_cleanup(() => videoTrack.stop());
+
+ const videoSender = caller.addTrack(videoTrack)
+ const senderStreams = videoSender.createEncodedStreams();
+ const senderReader = senderStreams.readable.getReader();
+ const senderWriter = senderStreams.writable.getWriter();
+
+ const frameInfos = [];
+ const numFramesPassthrough = 5;
+ const numFramesReplaceData = 5;
+ const numFramesModifyData = 5;
+ const numFramesToSend = numFramesPassthrough + numFramesReplaceData + numFramesModifyData;
+
+ const ontrackPromise = new Promise(resolve => {
+ callee.ontrack = t.step_func(() => {
+ const videoReceiver = callee.getReceivers().find(r => r.track.kind === 'video');
+ assert_not_equals(videoReceiver, undefined);
+
+ const receiverStreams =
+ videoReceiver.createEncodedStreams();
+ const receiverReader = receiverStreams.readable.getReader();
+ const receiverWriter = receiverStreams.writable.getWriter();
+
+ const maxFramesToReceive = numFramesToSend;
+ let numVerifiedFrames = 0;
+ for (let i = 0; i < maxFramesToReceive; i++) {
+ receiverReader.read().then(t.step_func(result => {
+ verifyNonstandardAdditionalDataIfPresent(result.value);
+ if (frameInfos[numVerifiedFrames] &&
+ areFrameInfosEqual(result.value, frameInfos[numVerifiedFrames])) {
+ numVerifiedFrames++;
+ } else {
+ // Receiving unexpected frames is an indication that
+ // frames are not passed correctly between sender and receiver.
+ assert_unreached("Incorrect frame received");
+ }
+
+ if (numVerifiedFrames == numFramesToSend)
+ resolve();
+ }));
+ }
+ });
+ });
+
+ exchangeIceCandidates(caller, callee);
+ await negotiationFunction(caller, callee);
+
+ // Pass frames as they come from the encoder.
+ for (let i = 0; i < numFramesPassthrough; i++) {
+ const result = await senderReader.read();
+ const metadata = result.value.getMetadata();
+ assert_true(containsVideoMetadata(metadata));
+ verifyNonstandardAdditionalDataIfPresent(result.value);
+ frameInfos.push({
+ timestamp: result.value.timestamp,
+ type: result.value.type,
+ data: result.value.data,
+ metadata: metadata,
+ getMetadata() { return this.metadata; }
+ });
+ senderWriter.write(result.value);
+ }
+
+ // Replace frame data with arbitrary buffers.
+ for (let i = 0; i < numFramesReplaceData; i++) {
+ const result = await senderReader.read();
+ const metadata = result.value.getMetadata();
+ assert_true(containsVideoMetadata(metadata));
+ const buffer = new ArrayBuffer(100);
+ const int8View = new Int8Array(buffer);
+ int8View.fill(i);
+
+ result.value.data = buffer;
+ frameInfos.push({
+ timestamp: result.value.timestamp,
+ type: result.value.type,
+ data: result.value.data,
+ metadata: metadata,
+ getMetadata() { return this.metadata; }
+ });
+ senderWriter.write(result.value);
+ }
+
+ // Modify frame data.
+ for (let i = 0; i < numFramesReplaceData; i++) {
+ const result = await senderReader.read();
+ const metadata = result.value.getMetadata();
+ assert_true(containsVideoMetadata(metadata));
+ const int8View = new Int8Array(result.value.data);
+ int8View.fill(i);
+
+ frameInfos.push({
+ timestamp: result.value.timestamp,
+ type: result.value.type,
+ data: result.value.data,
+ metadata: metadata,
+ getMetadata() { return this.metadata; }
+ });
+ senderWriter.write(result.value);
+ }
+
+ return ontrackPromise;
+}
+
+promise_test(async t => {
+ return testVideoFlow(t, exchangeOfferAnswer);
+}, 'Frames flow correctly using insertable streams');
+
+promise_test(async t => {
+ return testVideoFlow(t, exchangeOfferAnswerReverse);
+}, 'Frames flow correctly using insertable streams when receiver starts negotiation');
+
+promise_test(async t => {
+ const caller = new RTCPeerConnection({encodedInsertableStreams:true});
+ t.add_cleanup(() => caller.close());
+ const stream = await navigator.mediaDevices.getUserMedia({video:true});
+ const track = stream.getTracks()[0];
+ t.add_cleanup(() => track.stop());
+ const sender = caller.addTrack(track)
+ sender.createEncodedStreams();
+ assert_throws_dom("InvalidStateError", () => sender.createEncodedStreams());
+}, 'Creating streams twice throws');
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/webrtc-encoded-transform/RTCPeerConnection-insertable-streams-worker.https.html b/testing/web-platform/tests/webrtc-encoded-transform/RTCPeerConnection-insertable-streams-worker.https.html
new file mode 100644
index 0000000000..cb31057cac
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-encoded-transform/RTCPeerConnection-insertable-streams-worker.https.html
@@ -0,0 +1,196 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>RTCPeerConnection Insertable Streams - Worker</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src=/resources/testdriver.js></script>
+<script src=/resources/testdriver-vendor.js></script>
+<script src='../mediacapture-streams/permission-helper.js'></script>
+<script src="../webrtc/RTCPeerConnection-helper.js"></script>
+<script src="./RTCPeerConnection-insertable-streams.js"></script>
+</head>
+<body>
+<script>
+promise_test(async t => {
+ const caller = new RTCPeerConnection({encodedInsertableStreams:true});
+ t.add_cleanup(() => caller.close());
+ const callee = new RTCPeerConnection();
+ t.add_cleanup(() => callee.close());
+
+ // Video is used in a later test, so we ask for both permissions
+ await setMediaPermission();
+ const stream = await navigator.mediaDevices.getUserMedia({audio:true});
+ const track = stream.getTracks()[0];
+ t.add_cleanup(() => track.stop());
+
+ const sender = caller.addTrack(track)
+ const senderStreams = sender.createEncodedStreams();
+
+ const senderWorker = new Worker('RTCPeerConnection-sender-worker-single-frame.js')
+ t.add_cleanup(() => senderWorker.terminate());
+ senderWorker.postMessage(
+ {readableStream: senderStreams.readable},
+ [senderStreams.readable]);
+
+ let expectedFrameData = null;
+ let verifiedFrameData = false;
+ let numVerifiedFrames = 0;
+ const onmessagePromise = new Promise(resolve => {
+ senderWorker.onmessage = t.step_func(message => {
+ if (!(message.data instanceof RTCEncodedAudioFrame)) {
+ // This is the first message sent from the Worker to the test.
+ // It contains an object (not an RTCEncodedAudioFrame) with the same
+ // fields as the RTCEncodedAudioFrame to be sent in follow-up messages.
+ // These serve as expected values to validate that the
+ // RTCEncodedAudioFrame is sent correctly back to the test in the next
+ // message.
+ expectedFrameData = message.data;
+ } else {
+ // This is the frame sent by the Worker after reading it from the
+ // readable stream. The Worker sends it twice after sending the
+ // verification message.
+ assert_equals(message.data.type, expectedFrameData.type);
+ assert_equals(message.data.timestamp, expectedFrameData.timestamp);
+ assert_true(areArrayBuffersEqual(message.data.data, expectedFrameData.data));
+ if (++numVerifiedFrames == 2)
+ resolve();
+ }
+ });
+ });
+
+ exchangeIceCandidates(caller, callee);
+ await exchangeOfferAnswer(caller, callee);
+
+ return onmessagePromise;
+}, 'RTCRtpSender readable stream transferred to a Worker and the Worker sends an RTCEncodedAudioFrame back');
+
+promise_test(async t => {
+ const caller = new RTCPeerConnection({encodedInsertableStreams:true});
+ t.add_cleanup(() => caller.close());
+ const callee = new RTCPeerConnection();
+ t.add_cleanup(() => callee.close());
+
+ const stream = await navigator.mediaDevices.getUserMedia({video:true});
+ const videoTrack = stream.getVideoTracks()[0];
+ t.add_cleanup(() => videoTrack.stop());
+
+ const videoSender = caller.addTrack(videoTrack)
+ const senderStreams = videoSender.createEncodedStreams();
+
+ const senderWorker = new Worker('RTCPeerConnection-sender-worker-single-frame.js')
+ t.add_cleanup(() => senderWorker.terminate());
+ senderWorker.postMessage(
+ {readableStream: senderStreams.readable},
+ [senderStreams.readable]);
+
+ let expectedFrameData = null;
+ let verifiedFrameData = false;
+ let numVerifiedFrames = 0;
+ const onmessagePromise = new Promise(resolve => {
+ senderWorker.onmessage = t.step_func(message => {
+ if (!(message.data instanceof RTCEncodedVideoFrame)) {
+ // This is the first message sent from the Worker to the test.
+ // It contains an object (not an RTCEncodedVideoFrame) with the same
+ // fields as the RTCEncodedVideoFrame to be sent in follow-up messages.
+ // These serve as expected values to validate that the
+ // RTCEncodedVideoFrame is sent correctly back to the test in the next
+ // message.
+ expectedFrameData = message.data;
+ } else {
+ // This is the frame sent by the Worker after reading it from the
+ // readable stream. The Worker sends it twice after sending the
+ // verification message.
+ assert_equals(message.data.type, expectedFrameData.type);
+ assert_equals(message.data.timestamp, expectedFrameData.timestamp);
+ assert_true(areArrayBuffersEqual(message.data.data, expectedFrameData.data));
+ assert_equals(message.data.getMetadata().synchronizationSource, expectedFrameData.metadata.synchronizationSource);
+ if (++numVerifiedFrames == 2)
+ resolve();
+ }
+ });
+ });
+
+ exchangeIceCandidates(caller, callee);
+ await exchangeOfferAnswer(caller, callee);
+
+ return onmessagePromise;
+}, 'RTCRtpSender readable stream transferred to a Worker and the Worker sends an RTCEncodedVideoFrame back');
+
+promise_test(async t => {
+ const caller = new RTCPeerConnection({encodedInsertableStreams:true});
+ t.add_cleanup(() => caller.close());
+ const callee = new RTCPeerConnection();
+ t.add_cleanup(() => callee.close());
+
+ const stream = await navigator.mediaDevices.getUserMedia({video:true});
+ const videoTrack = stream.getVideoTracks()[0];
+ t.add_cleanup(() => videoTrack.stop());
+
+ const videoSender = caller.addTrack(videoTrack)
+ const senderStreams = videoSender.createEncodedStreams();
+
+ const senderWorker = new Worker('RTCPeerConnection-worker-transform.js')
+ t.add_cleanup(() => senderWorker.terminate());
+ senderWorker.postMessage(
+ {
+ readableStream: senderStreams.readable,
+ writableStream: senderStreams.writable,
+ insertError: true
+ },
+ [senderStreams.readable, senderStreams.writable]);
+
+ const onmessagePromise = new Promise(resolve => {
+ senderWorker.onmessage = t.step_func(message => {
+ assert_false(message.data.success);
+ assert_true(message.data.error instanceof TypeError);
+ resolve();
+ });
+ });
+
+ exchangeIceCandidates(caller, callee);
+ await exchangeOfferAnswer(caller, callee);
+
+ return onmessagePromise;
+}, 'Video RTCRtpSender insertable streams transferred to a worker, which tries to write an invalid frame');
+
+promise_test(async t => {
+ const caller = new RTCPeerConnection({encodedInsertableStreams:true});
+ t.add_cleanup(() => caller.close());
+ const callee = new RTCPeerConnection();
+ t.add_cleanup(() => callee.close());
+
+ const stream = await navigator.mediaDevices.getUserMedia({audio:true});
+ const audioTrack = stream.getAudioTracks()[0];
+ t.add_cleanup(() => audioTrack.stop());
+
+ const audioSender = caller.addTrack(audioTrack)
+ const senderStreams = audioSender.createEncodedStreams();
+
+ const senderWorker = new Worker('RTCPeerConnection-worker-transform.js')
+ t.add_cleanup(() => senderWorker.terminate());
+ senderWorker.postMessage(
+ {
+ readableStream: senderStreams.readable,
+ writableStream: senderStreams.writable,
+ insertError: true
+ },
+ [senderStreams.readable, senderStreams.writable]);
+
+ const onmessagePromise = new Promise(resolve => {
+ senderWorker.onmessage = t.step_func(message => {
+ assert_false(message.data.success);
+ assert_true(message.data.error instanceof TypeError);
+ resolve();
+ });
+ });
+
+ exchangeIceCandidates(caller, callee);
+ await exchangeOfferAnswer(caller, callee);
+
+ return onmessagePromise;
+}, 'Audio RTCRtpSender insertable streams transferred to a worker, which tries to write an invalid frame');
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/webrtc-encoded-transform/RTCPeerConnection-insertable-streams.js b/testing/web-platform/tests/webrtc-encoded-transform/RTCPeerConnection-insertable-streams.js
new file mode 100644
index 0000000000..f1b872294b
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-encoded-transform/RTCPeerConnection-insertable-streams.js
@@ -0,0 +1,242 @@
+function areArrayBuffersEqual(buffer1, buffer2)
+{
+ if (buffer1.byteLength !== buffer2.byteLength) {
+ return false;
+ }
+ let array1 = new Int8Array(buffer1);
+ var array2 = new Int8Array(buffer2);
+ for (let i = 0 ; i < buffer1.byteLength ; ++i) {
+ if (array1[i] !== array2[i]) {
+ return false;
+ }
+ }
+ return true;
+}
+
+function areArraysEqual(a1, a2) {
+ if (a1 === a1)
+ return true;
+ if (a1.length != a2.length)
+ return false;
+ for (let i = 0; i < a1.length; i++) {
+ if (a1[i] != a2[i])
+ return false;
+ }
+ return true;
+}
+
+function areMetadataEqual(metadata1, metadata2, type) {
+ return metadata1.synchronizationSource === metadata2.synchronizationSource &&
+ metadata1.payloadType == metadata2.payloadType &&
+ areArraysEqual(metadata1.contributingSources, metadata2.contributingSources) &&
+ metadata1.frameId === metadata2.frameId &&
+ areArraysEqual(metadata1.dependencies, metadata2.dependencies) &&
+ metadata1.spatialIndex === metadata2.spatialIndex &&
+ metadata1.temporalIndex === metadata2.temporalIndex &&
+ // Width and height are reported only for key frames on the receiver side.
+ type == "key"
+ ? metadata1.width === metadata2.width && metadata1.height === metadata2.height
+ : true;
+}
+
+function areFrameInfosEqual(frame1, frame2) {
+ return frame1.timestamp === frame2.timestamp &&
+ frame1.type === frame2.type &&
+ areMetadataEqual(frame1.getMetadata(), frame2.getMetadata(), frame1.type) &&
+ areArrayBuffersEqual(frame1.data, frame2.data);
+}
+
+function containsVideoMetadata(metadata) {
+ return metadata.synchronizationSource !== undefined &&
+ metadata.width !== undefined &&
+ metadata.height !== undefined &&
+ metadata.spatialIndex !== undefined &&
+ metadata.temporalIndex !== undefined &&
+ metadata.dependencies !== undefined;
+}
+
+function enableGFD(sdp) {
+ const GFD_V00_EXTENSION =
+ 'http://www.webrtc.org/experiments/rtp-hdrext/generic-frame-descriptor-00';
+ if (sdp.indexOf(GFD_V00_EXTENSION) !== -1)
+ return sdp;
+
+ const extensionIds = sdp.trim().split('\n')
+ .map(line => line.trim())
+ .filter(line => line.startsWith('a=extmap:'))
+ .map(line => line.split(' ')[0].substr(9))
+ .map(id => parseInt(id, 10))
+ .sort((a, b) => a - b);
+ for (let newId = 1; newId <= 14; newId++) {
+ if (!extensionIds.includes(newId)) {
+ return sdp += 'a=extmap:' + newId + ' ' + GFD_V00_EXTENSION + '\r\n';
+ }
+ }
+ if (sdp.indexOf('a=extmap-allow-mixed') !== -1) { // Pick the next highest one.
+ const newId = extensionIds[extensionIds.length - 1] + 1;
+ return sdp += 'a=extmap:' + newId + ' ' + GFD_V00_EXTENSION + '\r\n';
+ }
+ throw 'Could not find free extension id to use for ' + GFD_V00_EXTENSION;
+}
+
+async function exchangeOfferAnswer(pc1, pc2) {
+ const offer = await pc1.createOffer();
+ // Munge the SDP to enable the GFD extension in order to get correct metadata.
+ const sdpGFD = enableGFD(offer.sdp);
+ await pc1.setLocalDescription({type: offer.type, sdp: sdpGFD});
+ // Munge the SDP to disable bandwidth probing via RTX.
+ // TODO(crbug.com/1066819): remove this hack when we do not receive duplicates from RTX
+ // anymore.
+ const sdpRTX = sdpGFD.replace(new RegExp('rtx', 'g'), 'invalid');
+ await pc2.setRemoteDescription({type: 'offer', sdp: sdpRTX});
+
+ const answer = await pc2.createAnswer();
+ await pc2.setLocalDescription(answer);
+ await pc1.setRemoteDescription(answer);
+}
+
+async function exchangeOfferAnswerReverse(pc1, pc2) {
+ const offer = await pc2.createOffer({offerToReceiveAudio: true, offerToReceiveVideo: true});
+ // Munge the SDP to enable the GFD extension in order to get correct metadata.
+ const sdpGFD = enableGFD(offer.sdp);
+ // Munge the SDP to disable bandwidth probing via RTX.
+ // TODO(crbug.com/1066819): remove this hack when we do not receive duplicates from RTX
+ // anymore.
+ const sdpRTX = sdpGFD.replace(new RegExp('rtx', 'g'), 'invalid');
+ await pc1.setRemoteDescription({type: 'offer', sdp: sdpRTX});
+ await pc2.setLocalDescription({type: 'offer', sdp: sdpGFD});
+
+ const answer = await pc1.createAnswer();
+ await pc2.setRemoteDescription(answer);
+ await pc1.setLocalDescription(answer);
+}
+
+function createFrameDescriptor(videoFrame) {
+ const kMaxSpatialLayers = 8;
+ const kMaxTemporalLayers = 8;
+ const kMaxNumFrameDependencies = 8;
+
+ const metadata = videoFrame.getMetadata();
+ let frameDescriptor = {
+ beginningOfSubFrame: true,
+ endOfSubframe: false,
+ frameId: metadata.frameId & 0xFFFF,
+ spatialLayers: 1 << metadata.spatialIndex,
+ temporalLayer: metadata.temporalLayer,
+ frameDependenciesDiffs: [],
+ width: 0,
+ height: 0
+ };
+
+ for (const dependency of metadata.dependencies) {
+ frameDescriptor.frameDependenciesDiffs.push(metadata.frameId - dependency);
+ }
+ if (metadata.dependencies.length == 0) {
+ frameDescriptor.width = metadata.width;
+ frameDescriptor.height = metadata.height;
+ }
+ return frameDescriptor;
+}
+
+function additionalDataSize(descriptor) {
+ if (!descriptor.beginningOfSubFrame) {
+ return 1;
+ }
+
+ let size = 4;
+ for (const fdiff of descriptor.frameDependenciesDiffs) {
+ size += (fdiff >= (1 << 6)) ? 2 : 1;
+ }
+ if (descriptor.beginningOfSubFrame &&
+ descriptor.frameDependenciesDiffs.length == 0 &&
+ descriptor.width > 0 &&
+ descriptor.height > 0) {
+ size += 4;
+ }
+
+ return size;
+}
+
+// Compute the buffer reported in the additionalData field using the metadata
+// provided by a video frame.
+// Based on the webrtc::RtpDescriptorAuthentication() C++ function at
+// https://source.chromium.org/chromium/chromium/src/+/main:third_party/webrtc/modules/rtp_rtcp/source/rtp_descriptor_authentication.cc
+function computeAdditionalData(videoFrame) {
+ const kMaxSpatialLayers = 8;
+ const kMaxTemporalLayers = 8;
+ const kMaxNumFrameDependencies = 8;
+
+ const metadata = videoFrame.getMetadata();
+ if (metadata.spatialIndex < 0 ||
+ metadata.temporalIndex < 0 ||
+ metadata.spatialIndex >= kMaxSpatialLayers ||
+ metadata.temporalIndex >= kMaxTemporalLayers ||
+ metadata.dependencies.length > kMaxNumFrameDependencies) {
+ return new ArrayBuffer(0);
+ }
+
+ const descriptor = createFrameDescriptor(videoFrame);
+ const size = additionalDataSize(descriptor);
+ const additionalData = new ArrayBuffer(size);
+ const data = new Uint8Array(additionalData);
+
+ const kFlagBeginOfSubframe = 0x80;
+ const kFlagEndOfSubframe = 0x40;
+ const kFlagFirstSubframeV00 = 0x20;
+ const kFlagLastSubframeV00 = 0x10;
+
+ const kFlagDependencies = 0x08;
+ const kFlagMoreDependencies = 0x01;
+ const kFlageXtendedOffset = 0x02;
+
+ let baseHeader =
+ (descriptor.beginningOfSubFrame ? kFlagBeginOfSubframe : 0) |
+ (descriptor.endOfSubFrame ? kFlagEndOfSubframe : 0);
+ baseHeader |= kFlagFirstSubframeV00;
+ baseHeader |= kFlagLastSubframeV00;
+
+ if (!descriptor.beginningOfSubFrame) {
+ data[0] = baseHeader;
+ return additionalData;
+ }
+
+ data[0] =
+ baseHeader |
+ (descriptor.frameDependenciesDiffs.length == 0 ? 0 : kFlagDependencies) |
+ descriptor.temporalLayer;
+ data[1] = descriptor.spatialLayers;
+ data[2] = descriptor.frameId & 0xFF;
+ data[3] = descriptor.frameId >> 8;
+
+ const fdiffs = descriptor.frameDependenciesDiffs;
+ let offset = 4;
+ if (descriptor.beginningOfSubFrame &&
+ fdiffs.length == 0 &&
+ descriptor.width > 0 &&
+ descriptor.height > 0) {
+ data[offset++] = (descriptor.width >> 8);
+ data[offset++] = (descriptor.width & 0xFF);
+ data[offset++] = (descriptor.height >> 8);
+ data[offset++] = (descriptor.height & 0xFF);
+ }
+ for (let i = 0; i < fdiffs.length; i++) {
+ const extended = fdiffs[i] >= (1 << 6);
+ const more = i < fdiffs.length - 1;
+ data[offset++] = ((fdiffs[i] & 0x3f) << 2) |
+ (extended ? kFlageXtendedOffset : 0) |
+ (more ? kFlagMoreDependencies : 0);
+ if (extended) {
+ data[offset++] = fdiffs[i] >> 6;
+ }
+ }
+ return additionalData;
+}
+
+function verifyNonstandardAdditionalDataIfPresent(videoFrame) {
+ if (videoFrame.additionalData === undefined)
+ return;
+
+ const computedData = computeAdditionalData(videoFrame);
+ assert_true(areArrayBuffersEqual(videoFrame.additionalData, computedData));
+}
+
diff --git a/testing/web-platform/tests/webrtc-encoded-transform/RTCPeerConnection-sender-worker-single-frame.js b/testing/web-platform/tests/webrtc-encoded-transform/RTCPeerConnection-sender-worker-single-frame.js
new file mode 100644
index 0000000000..c943dafe5b
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-encoded-transform/RTCPeerConnection-sender-worker-single-frame.js
@@ -0,0 +1,19 @@
+onmessage = async (event) => {
+ const readableStream = event.data.readableStream;
+ const reader = readableStream.getReader();
+ const result = await reader.read();
+
+ // Post an object with individual fields so that the test side has
+ // values to verify the serialization of the RTCEncodedVideoFrame.
+ postMessage({
+ type: result.value.type,
+ timestamp: result.value.timestamp,
+ data: result.value.data,
+ metadata: result.value.getMetadata(),
+ });
+
+ // Send the frame twice to verify that the frame does not change after the
+ // first serialization.
+ postMessage(result.value);
+ postMessage(result.value);
+}
diff --git a/testing/web-platform/tests/webrtc-encoded-transform/RTCPeerConnection-worker-transform.js b/testing/web-platform/tests/webrtc-encoded-transform/RTCPeerConnection-worker-transform.js
new file mode 100644
index 0000000000..36e3949e4d
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-encoded-transform/RTCPeerConnection-worker-transform.js
@@ -0,0 +1,22 @@
+onmessage = async (event) => {
+ const readableStream = event.data.readableStream;
+ const writableStream = event.data.writableStream;
+ const insertError = event.data.insertError;
+
+ try {
+ await readableStream.pipeThrough(new TransformStream({
+ transform: (chunk, controller) => {
+ if (insertError) {
+ controller.enqueue("This is not a valid frame");
+ } else {
+ controller.enqueue(chunk);
+ }
+ }
+ })).pipeTo(writableStream);
+
+ postMessage({success:true});
+ } catch(e) {
+ postMessage({success:false, error: e});
+ }
+
+}
diff --git a/testing/web-platform/tests/webrtc-encoded-transform/codec-specific-metadata.https.html b/testing/web-platform/tests/webrtc-encoded-transform/codec-specific-metadata.https.html
new file mode 100644
index 0000000000..bef61b39f3
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-encoded-transform/codec-specific-metadata.https.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src=/resources/testdriver.js></script>
+<script src=/resources/testdriver-vendor.js></script>
+<script src='../mediacapture-streams/permission-helper.js'></script>
+<script src="../webrtc/RTCPeerConnection-helper.js"></script>
+<script src="../service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<script src='./helper.js'></script>
+<script>
+"use strict";
+
+promise_test(async t => {
+ const senderReader = await setupLoopbackWithCodecAndGetReader(t, 'VP8');
+ const result = await senderReader.read();
+ const metadata = result.value.getMetadata();
+ // RTCEncodedVideoFrameAdditionalMetadata-only fields.
+ assert_true(Array.isArray(metadata.decodeTargetIndications),
+ 'decodeTargetIndication is an array');
+ assert_equals(typeof metadata.isLastFrameInPicture, 'boolean',
+ 'isLastFrameInPicture is a boolean');
+ assert_equals(typeof metadata.simulcastIdx, 'number',
+ 'simulcastIdx is a number');
+ assert_equals(metadata.codec, 'vp8');
+ assert_equals(typeof metadata.codecSpecifics, 'object',
+ 'codecSpecifics is an object');
+ // VP8-only
+ assert_equals(typeof metadata.codecSpecifics.nonReference, 'boolean',
+ 'codecSpecifics.nonReference is a boolean');
+ assert_equals(typeof metadata.codecSpecifics.pictureId, 'number',
+ 'codecSpecifics.pictureId is a number');
+ assert_equals(typeof metadata.codecSpecifics.tl0PicIdx, 'number',
+ 'codecSpecifics.tl0PicIdx is a number');
+ assert_equals(typeof metadata.codecSpecifics.temporalIdx, 'number',
+ 'codecSpecifics.temporalIdx is a number');
+ assert_equals(typeof metadata.codecSpecifics.layerSync, 'boolean',
+ 'codecSpecifics.layerSync is a boolean');
+ assert_equals(typeof metadata.codecSpecifics.keyIdx, 'number',
+ 'codecSpecifics.keyIdx is a number');
+ assert_equals(typeof metadata.codecSpecifics.partitionId, 'number',
+ 'codecSpecifics.partitionId is a number');
+ assert_equals(typeof metadata.codecSpecifics.beginningOfPartition, 'boolean',
+ 'codecSpecifics.beginningOfPartition is a boolean');
+}, "[VP8] getMetadata() supports the expected codec specifics");
+</script>
diff --git a/testing/web-platform/tests/webrtc-encoded-transform/helper.js b/testing/web-platform/tests/webrtc-encoded-transform/helper.js
new file mode 100644
index 0000000000..d4cec39ffc
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-encoded-transform/helper.js
@@ -0,0 +1,26 @@
+"use strict";
+
+async function setupLoopbackWithCodecAndGetReader(t, codec) {
+ const caller = new RTCPeerConnection({encodedInsertableStreams:true});
+ t.add_cleanup(() => caller.close());
+ const callee = new RTCPeerConnection();
+ t.add_cleanup(() => callee.close());
+
+ await setMediaPermission("granted", ["camera"]);
+ const stream = await navigator.mediaDevices.getUserMedia({video:true});
+ const videoTrack = stream.getVideoTracks()[0];
+ t.add_cleanup(() => videoTrack.stop());
+
+ const transceiver = caller.addTransceiver(videoTrack);
+ const codecCapability =
+ RTCRtpSender.getCapabilities('video').codecs.find(capability => {
+ return capability.mimeType.includes(codec);
+ });
+ assert_not_equals(codecCapability, undefined);
+ transceiver.setCodecPreferences([codecCapability]);
+
+ const senderStreams = transceiver.sender.createEncodedStreams();
+ exchangeIceCandidates(caller, callee);
+ await exchangeOfferAnswer(caller, callee);
+ return senderStreams.readable.getReader();
+}
diff --git a/testing/web-platform/tests/webrtc-encoded-transform/idlharness.https.window.js b/testing/web-platform/tests/webrtc-encoded-transform/idlharness.https.window.js
new file mode 100644
index 0000000000..2c6ef19ca8
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-encoded-transform/idlharness.https.window.js
@@ -0,0 +1,18 @@
+// META: script=/resources/WebIDLParser.js
+// META: script=/resources/idlharness.js
+// META: script=./RTCPeerConnection-helper.js
+
+'use strict';
+
+idl_test(
+ ['webrtc-encoded-transform'],
+ ['webrtc', 'streams', 'html', 'dom'],
+ async idlArray => {
+ idlArray.add_objects({
+ // TODO: RTCEncodedVideoFrame
+ // TODO: RTCEncodedAudioFrame
+ RTCRtpSender: [`new RTCPeerConnection().addTransceiver('audio').sender`],
+ RTCRtpReceiver: [`new RTCPeerConnection().addTransceiver('audio').receiver`],
+ });
+ }
+);
diff --git a/testing/web-platform/tests/webrtc-encoded-transform/resources/blank.html b/testing/web-platform/tests/webrtc-encoded-transform/resources/blank.html
new file mode 100644
index 0000000000..a3c3a4689a
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-encoded-transform/resources/blank.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<title>Empty doc</title>
diff --git a/testing/web-platform/tests/webrtc-encoded-transform/resources/serviceworker-failure.js b/testing/web-platform/tests/webrtc-encoded-transform/resources/serviceworker-failure.js
new file mode 100644
index 0000000000..e7aa8e11be
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-encoded-transform/resources/serviceworker-failure.js
@@ -0,0 +1,30 @@
+// Based on similar tests in html/infrastructure/safe-passing-of-structured-data/shared-array-buffers/.
+"use strict";
+self.importScripts("/resources/testharness.js");
+
+let state = "start in worker";
+
+self.onmessage = e => {
+ if (e.data === "start in window") {
+ assert_equals(state, "start in worker");
+ e.source.postMessage(state);
+ state = "we are expecting a messageerror due to the window sending us an RTCEncodedVideoFrame or RTCEncodedAudioFrame";
+ } else {
+ e.source.postMessage(`worker onmessage was reached when in state "${state}" and data ${e.data}`);
+ }
+};
+
+self.onmessageerror = e => {
+ if (state === "we are expecting a messageerror due to the window sending us an RTCEncodedVideoFrame or RTCEncodedAudioFrame") {
+ assert_equals(e.constructor.name, "ExtendableMessageEvent", "type");
+ assert_equals(e.data, null, "data");
+ assert_equals(e.origin, self.origin, "origin");
+ assert_not_equals(e.source, null, "source");
+ assert_equals(e.ports.length, 0, "ports length");
+
+ state = "onmessageerror was received in worker";
+ e.source.postMessage(state);
+ } else {
+ e.source.postMessage(`worker onmessageerror was reached when in state "${state}" and data ${e.data}`);
+ }
+};
diff --git a/testing/web-platform/tests/webrtc-encoded-transform/routines.js b/testing/web-platform/tests/webrtc-encoded-transform/routines.js
new file mode 100644
index 0000000000..4db7f39621
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-encoded-transform/routines.js
@@ -0,0 +1,32 @@
+async function createConnections(test, setupLocalConnection, setupRemoteConnection, doNotCloseAutmoatically) {
+ const localConnection = new RTCPeerConnection();
+ const remoteConnection = new RTCPeerConnection();
+
+ remoteConnection.onicecandidate = (event) => { localConnection.addIceCandidate(event.candidate); };
+ localConnection.onicecandidate = (event) => { remoteConnection.addIceCandidate(event.candidate); };
+
+ await setupLocalConnection(localConnection);
+ await setupRemoteConnection(remoteConnection);
+
+ const offer = await localConnection.createOffer();
+ await localConnection.setLocalDescription(offer);
+ await remoteConnection.setRemoteDescription(offer);
+
+ const answer = await remoteConnection.createAnswer();
+ await remoteConnection.setLocalDescription(answer);
+ await localConnection.setRemoteDescription(answer);
+
+ if (!doNotCloseAutmoatically) {
+ test.add_cleanup(() => {
+ localConnection.close();
+ remoteConnection.close();
+ });
+ }
+
+ return [localConnection, remoteConnection];
+}
+
+function waitFor(test, duration)
+{
+ return new Promise((resolve) => test.step_timeout(resolve, duration));
+}
diff --git a/testing/web-platform/tests/webrtc-encoded-transform/script-audio-transform-worker.js b/testing/web-platform/tests/webrtc-encoded-transform/script-audio-transform-worker.js
new file mode 100644
index 0000000000..7cb43713d3
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-encoded-transform/script-audio-transform-worker.js
@@ -0,0 +1,30 @@
+class MockRTCRtpTransformer {
+ constructor(transformer) {
+ this.context = transformer;
+ this.start();
+ }
+ start()
+ {
+ this.reader = this.context.readable.getReader();
+ this.writer = this.context.writable.getWriter();
+ this.process();
+ this.context.options.port.postMessage("started " + this.context.options.mediaType + " " + this.context.options.side);
+ }
+
+ process()
+ {
+ this.reader.read().then(chunk => {
+ if (chunk.done)
+ return;
+
+ this.writer.write(chunk.value);
+ this.process();
+ });
+ }
+};
+
+onrtctransform = (event) => {
+ new MockRTCRtpTransformer(event.transformer);
+};
+
+self.postMessage("registered");
diff --git a/testing/web-platform/tests/webrtc-encoded-transform/script-audio-transform.https.html b/testing/web-platform/tests/webrtc-encoded-transform/script-audio-transform.https.html
new file mode 100644
index 0000000000..0d7f401582
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-encoded-transform/script-audio-transform.https.html
@@ -0,0 +1,69 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src=/resources/testdriver.js></script>
+<script src=/resources/testdriver-vendor.js></script>
+<script src='../mediacapture-streams/permission-helper.js'></script>
+ </head>
+ <body>
+ <video id="video" autoplay playsInline></video>
+ <script src="routines.js"></script>
+ <script>
+function waitForMessage(test, port, data)
+{
+ let gotMessage;
+ const promise = new Promise((resolve, reject) => {
+ gotMessage = resolve;
+ test.step_timeout(() => { reject("did not get " + data) }, 5000);
+ });
+ port.onmessage = event => {
+ if (event.data === data)
+ gotMessage();
+ };
+ return promise;
+}
+
+promise_test(async (test) => {
+ worker = new Worker("script-audio-transform-worker.js");
+ const data = await new Promise(resolve => worker.onmessage = (event) => resolve(event.data));
+ assert_equals(data, "registered");
+
+ await setMediaPermission("granted", ["microphone"]);
+ const localStream = await navigator.mediaDevices.getUserMedia({audio: true});
+
+ const senderChannel = new MessageChannel;
+ const receiverChannel = new MessageChannel;
+ const senderTransform = new RTCRtpScriptTransform(worker, {name:'MockRTCRtpTransform', mediaType:'audio', side:'sender', port:senderChannel.port2}, [senderChannel.port2]);
+ const receiverTransform = new RTCRtpScriptTransform(worker, {name:'MockRTCRtpTransform', mediaType:'audio', side:'receiver', port:receiverChannel.port2}, [receiverChannel.port2]);
+ senderTransform.port = senderChannel.port1;
+ receiverTransform.port = receiverChannel.port1;
+
+ promise1 = waitForMessage(test, senderTransform.port, "started audio sender");
+ promise2 = waitForMessage(test, receiverTransform.port, "started audio receiver");
+
+ const stream = await new Promise((resolve, reject) => {
+ createConnections(test, (firstConnection) => {
+ sender = firstConnection.addTrack(localStream.getAudioTracks()[0], localStream);
+ sender.transform = senderTransform;
+ }, (secondConnection) => {
+ secondConnection.ontrack = (trackEvent) => {
+ receiver = trackEvent.receiver;
+ receiver.transform = receiverTransform;
+ resolve(trackEvent.streams[0]);
+ };
+ });
+ test.step_timeout(() => reject("Test timed out"), 5000);
+ });
+
+ await promise1;
+ await promise2;
+
+ video.srcObject = stream;
+ return video.play();
+});
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/webrtc-encoded-transform/script-change-transform-worker.js b/testing/web-platform/tests/webrtc-encoded-transform/script-change-transform-worker.js
new file mode 100644
index 0000000000..84a7aaac18
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-encoded-transform/script-change-transform-worker.js
@@ -0,0 +1,39 @@
+function appendToBuffer(buffer, value) {
+ const result = new ArrayBuffer(buffer.byteLength + 1);
+ const byteResult = new Uint8Array(result);
+ byteResult.set(new Uint8Array(buffer), 0);
+ byteResult[buffer.byteLength] = value;
+ return result;
+}
+
+onrtctransform = (event) => {
+ const transformer = event.transformer;
+
+ transformer.reader = transformer.readable.getReader();
+ transformer.writer = transformer.writable.getWriter();
+
+ function process(transformer)
+ {
+ transformer.reader.read().then(chunk => {
+ if (chunk.done)
+ return;
+ if (transformer.options.name === 'sender1')
+ chunk.value.data = appendToBuffer(chunk.value.data, 1);
+ else if (transformer.options.name === 'sender2')
+ chunk.value.data = appendToBuffer(chunk.value.data, 2);
+ else {
+ const value = new Uint8Array(chunk.value.data)[chunk.value.data.byteLength - 1];
+ if (value !== 1 && value !== 2)
+ self.postMessage("unexpected value: " + value);
+ else if (value === 2)
+ self.postMessage("got value 2");
+ chunk.value.data = chunk.value.data.slice(0, chunk.value.data.byteLength - 1);
+ }
+ transformer.writer.write(chunk.value);
+ process(transformer);
+ });
+ }
+
+ process(transformer);
+};
+self.postMessage("registered");
diff --git a/testing/web-platform/tests/webrtc-encoded-transform/script-change-transform.https.html b/testing/web-platform/tests/webrtc-encoded-transform/script-change-transform.https.html
new file mode 100644
index 0000000000..9ec82a9484
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-encoded-transform/script-change-transform.https.html
@@ -0,0 +1,61 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src=/resources/testdriver.js></script>
+<script src=/resources/testdriver-vendor.js></script>
+<script src='../mediacapture-streams/permission-helper.js'></script>
+ </head>
+ <body>
+ <video id="video1" autoplay controls playsinline></video>
+ <script src ="routines.js"></script>
+ <script>
+async function waitForMessage(worker, data)
+{
+ while (true) {
+ const received = await new Promise(resolve => worker.onmessage = (event) => resolve(event.data));
+ if (data === received)
+ return;
+ }
+}
+
+promise_test(async (test) => {
+ worker = new Worker('script-change-transform-worker.js');
+ const data = await new Promise(resolve => worker.onmessage = (event) => resolve(event.data));
+ assert_equals(data, "registered");
+
+ await setMediaPermission("granted", ["camera"]);
+ const localStream = await navigator.mediaDevices.getUserMedia({video: true});
+
+ let sender, receiver;
+ const senderTransform1 = new RTCRtpScriptTransform(worker, {name:'sender1'});
+ const senderTransform2 = new RTCRtpScriptTransform(worker, {name:'sender2'});
+ const receiverTransform = new RTCRtpScriptTransform(worker, {name:'receiver'});
+
+ const stream = await new Promise((resolve, reject) => {
+ createConnections(test, (firstConnection) => {
+ sender = firstConnection.addTrack(localStream.getVideoTracks()[0], localStream);
+ firstConnection.getTransceivers()[0].setCodecPreferences([{mimeType: "video/VP8", clockRate: 90000}]);
+ sender.transform = senderTransform1;
+ }, (secondConnection) => {
+ secondConnection.ontrack = (trackEvent) => {
+ receiver = trackEvent.receiver;
+ receiver.transform = receiverTransform;
+ resolve(trackEvent.streams[0]);
+ };
+ });
+ test.step_timeout(() => reject("Test timed out"), 5000);
+ });
+
+ video1.srcObject = stream;
+ await video1.play();
+
+ const updatePromise = new Promise(resolve => worker.onmessage = (event) => resolve(event.data));
+ sender.transform = senderTransform2;
+ assert_equals(await updatePromise, "got value 2");
+}, "change sender transform");
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/webrtc-encoded-transform/script-late-transform.https.html b/testing/web-platform/tests/webrtc-encoded-transform/script-late-transform.https.html
new file mode 100644
index 0000000000..726852bad9
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-encoded-transform/script-late-transform.https.html
@@ -0,0 +1,94 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src=/resources/testdriver.js></script>
+<script src=/resources/testdriver-vendor.js></script>
+<script src='../mediacapture-streams/permission-helper.js'></script>
+ </head>
+ <body>
+ <video controls id="video" autoplay></video>
+ <canvas id="canvas" width="640" height="480"></canvas>
+ <script src ="routines.js"></script>
+ <script>
+function grabFrameData(x, y, w, h)
+{
+ canvas.width = video.videoWidth;
+ canvas.height = video.videoHeight;
+
+ canvas.getContext('2d').drawImage(video, x, y, w, h, x, y, w, h);
+ return canvas.getContext('2d').getImageData(x, y, w, h).data;
+}
+
+function getCircleImageData()
+{
+ return grabFrameData(450, 100, 150, 100);
+}
+
+async function checkVideoIsUpdated(test, shouldBeUpdated, count, referenceData)
+{
+ if (count === undefined)
+ count = 0;
+ else if (count >= 20)
+ return Promise.reject("checkVideoIsUpdated timed out :" + shouldBeUpdated + " " + count);
+
+ if (referenceData === undefined)
+ referenceData = getCircleImageData();
+
+ await waitFor(test, 200);
+ const newData = getCircleImageData();
+
+ if (shouldBeUpdated === (JSON.stringify(referenceData) !== JSON.stringify(newData)))
+ return;
+
+ await checkVideoIsUpdated(test, shouldBeUpdated, ++count, newData);
+}
+
+promise_test(async (test) => {
+ await setMediaPermission("granted", ["camera"]);
+ const localStream = await navigator.mediaDevices.getUserMedia({video: true});
+ const senderTransform = new SFrameTransform({ compatibilityMode: "H264" });
+ const receiverTransform = new SFrameTransform({ compatibilityMode: "H264" });
+ await crypto.subtle.importKey("raw", new Uint8Array([143, 77, 43, 10, 72, 19, 37, 67, 236, 219, 24, 93, 26, 165, 91, 178]), "HKDF", false, ["deriveBits", "deriveKey"]).then(key => {
+ senderTransform.setEncryptionKey(key);
+ receiverTransform.setEncryptionKey(key);
+ });
+
+ let sender, receiver;
+ const stream = await new Promise((resolve, reject) => {
+ createConnections(test, (firstConnection) => {
+ pc1 = firstConnection;
+ sender = firstConnection.addTrack(localStream.getVideoTracks()[0], localStream);
+ sender.transform = senderTransform;
+ }, (secondConnection) => {
+ pc2 = secondConnection;
+ secondConnection.ontrack = (trackEvent) => {
+ receiver = trackEvent.receiver;
+ // we do not set the receiver transform here;
+ resolve(trackEvent.streams[0]);
+ };
+ }, {
+ observeOffer : (offer) => {
+ const lines = offer.sdp.split('\r\n');
+ const h264Lines = lines.filter(line => line.indexOf("a=fmtp") === 0 && line.indexOf("42e01f") !== -1);
+ const baselineNumber = h264Lines[0].substring(6).split(' ')[0];
+ offer.sdp = lines.filter(line => {
+ return (line.indexOf('a=fmtp') === -1 && line.indexOf('a=rtcp-fb') === -1 && line.indexOf('a=rtpmap') === -1) || line.indexOf(baselineNumber) !== -1;
+ }).join('\r\n');
+ }
+ });
+ test.step_timeout(() => reject("Test timed out"), 5000);
+ });
+
+ video.srcObject = stream;
+ video.play();
+
+ // We set the receiver transform here so that the decoder probably tried to decode sframe content.
+ test.step_timeout(() => receiver.transform = receiverTransform, 50);
+ await checkVideoIsUpdated(test, true);
+}, "video exchange with late receiver transform");
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/webrtc-encoded-transform/script-metadata-transform-worker.js b/testing/web-platform/tests/webrtc-encoded-transform/script-metadata-transform-worker.js
new file mode 100644
index 0000000000..03ba1f4ee6
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-encoded-transform/script-metadata-transform-worker.js
@@ -0,0 +1,24 @@
+onrtctransform = (event) => {
+ const transformer = event.transformer;
+
+ transformer.reader = transformer.readable.getReader();
+ transformer.writer = transformer.writable.getWriter();
+
+ let isFirstFrame = true;
+ function process(transformer)
+ {
+ transformer.reader.read().then(chunk => {
+ if (chunk.done)
+ return;
+
+ if (isFirstFrame) {
+ isFirstFrame = false;
+ self.postMessage({ name: transformer.options.name, timestamp: chunk.value.timestamp, metadata: chunk.value.getMetadata() });
+ }
+ transformer.writer.write(chunk.value);
+ process(transformer);
+ });
+ }
+ process(transformer);
+};
+self.postMessage("registered");
diff --git a/testing/web-platform/tests/webrtc-encoded-transform/script-metadata-transform.https.html b/testing/web-platform/tests/webrtc-encoded-transform/script-metadata-transform.https.html
new file mode 100644
index 0000000000..c565caba7d
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-encoded-transform/script-metadata-transform.https.html
@@ -0,0 +1,91 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src=/resources/testdriver.js></script>
+<script src=/resources/testdriver-vendor.js></script>
+<script src='../mediacapture-streams/permission-helper.js'></script>
+ </head>
+ <body>
+ <video id="video1" autoplay></video>
+ <script src ="routines.js"></script>
+ <script>
+async function waitForMessage(worker, data)
+{
+ while (true) {
+ const received = await new Promise(resolve => worker.onmessage = (event) => resolve(event.data));
+ if (data === received)
+ return;
+ }
+}
+
+async function gatherMetadata(test, audio)
+{
+ worker = new Worker('script-metadata-transform-worker.js');
+ const data = await new Promise(resolve => worker.onmessage = (event) => resolve(event.data));
+ assert_equals(data, "registered");
+
+ // Both audio and vido are needed at one time or another
+ // so asking for both permissions
+ await setMediaPermission();
+ const localStream = await navigator.mediaDevices.getUserMedia({audio: audio, video: !audio});
+
+ let sender, receiver;
+ const senderTransform = new RTCRtpScriptTransform(worker, {name:'sender'});
+ const receiverTransform = new RTCRtpScriptTransform(worker, {name:'receiver'});
+
+ await new Promise((resolve, reject) => {
+ createConnections(test, (firstConnection) => {
+ pc1 = firstConnection;
+ sender = firstConnection.addTrack(localStream.getTracks()[0], localStream);
+ sender.transform = senderTransform;
+ }, (secondConnection) => {
+ pc2 = secondConnection;
+ secondConnection.ontrack = (trackEvent) => {
+ receiver = trackEvent.receiver;
+ receiver.transform = receiverTransform;
+ resolve(trackEvent.streams[0]);
+ };
+ });
+ test.step_timeout(() => reject("Test timed out"), 5000);
+ });
+
+ return new Promise((resolve, reject) => {
+ let senderMetadata, senderTimestamp;
+ worker.onmessage = (event) => {
+ if (event.data.name === 'sender') {
+ senderMetadata = event.data.metadata;
+ senderTimestamp = event.data.timestamp;
+ } else if (event.data.name === 'receiver')
+ resolve([senderMetadata, senderTimestamp, event.data.metadata, event.data.timestamp]);
+ };
+ test.step_timeout(() => reject("Metadata test timed out"), 5000);
+ });
+}
+
+promise_test(async (test) => {
+ const [senderMetadata, senderTimestamp, receiverMetadata, receiverTimestamp] = await gatherMetadata(test, true);
+
+ assert_equals(senderTimestamp, receiverTimestamp, "timestamp");
+ assert_true(!!senderMetadata.synchronizationSource, "ssrc");
+ assert_equals(senderMetadata.synchronizationSource, receiverMetadata.synchronizationSource, "ssrc");
+ assert_array_equals(senderMetadata.contributingSources, receiverMetadata.contributingSources, "csrc");
+}, "audio exchange with transform");
+
+promise_test(async (test) => {
+ const [senderMetadata, senderTimestamp, receiverMetadata, receiverTimestamp] = await gatherMetadata(test, false);
+
+ assert_equals(senderTimestamp, receiverTimestamp, "timestamp");
+ assert_true(!!senderMetadata.synchronizationSource, "ssrc");
+ assert_equals(senderMetadata.synchronizationSource, receiverMetadata.synchronizationSource, "ssrc");
+ assert_array_equals(senderMetadata.contributingSources, receiverMetadata.contributingSources, "csrc");
+ assert_equals(senderMetadata.height, receiverMetadata.height, "height");
+ assert_equals(senderMetadata.width, receiverMetadata.width, "width");
+ assert_equals(senderMetadata.spatialIndex, receiverMetadata.spatialIndex, "spatialIndex");
+ assert_equals(senderMetadata.temporalIndex, receiverMetadata.temporalIndex, "temporalIndex");
+}, "video exchange with transform");
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/webrtc-encoded-transform/script-transform-worker.js b/testing/web-platform/tests/webrtc-encoded-transform/script-transform-worker.js
new file mode 100644
index 0000000000..5ea99cd2bf
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-encoded-transform/script-transform-worker.js
@@ -0,0 +1,25 @@
+onrtctransform = (event) => {
+ const transformer = event.transformer;
+ transformer.options.port.onmessage = (event) => transformer.options.port.postMessage(event.data);
+
+ self.postMessage("started");
+ transformer.reader = transformer.readable.getReader();
+ transformer.writer = transformer.writable.getWriter();
+
+ function process(transformer)
+ {
+ transformer.reader.read().then(chunk => {
+ if (chunk.done)
+ return;
+ if (chunk.value instanceof RTCEncodedVideoFrame)
+ self.postMessage("video chunk");
+ else if (chunk.value instanceof RTCEncodedAudioFrame)
+ self.postMessage("audio chunk");
+ transformer.writer.write(chunk.value);
+ process(transformer);
+ });
+ }
+
+ process(transformer);
+};
+self.postMessage("registered");
diff --git a/testing/web-platform/tests/webrtc-encoded-transform/script-transform.https.html b/testing/web-platform/tests/webrtc-encoded-transform/script-transform.https.html
new file mode 100644
index 0000000000..e02982f470
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-encoded-transform/script-transform.https.html
@@ -0,0 +1,153 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src=/resources/testdriver.js></script>
+<script src=/resources/testdriver-vendor.js></script>
+<script src='../mediacapture-streams/permission-helper.js'></script>
+ </head>
+ <body>
+ <video id="video1" autoplay></video>
+ <video id="video2" autoplay></video>
+ <script src ="routines.js"></script>
+ <script>
+async function waitForMessage(worker, data)
+{
+ while (true) {
+ const received = await new Promise(resolve => worker.onmessage = (event) => resolve(event.data));
+ if (data === received)
+ return;
+ }
+}
+
+promise_test(async (test) => {
+ worker = new Worker('script-transform-worker.js');
+ const data = await new Promise(resolve => worker.onmessage = (event) => resolve(event.data));
+ assert_equals(data, "registered");
+
+ const channel = new MessageChannel;
+ const transform = new RTCRtpScriptTransform(worker, {name:'MockRTCRtpTransform', port: channel.port2}, [channel.port2]);
+ transform.port = channel.port1;
+ const promise = new Promise(resolve => transform.port.onmessage = (event) => resolve(event.data));
+ transform.port.postMessage("test");
+ assert_equals(await promise, "test");
+}, "transform messaging");
+
+promise_test(async (test) => {
+ worker = new Worker('script-transform-worker.js');
+ const data = await new Promise(resolve => worker.onmessage = (event) => resolve(event.data));
+ assert_equals(data, "registered");
+
+ const pc = new RTCPeerConnection();
+
+ const senderChannel = new MessageChannel;
+ const receiverChannel = new MessageChannel;
+ const senderTransform = new RTCRtpScriptTransform(worker, {name:'MockRTCRtpTransform', port: senderChannel.port2}, [senderChannel.port2]);
+ const receiverTransform = new RTCRtpScriptTransform(worker, {name:'MockRTCRtpTransform', port: receiverChannel.port2}, [receiverChannel.port2]);
+ senderTransform.port = senderChannel.port1;
+ receiverTransform.port = receiverChannel.port1;
+
+ const sender1 = pc.addTransceiver('audio').sender;
+ const sender2 = pc.addTransceiver('video').sender;
+ const receiver1 = pc.getReceivers()[0];
+ const receiver2 = pc.getReceivers()[1];
+
+ sender1.transform = senderTransform;
+ receiver1.transform = receiverTransform;
+ assert_throws_dom("InvalidStateError", () => sender2.transform = senderTransform);
+ assert_throws_dom("InvalidStateError", () => receiver2.transform = receiverTransform);
+
+ sender1.transform = senderTransform;
+ receiver1.transform = receiverTransform;
+
+ sender1.transform = null;
+ receiver1.transform = null;
+}, "Cannot reuse attached transforms");
+
+promise_test(async (test) => {
+ worker = new Worker('script-transform-worker.js');
+ const data = await new Promise(resolve => worker.onmessage = (event) => resolve(event.data));
+ assert_equals(data, "registered");
+ // Video is needed in a later test, so we ask for both permissions
+ await setMediaPermission();
+ const localStream = await navigator.mediaDevices.getUserMedia({audio: true});
+
+ const senderChannel = new MessageChannel;
+ const receiverChannel = new MessageChannel;
+ let sender, receiver;
+ const senderTransform = new RTCRtpScriptTransform(worker, {name:'MockRTCRtpTransform', port: senderChannel.port2}, [senderChannel.port2]);
+ const receiverTransform = new RTCRtpScriptTransform(worker, {name:'MockRTCRtpTransform', port: receiverChannel.port2}, [receiverChannel.port2]);
+ senderTransform.port = senderChannel.port1;
+ receiverTransform.port = receiverChannel.port1;
+
+ const startedPromise = new Promise(resolve => worker.onmessage = (event) => resolve(event.data));
+
+ const stream = await new Promise((resolve, reject) => {
+ createConnections(test, (firstConnection) => {
+ pc1 = firstConnection;
+ sender = firstConnection.addTrack(localStream.getAudioTracks()[0], localStream);
+ sender.transform = senderTransform;
+ }, (secondConnection) => {
+ pc2 = secondConnection;
+ secondConnection.ontrack = (trackEvent) => {
+ receiver = trackEvent.receiver;
+ receiver.transform = receiverTransform;
+ resolve(trackEvent.streams[0]);
+ };
+ });
+ test.step_timeout(() => reject("Test timed out"), 5000);
+ });
+
+ assert_equals(await startedPromise, "started");
+
+ await waitForMessage(worker, "audio chunk");
+
+ video1.srcObject = stream;
+ await video1.play();
+}, "audio exchange with transform");
+
+promise_test(async (test) => {
+ worker = new Worker('script-transform-worker.js');
+ const data = await new Promise(resolve => worker.onmessage = (event) => resolve(event.data));
+ assert_equals(data, "registered");
+
+ const localStream = await navigator.mediaDevices.getUserMedia({video: true});
+
+ const senderChannel = new MessageChannel;
+ const receiverChannel = new MessageChannel;
+ let sender, receiver;
+ const senderTransform = new RTCRtpScriptTransform(worker, {name:'MockRTCRtpTransform', port: senderChannel.port2}, [senderChannel.port2]);
+ const receiverTransform = new RTCRtpScriptTransform(worker, {name:'MockRTCRtpTransform', port: receiverChannel.port2}, [receiverChannel.port2]);
+ senderTransform.port = senderChannel.port1;
+ receiverTransform.port = receiverChannel.port1;
+
+ const startedPromise = new Promise(resolve => worker.onmessage = (event) => resolve(event.data));
+
+ const stream = await new Promise((resolve, reject) => {
+ createConnections(test, (firstConnection) => {
+ pc1 = firstConnection;
+ sender = firstConnection.addTrack(localStream.getVideoTracks()[0], localStream);
+ sender.transform = senderTransform;
+ }, (secondConnection) => {
+ pc2 = secondConnection;
+ secondConnection.ontrack = (trackEvent) => {
+ receiver = trackEvent.receiver;
+ receiver.transform = receiverTransform;
+ resolve(trackEvent.streams[0]);
+ };
+ });
+ test.step_timeout(() => reject("Test timed out"), 5000);
+ });
+
+ assert_equals(await startedPromise, "started");
+
+ await waitForMessage(worker, "video chunk");
+
+ video1.srcObject = stream;
+ await video1.play();
+}, "video exchange with transform");
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/webrtc-encoded-transform/script-write-twice-transform-worker.js b/testing/web-platform/tests/webrtc-encoded-transform/script-write-twice-transform-worker.js
new file mode 100644
index 0000000000..5d428c81b3
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-encoded-transform/script-write-twice-transform-worker.js
@@ -0,0 +1,22 @@
+onrtctransform = (event) => {
+ const transformer = event.transformer;
+
+ self.postMessage("started");
+
+ transformer.reader = transformer.readable.getReader();
+ transformer.writer = transformer.writable.getWriter();
+ function process(transformer)
+ {
+ transformer.reader.read().then(chunk => {
+ if (chunk.done)
+ return;
+
+ transformer.writer.write(chunk.value);
+ transformer.writer.write(chunk.value);
+ process(transformer);
+ });
+ }
+
+ process(transformer);
+};
+self.postMessage("registered");
diff --git a/testing/web-platform/tests/webrtc-encoded-transform/script-write-twice-transform.https.html b/testing/web-platform/tests/webrtc-encoded-transform/script-write-twice-transform.https.html
new file mode 100644
index 0000000000..c4a49af7ac
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-encoded-transform/script-write-twice-transform.https.html
@@ -0,0 +1,62 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src=/resources/testdriver.js></script>
+<script src=/resources/testdriver-vendor.js></script>
+<script src='../mediacapture-streams/permission-helper.js'></script>
+ </head>
+ <body>
+ <video id="video1" autoplay></video>
+ <video id="video2" autoplay></video>
+ <script src ="routines.js"></script>
+ <script>
+async function waitForMessage(worker, data)
+{
+ while (true) {
+ const received = await new Promise(resolve => worker.onmessage = (event) => resolve(event.data));
+ if (data === received)
+ return;
+ }
+}
+
+promise_test(async (test) => {
+ worker = new Worker('script-write-twice-transform-worker.js');
+ const data = await new Promise(resolve => worker.onmessage = (event) => resolve(event.data));
+ assert_equals(data, "registered");
+
+ await setMediaPermission("granted", ["camera"]);
+ const localStream = await navigator.mediaDevices.getUserMedia({video: true});
+
+ let sender, receiver;
+ const senderTransform = new RTCRtpScriptTransform(worker, {name:'MockRTCRtpTransform', side:'sender', role:'encrypt'});
+ const receiverTransform = new RTCRtpScriptTransform(worker, {name:'MockRTCRtpTransform', side:'receiver', role:'decrypt'});
+
+ const startedPromise = new Promise(resolve => worker.onmessage = (event) => resolve(event.data));
+
+ const stream = await new Promise((resolve, reject) => {
+ createConnections(test, (firstConnection) => {
+ pc1 = firstConnection;
+ sender = firstConnection.addTrack(localStream.getVideoTracks()[0], localStream);
+ sender.transform = senderTransform;
+ }, (secondConnection) => {
+ pc2 = secondConnection;
+ secondConnection.ontrack = (trackEvent) => {
+ receiver = trackEvent.receiver;
+ receiver.transform = receiverTransform;
+ resolve(trackEvent.streams[0]);
+ };
+ });
+ test.step_timeout(() => reject("Test timed out"), 5000);
+ });
+
+ assert_equals(await startedPromise, "started");
+
+ video1.srcObject = stream;
+ await video1.play();
+}, "video exchange with write twice transform");
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/webrtc-encoded-transform/set-metadata.https.html b/testing/web-platform/tests/webrtc-encoded-transform/set-metadata.https.html
new file mode 100644
index 0000000000..712971a626
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-encoded-transform/set-metadata.https.html
@@ -0,0 +1,83 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src=/resources/testdriver.js></script>
+<script src=/resources/testdriver-vendor.js></script>
+<script src='../mediacapture-streams/permission-helper.js'></script>
+<script src="../webrtc/RTCPeerConnection-helper.js"></script>
+<script src="../service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<script src='./helper.js'></script>
+<script>
+"use strict";
+
+promise_test(async t => {
+ const senderReader = await setupLoopbackWithCodecAndGetReader(t, 'VP8');
+ const result = await senderReader.read();
+ const metadata = result.value.getMetadata();
+ // TODO(https://crbug.com/webrtc/14709): When RTCEncodedVideoFrame has a
+ // constructor, create a new frame from scratch instead of cloning it to
+ // ensure that none of the metadata was carried over via clone(). This would
+ // allow us to be confident that setMetadata() is doing all the work.
+ //
+ // At that point, we can refactor the clone() implementation to be the same as
+ // constructor() + set data + setMetadata() to ensure that clone() cannot do
+ // things that are not already exposed in JavaScript (no secret steps!).
+ const clone = result.value.clone();
+ clone.setMetadata(metadata);
+ const cloneMetadata = clone.getMetadata();
+ // Encoding related metadata.
+ assert_equals(cloneMetadata.frameId, metadata.frameId, 'frameId');
+ assert_array_equals(cloneMetadata.dependencies, metadata.dependencies,
+ 'dependencies');
+ assert_equals(cloneMetadata.width, metadata.width, 'width');
+ assert_equals(cloneMetadata.height, metadata.height, 'height');
+ assert_equals(cloneMetadata.spatialIndex, metadata.spatialIndex,
+ 'spatialIndex');
+ assert_equals(cloneMetadata.temporalIndex, metadata.temporalIndex,
+ 'temporalIndex');
+ assert_equals(cloneMetadata.frameType, metadata.frameType,
+ 'frameType');
+ // RTCEncodedVideoFrameAdditionalMetadata-only fields.
+ assert_array_equals(cloneMetadata.decodeTargetIndications,
+ metadata.decodeTargetIndications,
+ 'decodeTargetIndications');
+ assert_equals(cloneMetadata.isLastFrameInPicture,
+ metadata.isLastFrameInPicture, 'isLastFrameInPicture');
+ assert_equals(cloneMetadata.simulcastIdx, metadata.simulcastIdx,
+ 'simulcastIdx');
+ assert_equals(cloneMetadata.codec, metadata.codec, 'codec');
+ // VP8-specifics.
+ assert_equals(cloneMetadata.codecSpecifics.nonReference,
+ metadata.codecSpecifics.nonReference,
+ 'codecSpecifics.nonReference');
+ assert_equals(cloneMetadata.codecSpecifics.pictureId,
+ metadata.codecSpecifics.pictureId, 'codecSpecifics.pictureId');
+ assert_equals(cloneMetadata.codecSpecifics.tl0PicIdx,
+ metadata.codecSpecifics.tl0PicIdx, 'codecSpecifics.tl0PicIdx');
+ assert_equals(cloneMetadata.codecSpecifics.temporalIdx,
+ metadata.codecSpecifics.temporalIdx,
+ 'codecSpecifics.temporalIdx');
+ assert_equals(cloneMetadata.codecSpecifics.layerSync,
+ metadata.codecSpecifics.layerSync, 'codecSpecifics.layerSync');
+ assert_equals(cloneMetadata.codecSpecifics.keyIdx,
+ metadata.codecSpecifics.keyIdx, 'codecSpecifics.keyIdx.');
+ assert_equals(cloneMetadata.codecSpecifics.partitionId,
+ metadata.codecSpecifics.partitionId,
+ 'codecSpecifics.partitionId');
+ assert_equals(cloneMetadata.codecSpecifics.beginningOfPartition,
+ metadata.codecSpecifics.beginningOfPartition,
+ 'codecSpecifics.beginningOfPartition');
+ // RTP related metadata.
+ // TODO(https://crbug.com/webrtc/14709): This information also needs to be
+ // settable but isn't - the assertions only pass because clone() copies them
+ // for us. It would be great if different layers didn't consider different
+ // subset of the struct as "the metadata". Can we consolidate into a single
+ // webrtc struct for all metadata?
+ assert_equals(cloneMetadata.synchronizationSource,
+ metadata.synchronizationSource, 'synchronizationSource');
+ assert_array_equals(cloneMetadata.contributingSources,
+ metadata.contributingSources, 'contributingSources');
+ assert_equals(cloneMetadata.payloadType, metadata.payloadType, 'payloadType');
+}, "[VP8] setMetadata() carries over codec-specific properties");
+
+</script>
diff --git a/testing/web-platform/tests/webrtc-encoded-transform/sframe-keys.https.html b/testing/web-platform/tests/webrtc-encoded-transform/sframe-keys.https.html
new file mode 100644
index 0000000000..c87ac12e29
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-encoded-transform/sframe-keys.https.html
@@ -0,0 +1,69 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src=/resources/testdriver.js></script>
+<script src=/resources/testdriver-vendor.js></script>
+<script src='../mediacapture-streams/permission-helper.js'></script>
+ </head>
+ <body>
+ <video id="audio" autoplay playsInline></video>
+ <script src ="routines.js"></script>
+ <script>
+let sender, receiver;
+let key1, key2, key3, key4;
+
+promise_test(async (test) => {
+ const key = await crypto.subtle.importKey("raw", new Uint8Array([143, 77, 43, 10, 72, 19, 37, 67, 236, 219, 24, 93, 26, 165, 91, 178]), "HKDF", false, ["deriveBits", "deriveKey"]);
+ const transform = new SFrameTransform;
+
+ await transform.setEncryptionKey(key);
+ await transform.setEncryptionKey(key, 1);
+
+ await transform.setEncryptionKey(key, BigInt('18446744073709551613'));
+ await transform.setEncryptionKey(key, BigInt('18446744073709551614'));
+ await transform.setEncryptionKey(key, BigInt('18446744073709551615'));
+ await transform.setEncryptionKey(key, BigInt('18446744073709551616')).then(assert_unreached, (e) => {
+ assert_true(e instanceof RangeError);
+ assert_equals(e.message, "Not a 64 bits integer");
+ });
+}, "Passing various key IDs");
+
+promise_test(async (test) => {
+ key1 = await crypto.subtle.importKey("raw", new Uint8Array([143, 77, 43, 10, 72, 19, 37, 67, 236, 219, 24, 93, 26, 165, 91, 178]), "HKDF", false, ["deriveBits", "deriveKey"]);
+ key2 = await crypto.subtle.importKey("raw", new Uint8Array([144, 77, 43, 10, 72, 19, 37, 67, 236, 219, 24, 93, 26, 165, 91, 178]), "HKDF", false, ["deriveBits", "deriveKey"]);
+ key3 = await crypto.subtle.importKey("raw", new Uint8Array([145, 77, 43, 10, 72, 19, 37, 67, 236, 219, 24, 93, 26, 165, 91, 178]), "HKDF", false, ["deriveBits", "deriveKey"]);
+ key4 = await crypto.subtle.importKey("raw", new Uint8Array([146, 77, 43, 10, 72, 19, 37, 67, 236, 219, 24, 93, 26, 165, 91, 178]), "HKDF", false, ["deriveBits", "deriveKey"]);
+
+ await setMediaPermission("granted", ["microphone"]);
+ const localStream = await navigator.mediaDevices.getUserMedia({audio: true});
+ const stream = await new Promise((resolve, reject) => {
+ const connections = createConnections(test, (firstConnection) => {
+ sender = firstConnection.addTrack(localStream.getAudioTracks()[0], localStream);
+ let transform = new SFrameTransform;
+ transform.setEncryptionKey(key1);
+ sender.transform = transform;
+ }, (secondConnection) => {
+ secondConnection.ontrack = (trackEvent) => {
+ let transform = new SFrameTransform;
+ transform.setEncryptionKey(key1);
+ transform.setEncryptionKey(key2);
+ transform.setEncryptionKey(key3, 1000);
+ transform.setEncryptionKey(key4, BigInt('18446744073709551615'));
+ receiver = trackEvent.receiver;
+ receiver.transform = transform;
+ resolve(trackEvent.streams[0]);
+ };
+ });
+
+ test.step_timeout(() => reject("Test timed out"), 5000);
+ });
+
+ audio.srcObject = stream;
+ await audio.play();
+}, "Audio exchange with SFrame setup");
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/webrtc-encoded-transform/sframe-transform-buffer-source.html b/testing/web-platform/tests/webrtc-encoded-transform/sframe-transform-buffer-source.html
new file mode 100644
index 0000000000..99b45f22c9
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-encoded-transform/sframe-transform-buffer-source.html
@@ -0,0 +1,50 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <script>
+
+async function getEncryptedData(transform)
+{
+ const chunk = await transform.readable.getReader().read();
+ const value = new Uint8Array(chunk.value);
+ return [...value];
+}
+
+promise_test(async (test) => {
+ const key = await crypto.subtle.importKey("raw", new Uint8Array([143, 77, 43, 10, 72, 19, 37, 67, 236, 219, 24, 93, 26, 165, 91, 178]), "HKDF", false, ["deriveBits", "deriveKey"]);
+ const transform1 = new SFrameTransform;
+ const transform2 = new SFrameTransform;
+ const transform3 = new SFrameTransform;
+
+ await transform1.setEncryptionKey(key);
+ await transform2.setEncryptionKey(key);
+ await transform3.setEncryptionKey(key);
+
+ const buffer1 = new ArrayBuffer(10);
+ const buffer2 = new ArrayBuffer(11);
+ const view1 = new Uint8Array(buffer1);
+ const view2 = new Uint8Array(buffer2, 1);
+ for (let i = 0 ; i < buffer1.byteLength; ++i) {
+ view1[i] = i;
+ view2[i] = i;
+ }
+
+ transform1.writable.getWriter().write(buffer1);
+ transform2.writable.getWriter().write(view1);
+ transform3.writable.getWriter().write(view2);
+
+ const result1 = await getEncryptedData(transform1);
+ const result2 = await getEncryptedData(transform2);
+ const result3 = await getEncryptedData(transform3);
+
+ assert_array_equals(result1, result2, "result2");
+ assert_array_equals(result1, result3, "result3");
+}, "Uint8Array as input to SFrameTransform");
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/webrtc-encoded-transform/sframe-transform-in-worker.https.html b/testing/web-platform/tests/webrtc-encoded-transform/sframe-transform-in-worker.https.html
new file mode 100644
index 0000000000..f5d7b5a930
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-encoded-transform/sframe-transform-in-worker.https.html
@@ -0,0 +1,61 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src=/resources/testdriver.js></script>
+<script src=/resources/testdriver-vendor.js></script>
+<script src='../mediacapture-streams/permission-helper.js'></script>
+ </head>
+ <body>
+ <video id="video1" controls autoplay></video>
+ <script src ="routines.js"></script>
+ <script>
+async function waitForMessage(worker, data)
+{
+ while (true) {
+ const received = await new Promise(resolve => worker.onmessage = (event) => resolve(event.data));
+ if (data === received)
+ return;
+ }
+}
+
+promise_test(async (test) => {
+ worker = new Worker('sframe-transform-worker.js');
+ const data = await new Promise(resolve => worker.onmessage = (event) => resolve(event.data));
+ assert_equals(data, "registered");
+ await setMediaPermission("granted", ["camera"]);
+ const localStream = await navigator.mediaDevices.getUserMedia({ video: true });
+
+ let sender, receiver;
+ const senderTransform = new SFrameTransform({ compatibilityMode: "H264" });
+ const receiverTransform = new RTCRtpScriptTransform(worker, "SFrameRTCRtpTransform");
+
+ const key = await crypto.subtle.importKey("raw", new Uint8Array([143, 77, 43, 10, 72, 19, 37, 67, 236, 219, 24, 93, 26, 165, 91, 178]), "HKDF", false, ["deriveBits", "deriveKey"]);
+ senderTransform.setEncryptionKey(key);
+
+ const startedPromise = new Promise(resolve => worker.onmessage = (event) => resolve(event.data));
+
+ const stream = await new Promise((resolve, reject) => {
+ createConnections(test, (firstConnection) => {
+ pc1 = firstConnection;
+ sender = firstConnection.addTrack(localStream.getTracks()[0], localStream);
+ sender.transform = senderTransform;
+ }, (secondConnection) => {
+ pc2 = secondConnection;
+ secondConnection.ontrack = (trackEvent) => {
+ receiver = trackEvent.receiver;
+ receiver.transform = receiverTransform;
+ resolve(trackEvent.streams[0]);
+ };
+ });
+ test.step_timeout(() => reject("Test timed out"), 5000);
+ });
+
+ video1.srcObject = stream;
+ await video1.play();
+}, "video exchange with SFrame transform in worker");
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/webrtc-encoded-transform/sframe-transform-readable.html b/testing/web-platform/tests/webrtc-encoded-transform/sframe-transform-readable.html
new file mode 100644
index 0000000000..2afce1b271
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-encoded-transform/sframe-transform-readable.html
@@ -0,0 +1,19 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <iframe src="." id="frame"></iframe>
+ <script>
+promise_test(async (test) => {
+ const frameDOMException = frame.contentWindow.DOMException;
+ const transform = new frame.contentWindow.SFrameTransform;
+ frame.remove();
+ assert_throws_dom("InvalidStateError", frameDOMException, () => transform.readable);
+});
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/webrtc-encoded-transform/sframe-transform-worker.js b/testing/web-platform/tests/webrtc-encoded-transform/sframe-transform-worker.js
new file mode 100644
index 0000000000..617cf0a38a
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-encoded-transform/sframe-transform-worker.js
@@ -0,0 +1,7 @@
+onrtctransform = (event) => {
+ const sframeTransform = new SFrameTransform({ role : "decrypt", authenticationSize: "10", compatibilityMode: "H264" });
+ crypto.subtle.importKey("raw", new Uint8Array([143, 77, 43, 10, 72, 19, 37, 67, 236, 219, 24, 93, 26, 165, 91, 178]), "HKDF", false, ["deriveBits", "deriveKey"]).then(key => sframeTransform.setEncryptionKey(key));
+ const transformer = event.transformer;
+ transformer.readable.pipeThrough(sframeTransform).pipeTo(transformer.writable);
+}
+self.postMessage("registered");
diff --git a/testing/web-platform/tests/webrtc-encoded-transform/sframe-transform.html b/testing/web-platform/tests/webrtc-encoded-transform/sframe-transform.html
new file mode 100644
index 0000000000..2e40135b04
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-encoded-transform/sframe-transform.html
@@ -0,0 +1,141 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <script>
+
+promise_test(async (test) => {
+ const pc = new RTCPeerConnection();
+ const senderTransform = new SFrameTransform();
+ const receiverTransform = new SFrameTransform();
+ const sender1 = pc.addTransceiver('audio').sender;
+ const sender2 = pc.addTransceiver('video').sender;
+ const receiver1 = pc.getReceivers()[0];
+ const receiver2 = pc.getReceivers()[1];
+
+ sender1.transform = senderTransform;
+ receiver1.transform = receiverTransform;
+ assert_throws_dom("InvalidStateError", () => sender2.transform = senderTransform);
+ assert_throws_dom("InvalidStateError", () => receiver2.transform = receiverTransform);
+
+ sender1.transform = senderTransform;
+ receiver1.transform = receiverTransform;
+
+ sender1.transform = null;
+ receiver1.transform = null;
+}, "Cannot reuse attached transforms");
+
+test(() => {
+ const senderTransform = new SFrameTransform();
+
+ assert_true(senderTransform.readable instanceof ReadableStream);
+ assert_true(senderTransform.writable instanceof WritableStream);
+}, "SFrameTransform exposes readable and writable");
+
+promise_test(async (test) => {
+ const pc = new RTCPeerConnection();
+ const senderTransform = new SFrameTransform();
+ const receiverTransform = new SFrameTransform();
+ const sender1 = pc.addTransceiver('audio').sender;
+ const sender2 = pc.addTransceiver('video').sender;
+ const receiver1 = pc.getReceivers()[0];
+ const receiver2 = pc.getReceivers()[1];
+
+ assert_false(senderTransform.readable.locked, "sender readable before");
+ assert_false(senderTransform.writable.locked, "sender writable before");
+ assert_false(receiverTransform.readable.locked, "receiver readable before");
+ assert_false(receiverTransform.writable.locked, "receiver writable before");
+
+ sender1.transform = senderTransform;
+ receiver1.transform = receiverTransform;
+
+ assert_true(senderTransform.readable.locked, "sender readable during");
+ assert_true(senderTransform.writable.locked, "sender writable during");
+ assert_true(receiverTransform.readable.locked, "receiver readable during");
+ assert_true(receiverTransform.writable.locked, "receiver writable during");
+
+ sender1.transform = null;
+ receiver1.transform = null;
+
+ assert_true(senderTransform.readable.locked, "sender readable after");
+ assert_true(senderTransform.writable.locked, "sender writable after");
+ assert_true(receiverTransform.readable.locked, "receiver readable after");
+ assert_true(receiverTransform.writable.locked, "receiver writable after");
+}, "readable/writable are locked when attached and after being attached");
+
+promise_test(async (test) => {
+ const key = await crypto.subtle.importKey("raw", new Uint8Array([143, 77, 43, 10, 72, 19, 37, 67, 236, 219, 24, 93, 26, 165, 91, 178]), "HKDF", false, ["deriveBits", "deriveKey"]);
+
+ const senderTransform = new SFrameTransform({ role : 'encrypt', authenticationSize: 10 });
+ senderTransform.setEncryptionKey(key);
+
+ const receiverTransform = new SFrameTransform({ role : 'decrypt', authenticationSize: 10 });
+ receiverTransform.setEncryptionKey(key);
+
+ const writer = senderTransform.writable.getWriter();
+ const reader = receiverTransform.readable.getReader();
+
+ senderTransform.readable.pipeTo(receiverTransform.writable);
+
+ const sent = new ArrayBuffer(8);
+ const view = new Int8Array(sent);
+ for (let cptr = 0; cptr < sent.byteLength; ++cptr)
+ view[cptr] = cptr;
+
+ writer.write(sent);
+ const received = await reader.read();
+
+ assert_equals(received.value.byteLength, 8);
+ const view2 = new Int8Array(received.value);
+ for (let cptr = 0; cptr < sent.byteLength; ++cptr)
+ assert_equals(view2[cptr], view[cptr]);
+}, "SFrame with array buffer - authentication size 10");
+
+promise_test(async (test) => {
+ const key = await crypto.subtle.importKey("raw", new Uint8Array([143, 77, 43, 10, 72, 19, 37, 67, 236, 219, 24, 93, 26, 165, 91, 178]), "HKDF", false, ["deriveBits", "deriveKey"]);
+
+ const senderTransform = new SFrameTransform({ role : 'encrypt', authenticationSize: 10 });
+ const senderWriter = senderTransform.writable.getWriter();
+ const senderReader = senderTransform.readable.getReader();
+
+ const receiverTransform = new SFrameTransform({ role : 'decrypt', authenticationSize: 10 });
+ const receiverWriter = receiverTransform.writable.getWriter();
+ const receiverReader = receiverTransform.readable.getReader();
+
+ senderTransform.setEncryptionKey(key);
+ receiverTransform.setEncryptionKey(key);
+
+ const chunk = new ArrayBuffer(8);
+
+ // decryption should fail, leading to an empty array buffer.
+ await receiverWriter.write(chunk);
+ let received = await receiverReader.read();
+ assert_equals(received.value.byteLength, 0);
+
+ // We write again but this time with a chunk we can decrypt.
+ await senderWriter.write(chunk);
+ const encrypted = await senderReader.read();
+ await receiverWriter.write(encrypted.value);
+ received = await receiverReader.read();
+ assert_equals(received.value.byteLength, 8);
+}, "SFrame decryption with array buffer that is too small");
+
+promise_test(async (test) => {
+ const key = await crypto.subtle.importKey("raw", new Uint8Array([143, 77, 43, 10, 72, 19, 37, 67, 236, 219, 24, 93, 26, 165, 91, 178]), "HKDF", false, ["deriveBits", "deriveKey"]);
+
+ const receiverTransform = new SFrameTransform({ role : 'decrypt', authenticationSize: 10 });
+ const receiverWriter = receiverTransform.writable.getWriter();
+ receiverTransform.setEncryptionKey(key);
+
+ // decryption should fail, leading to erroring the transform.
+ await promise_rejects_js(test, TypeError, receiverWriter.write({ }));
+ await promise_rejects_js(test, TypeError, receiverWriter.closed);
+}, "SFrame transform gets errored if trying to process unexpected value types");
+
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/webrtc-extensions/META.yml b/testing/web-platform/tests/webrtc-extensions/META.yml
new file mode 100644
index 0000000000..be8cb028f0
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-extensions/META.yml
@@ -0,0 +1,3 @@
+spec: https://w3c.github.io/webrtc-extensions/
+suggested_reviewers:
+ - hbos
diff --git a/testing/web-platform/tests/webrtc-extensions/RTCOAuthCredential.html b/testing/web-platform/tests/webrtc-extensions/RTCOAuthCredential.html
new file mode 100644
index 0000000000..63e92c6d08
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-extensions/RTCOAuthCredential.html
@@ -0,0 +1,44 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCConfiguration iceServers with OAuth credentials</title>
+<script src='/resources/testharness.js'></script>
+<script src='/resources/testharnessreport.js'></script>
+<script src='../webrtc/RTCConfiguration-helper.js'></script>
+<script>
+ 'use strict';
+
+// These tests are based on
+// https://w3c.github.io/webrtc-extensions/#rtcoauthcredential-dictionary
+
+/*
+ 4.3.2. To set a configuration
+ 11.6. If scheme name is turn or turns, and server.credentialType is "oauth",
+ and server.credential is not an RTCOAuthCredential, then throw an
+ InvalidAccessError and abort these steps.
+*/
+config_test(makePc => {
+ assert_throws_dom('InvalidAccessError', () =>
+ makePc({ iceServers: [{
+ urls: 'turns:turn.example.org',
+ credentialType: 'oauth',
+ username: 'user',
+ credential: 'cred'
+ }] }));
+}, 'with turns server, credentialType oauth, and string credential should throw InvalidAccessError');
+
+config_test(makePc => {
+ const pc = makePc({ iceServers: [{
+ urls: 'turns:turn2.example.net',
+ username: '22BIjxU93h/IgwEb',
+ credential: {
+ macKey: 'WmtzanB3ZW9peFhtdm42NzUzNG0=',
+ accessToken: 'AAwg3kPHWPfvk9bDFL936wYvkoctMADzQ5VhNDgeMR3+ZlZ35byg972fW8QjpEl7bx91YLBPFsIhsxloWcXPhA=='
+ },
+ credentialType: 'oauth'
+ }]});
+ const { iceServers } = pc.getConfiguration();
+ const server = iceServers[0];
+ assert_equals(server.credentialType, 'oauth');
+}, 'with turns server, credential type and credential from spec should not throw');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc-extensions/RTCRtpParameters-adaptivePtime.html b/testing/web-platform/tests/webrtc-extensions/RTCRtpParameters-adaptivePtime.html
new file mode 100644
index 0000000000..8a7a8b6ba6
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-extensions/RTCRtpParameters-adaptivePtime.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>RTCRtpEncodingParameters adaptivePtime property</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+ 'use strict';
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const { sender } = pc.addTransceiver('audio', {
+ sendEncodings: [{adaptivePtime: true}],
+ });
+
+ let param = sender.getParameters();
+ let encoding = param.encodings[0];
+
+ assert_true(encoding.adaptivePtime);
+
+ encoding.adaptivePtime = false;
+ await sender.setParameters(param);
+ param = sender.getParameters();
+ encoding = param.encodings[0];
+
+ assert_false(encoding.adaptivePtime);
+
+ }, `Setting adaptivePtime should be accepted`);
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const { sender } = pc.addTransceiver('audio', { sendEncodings: [{}] });
+
+ const param = sender.getParameters();
+ const encoding = param.encodings[0];
+
+ assert_false(encoding.adaptivePtime);
+
+ }, `adaptivePtime should be default false`);
+
+</script>
diff --git a/testing/web-platform/tests/webrtc-extensions/RTCRtpParameters-codec.html b/testing/web-platform/tests/webrtc-extensions/RTCRtpParameters-codec.html
new file mode 100644
index 0000000000..5c81349b15
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-extensions/RTCRtpParameters-codec.html
@@ -0,0 +1,422 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>RTCRtpEncodingParameters codec property</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../webrtc/RTCPeerConnection-helper.js"></script>
+<script src="../webrtc/third_party/sdp/sdp.js"></script>
+<script src="../webrtc/simulcast/simulcast.js"></script>
+<script>
+ 'use strict';
+
+ function findFirstCodec(name) {
+ return RTCRtpSender.getCapabilities(name.split('/')[0]).codecs.filter(c => c.mimeType.localeCompare(name, undefined, { sensitivity: 'base' }) === 0)[0];
+ }
+
+ function codecsNotMatching(mimeType) {
+ return RTCRtpSender.getCapabilities(mimeType.split('/')[0]).codecs.filter(c => c.mimeType.localeCompare(mimeType, undefined, {sensitivity: 'base'}) !== 0);
+ }
+
+ function assertCodecEquals(a, b) {
+ assert_equals(a.mimeType, b.mimeType);
+ assert_equals(a.clockRate, b.clockRate);
+ assert_equals(a.channels, b.channels);
+ assert_equals(a.sdpFmtpLine, b.sdpFmtpLine);
+ }
+
+ async function codecsForSender(sender) {
+ const rids = sender.getParameters().encodings.map(e => e.rid);
+ const stats = await sender.getStats();
+ const codecs = [...stats]
+ .filter(([k, v]) => v.type === 'outbound-rtp')
+ .sort(([k, v], [k2, v2]) => rids.indexOf(v.rid) - rids.indexOf(v2.rid))
+ .map(([k, v]) => stats.get(v.codecId).mimeType);
+ return codecs;
+ }
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const { sender } = pc.addTransceiver('audio');
+
+ let param = sender.getParameters();
+ let encoding = param.encodings[0];
+
+ assert_equals(encoding.codec, undefined);
+ }, `Codec should be undefined by default on audio encodings`);
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const { sender } = pc.addTransceiver('video');
+
+ let param = sender.getParameters();
+ let encoding = param.encodings[0];
+
+ assert_equals(encoding.codec, undefined);
+ }, `Codec should be undefined by default on video encodings`);
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const opus = findFirstCodec('audio/opus');
+
+ const { sender } = pc.addTransceiver('audio', {
+ sendEncodings: [{codec: opus}],
+ });
+
+ let param = sender.getParameters();
+ let encoding = param.encodings[0];
+
+ assertCodecEquals(opus, encoding.codec);
+ }, `Creating an audio sender with addTransceiver and codec should work`);
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const vp8 = findFirstCodec('video/VP8');
+
+ const { sender } = pc.addTransceiver('video', {
+ sendEncodings: [{codec: vp8}],
+ });
+
+ let param = sender.getParameters();
+ let encoding = param.encodings[0];
+
+ assertCodecEquals(vp8, encoding.codec);
+ }, `Creating a video sender with addTransceiver and codec should work`);
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const opus = findFirstCodec('audio/opus');
+
+ const { sender } = pc.addTransceiver('audio');
+
+ let param = sender.getParameters();
+ let encoding = param.encodings[0];
+
+ encoding.codec = opus;
+ await sender.setParameters(param);
+ param = sender.getParameters();
+ encoding = param.encodings[0];
+
+ assertCodecEquals(opus, encoding.codec);
+
+ delete encoding.codec;
+ await sender.setParameters(param);
+ param = sender.getParameters();
+ encoding = param.encodings[0];
+
+ assert_equals(encoding.codec, undefined);
+ }, `Setting codec on an audio sender with setParameters should work`);
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const vp8 = findFirstCodec('video/VP8');
+
+ const { sender } = pc.addTransceiver('video');
+
+ let param = sender.getParameters();
+ let encoding = param.encodings[0];
+
+ encoding.codec = vp8;
+ await sender.setParameters(param);
+ param = sender.getParameters();
+ encoding = param.encodings[0];
+
+ assertCodecEquals(vp8, encoding.codec);
+
+ delete encoding.codec;
+ await sender.setParameters(param);
+ param = sender.getParameters();
+ encoding = param.encodings[0];
+
+ assert_equals(encoding.codec, undefined);
+ }, `Setting codec on a video sender with setParameters should work`);
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const newCodec = {
+ mimeType: "audio/newCodec",
+ clockRate: 90000,
+ channel: 2,
+ };
+
+ assert_throws_dom('OperationError', () => pc.addTransceiver('video', {
+ sendEncodings: [{codec: newCodec}],
+ }));
+ }, `Creating an audio sender with addTransceiver and non-existing codec should throw OperationError`);
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const newCodec = {
+ mimeType: "video/newCodec",
+ clockRate: 90000,
+ };
+
+ assert_throws_dom('OperationError', () => pc.addTransceiver('video', {
+ sendEncodings: [{codec: newCodec}],
+ }));
+ }, `Creating a video sender with addTransceiver and non-existing codec should throw OperationError`);
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const newCodec = {
+ mimeType: "audio/newCodec",
+ clockRate: 90000,
+ channel: 2,
+ };
+
+ const { sender } = pc.addTransceiver('audio');
+
+ let param = sender.getParameters();
+ let encoding = param.encodings[0];
+
+ encoding.codec = newCodec;
+ await promise_rejects_dom(t, "InvalidModificationError", sender.setParameters(param));
+ }, `Setting a non-existing codec on an audio sender with setParameters should throw InvalidModificationError`);
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const newCodec = {
+ mimeType: "video/newCodec",
+ clockRate: 90000,
+ };
+
+ const { sender } = pc.addTransceiver('video');
+
+ let param = sender.getParameters();
+ let encoding = param.encodings[0];
+
+ encoding.codec = newCodec;
+ await promise_rejects_dom(t, "InvalidModificationError", sender.setParameters(param));
+ }, `Setting a non-existing codec on a video sender with setParameters should throw InvalidModificationError`);
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const opus = findFirstCodec('audio/opus');
+ const nonOpus = codecsNotMatching(opus.mimeType);
+
+ const transceiver = pc.addTransceiver('audio');
+ const sender = transceiver.sender;
+
+ transceiver.setCodecPreferences(nonOpus);
+
+ let param = sender.getParameters();
+ let encoding = param.encodings[0];
+
+ encoding.codec = opus;
+ await promise_rejects_dom(t, "InvalidModificationError", sender.setParameters(param));
+ }, `Setting a non-preferred codec on an audio sender with setParameters should throw InvalidModificationError`);
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const vp8 = findFirstCodec('video/VP8');
+ const nonVP8 = codecsNotMatching(vp8.mimeType);
+
+ const transceiver = pc.addTransceiver('video');
+ const sender = transceiver.sender;
+
+ transceiver.setCodecPreferences(nonVP8);
+
+ let param = sender.getParameters();
+ let encoding = param.encodings[0];
+
+ encoding.codec = vp8;
+ await promise_rejects_dom(t, "InvalidModificationError", sender.setParameters(param));
+ }, `Setting a non-preferred codec on a video sender with setParameters should throw InvalidModificationError`);
+
+ promise_test(async (t) => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+
+ const opus = findFirstCodec('audio/opus');
+ const nonOpus = codecsNotMatching(opus.mimeType);
+
+ const transceiver = pc1.addTransceiver('audio');
+ const sender = transceiver.sender;
+
+ transceiver.setCodecPreferences(nonOpus);
+
+ exchangeIceCandidates(pc1, pc2);
+ await exchangeOfferAnswer(pc1, pc2);
+
+ let param = sender.getParameters();
+ let encoding = param.encodings[0];
+
+ encoding.codec = opus;
+ await promise_rejects_dom(t, "InvalidModificationError", sender.setParameters(param));
+ }, `Setting a non-negotiated codec on an audio sender with setParameters should throw InvalidModificationError`);
+
+ promise_test(async (t) => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+
+ const vp8 = findFirstCodec('video/VP8');
+ const nonVP8 = codecsNotMatching(vp8.mimeType);
+
+ const transceiver = pc1.addTransceiver('video');
+ const sender = transceiver.sender;
+
+ transceiver.setCodecPreferences(nonVP8);
+
+ exchangeIceCandidates(pc1, pc2);
+ await exchangeOfferAnswer(pc1, pc2);
+
+ let param = sender.getParameters();
+ let encoding = param.encodings[0];
+
+ encoding.codec = vp8;
+ await promise_rejects_dom(t, "InvalidModificationError", sender.setParameters(param));
+ }, `Setting a non-negotiated codec on a video sender with setParameters should throw InvalidModificationError`);
+
+ promise_test(async (t) => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+
+ const opus = findFirstCodec('audio/opus');
+ const nonOpus = codecsNotMatching(opus.mimeType);
+
+ const transceiver = pc1.addTransceiver('audio', {
+ sendEncodings: [{codec: opus}],
+ });
+ const sender = transceiver.sender;
+
+ exchangeIceCandidates(pc1, pc2);
+ await exchangeOfferAnswer(pc1, pc2);
+
+ let param = sender.getParameters();
+ let encoding = param.encodings[0];
+
+ assertCodecEquals(opus, encoding.codec);
+
+ transceiver.setCodecPreferences(nonOpus);
+ await exchangeOfferAnswer(pc1, pc2);
+
+ param = sender.getParameters();
+ encoding = param.encodings[0];
+
+ assert_equals(encoding.codec, undefined);
+ }, `Codec should be undefined after negotiating away the currently set codec on an audio sender`);
+
+ promise_test(async (t) => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+
+ const vp8 = findFirstCodec('video/VP8');
+ const nonVP8 = codecsNotMatching(vp8.mimeType);
+
+ const transceiver = pc1.addTransceiver('video', {
+ sendEncodings: [{codec: vp8}],
+ });
+ const sender = transceiver.sender;
+
+ exchangeIceCandidates(pc1, pc2);
+ await exchangeOfferAnswer(pc1, pc2);
+
+ let param = sender.getParameters();
+ let encoding = param.encodings[0];
+
+ assertCodecEquals(vp8, encoding.codec);
+
+ transceiver.setCodecPreferences(nonVP8);
+ await exchangeOfferAnswer(pc1, pc2);
+
+ param = sender.getParameters();
+ encoding = param.encodings[0];
+
+ assert_equals(encoding.codec, undefined);
+ }, `Codec should be undefined after negotiating away the currently set codec on a video sender`);
+
+ promise_test(async (t) => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+ const stream = await getNoiseStream({audio:true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+
+ const opus = findFirstCodec('audio/opus');
+ const nonOpus = codecsNotMatching(opus.mimeType);
+
+ const transceiver = pc1.addTransceiver(stream.getTracks()[0]);
+ const sender = transceiver.sender;
+
+ transceiver.setCodecPreferences(nonOpus.concat([opus]));
+
+ exchangeIceCandidates(pc1, pc2);
+ await exchangeOfferAnswer(pc1, pc2);
+
+ let codecs = await codecsForSender(sender);
+ assert_not_equals(codecs[0], opus.mimeType);
+
+ let param = sender.getParameters();
+ let encoding = param.encodings[0];
+ encoding.codec = opus;
+
+ await sender.setParameters(param);
+
+ codecs = await codecsForSender(sender);
+ assert_array_equals(codecs, [opus.mimeType]);
+ }, `Stats output-rtp should match the selected codec in non-simulcast usecase on an audio sender`);
+
+ promise_test(async (t) => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+ const stream = await getNoiseStream({video:true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+
+ const vp8 = findFirstCodec('video/VP8');
+ const nonVP8 = codecsNotMatching(vp8.mimeType);
+
+ const transceiver = pc1.addTransceiver(stream.getTracks()[0]);
+ const sender = transceiver.sender;
+
+ transceiver.setCodecPreferences(nonVP8.concat([vp8]));
+
+ exchangeIceCandidates(pc1, pc2);
+ await exchangeOfferAnswer(pc1, pc2);
+
+ let codecs = await codecsForSender(sender);
+ assert_not_equals(codecs[0], vp8.mimeType);
+
+ let param = sender.getParameters();
+ let encoding = param.encodings[0];
+ encoding.codec = vp8;
+
+ await sender.setParameters(param);
+
+ codecs = await codecsForSender(sender);
+ assert_array_equals(codecs, [vp8.mimeType]);
+ }, `Stats output-rtp should match the selected codec in non-simulcast usecase on a video sender`);
+</script>
diff --git a/testing/web-platform/tests/webrtc-extensions/RTCRtpParameters-maxFramerate.html b/testing/web-platform/tests/webrtc-extensions/RTCRtpParameters-maxFramerate.html
new file mode 100644
index 0000000000..3e348f0d14
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-extensions/RTCRtpParameters-maxFramerate.html
@@ -0,0 +1,101 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCRtpParameters encodings</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/webrtc/dictionary-helper.js"></script>
+<script src="/webrtc/RTCRtpParameters-helper.js"></script>
+<script>
+'use strict';
+
+test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ assert_throws_js(RangeError, () => pc.addTransceiver('video', {
+ sendEncodings: [{
+ maxFramerate: -10
+ }]
+ }));
+}, `addTransceiver() with sendEncoding.maxFramerate field set to less than 0 should reject with RangeError`);
+
+test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ let {sender} = pc.addTransceiver('audio', {
+ sendEncodings: [{
+ maxFramerate: -10
+ }]
+ });
+ let encodings = sender.getParameters().encodings;
+ assert_equals(encodings.length, 1);
+ assert_not_own_property(encodings[0], "maxFramerate");
+
+ sender = pc.addTransceiver('audio', {
+ sendEncodings: [{
+ maxFramerate: 10
+ }]
+ }).sender;
+ encodings = sender.getParameters().encodings;
+ assert_equals(encodings.length, 1);
+ assert_not_own_property(encodings[0], "maxFramerate");
+}, `addTransceiver('audio') with sendEncoding.maxFramerate should succeed, but remove the maxFramerate, even if it is invalid`);
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const {sender} = pc.addTransceiver('audio');
+ let params = sender.getParameters();
+ assert_equals(params.encodings.length, 1);
+ params.encodings[0].maxFramerate = 20;
+ await sender.setParameters(params);
+ const {encodings} = sender.getParameters();
+ assert_equals(encodings.length, 1);
+ assert_not_own_property(encodings[0], "maxFramerate");
+}, `setParameters with maxFramerate on an audio sender should succeed, but remove the maxFramerate`);
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const {sender} = pc.addTransceiver('audio');
+ let params = sender.getParameters();
+ assert_equals(params.encodings.length, 1);
+ params.encodings[0].maxFramerate = -1;
+ await sender.setParameters(params);
+ const {encodings} = sender.getParameters();
+ assert_equals(encodings.length, 1);
+ assert_not_own_property(encodings[0], "maxFramerate");
+}, `setParameters with an invalid maxFramerate on an audio sender should succeed, but remove the maxFramerate`);
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const { sender } = pc.addTransceiver('video');
+ await doOfferAnswerExchange(t, pc);
+
+ const param = sender.getParameters();
+ const encoding = param.encodings[0];
+ assert_not_own_property(encoding, "maxFramerate");
+
+ encoding.maxFramerate = -10;
+ return promise_rejects_js(t, RangeError,
+ sender.setParameters(param));
+}, `setParameters() with encoding.maxFramerate field set to less than 0 should reject with RangeError`);
+
+// It would be great if we could test to see whether maxFramerate is actually
+// honored.
+test_modified_encoding('video', 'maxFramerate', 24, 16,
+ 'setParameters() with maxFramerate 24->16 should succeed');
+
+test_modified_encoding('video', 'maxFramerate', undefined, 16,
+ 'setParameters() with maxFramerate undefined->16 should succeed');
+
+test_modified_encoding('video', 'maxFramerate', 24, undefined,
+ 'setParameters() with maxFramerate 24->undefined should succeed');
+
+test_modified_encoding('video', 'maxFramerate', 0, 16,
+ 'setParameters() with maxFramerate 0->16 should succeed');
+
+test_modified_encoding('video', 'maxFramerate', 24, 0,
+ 'setParameters() with maxFramerate 24->0 should succeed');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc-extensions/RTCRtpReceiver-jitterBufferTarget-stats.html b/testing/web-platform/tests/webrtc-extensions/RTCRtpReceiver-jitterBufferTarget-stats.html
new file mode 100644
index 0000000000..33f71800bd
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-extensions/RTCRtpReceiver-jitterBufferTarget-stats.html
@@ -0,0 +1,96 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<meta name="timeout" content="long">
+<title>Tests RTCRtpReceiver-jitterBufferTarget verified with stats</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/webrtc/RTCPeerConnection-helper.js"></script>
+<body>
+<script>
+'use strict'
+
+function async_promise_test(func, name, properties) {
+ async_test(t => {
+ Promise.resolve(func(t))
+ .catch(t.step_func(e => { throw e; }))
+ .then(() => t.done());
+ }, name, properties);
+}
+
+async_promise_test(t => applyJitterBufferTarget(t, "video", 4000),
+ "measure raising and lowering video jitterBufferTarget");
+async_promise_test(t => applyJitterBufferTarget(t, "audio", 4000),
+ "measure raising and lowering audio jitterBufferTarget");
+
+async function applyJitterBufferTarget(t, kind, target) {
+ const caller = new RTCPeerConnection();
+ t.add_cleanup(() => caller.close());
+ const callee = new RTCPeerConnection();
+ t.add_cleanup(() => callee.close());
+
+ const stream = await getNoiseStream({[kind]:true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ caller.addTransceiver(stream.getTracks()[0], {streams: [stream]});
+ caller.addTransceiver(stream.getTracks()[0], {streams: [stream]});
+
+ exchangeIceCandidates(caller, callee);
+ await exchangeOffer(caller, callee);
+ const [unconstrainedReceiver, constrainedReceiver] = callee.getReceivers();
+ const haveRtp = Promise.all([
+ new Promise(r => constrainedReceiver.track.onunmute = r),
+ new Promise(r => unconstrainedReceiver.track.onunmute = r)
+ ]);
+ await exchangeAnswer(caller, callee);
+ const chromeTimeout = new Promise(r => t.step_timeout(r, 1000)); // crbug.com/1295295
+ await Promise.race([haveRtp, chromeTimeout]);
+
+ // Allow some data to be processed to let the jitter buffer to stabilize a bit before measuring
+ await new Promise(r => t.step_timeout(r, 5000));
+
+ t.step(() => assert_equals(constrainedReceiver.jitterBufferTarget, null,
+ `jitterBufferTarget supported for ${kind}`));
+
+ constrainedReceiver.jitterBufferTarget = target;
+ t.step(() => assert_equals(constrainedReceiver.jitterBufferTarget, target,
+ `jitterBufferTarget increase target for ${kind}`));
+
+ const [increased, base] = await Promise.all([
+ measureDelayFromStats(t, constrainedReceiver, 20),
+ measureDelayFromStats(t, unconstrainedReceiver, 20)
+ ]);
+
+ t.step(() => assert_greater_than(increased , base,
+ `${kind} increased delay ${increased} ` +
+ ` greater than base delay ${base}`));
+
+ constrainedReceiver.jitterBufferTarget = 0;
+
+ // Allow the jitter buffer to stabilize a bit before measuring
+ await new Promise(r => t.step_timeout(r, 5000));
+ t.step(() => assert_equals(constrainedReceiver.jitterBufferTarget, 0,
+ `jitterBufferTarget decrease target for ${kind}`));
+
+ const decreased = await measureDelayFromStats(t, constrainedReceiver, 20);
+
+ t.step(() => assert_less_than(decreased, increased,
+ `${kind} decreasedDelay ${decreased} ` +
+ `less than increased delay ${increased}`));
+}
+
+async function measureDelayFromStats(t, receiver, cycles) {
+
+ let statsReport = await receiver.getStats();
+ const oldInboundStats = [...statsReport.values()].find(({type}) => type == "inbound-rtp");
+
+ await new Promise(r => t.step_timeout(r, 1000 * cycles));
+
+ statsReport = await receiver.getStats();
+ const inboundStats = [...statsReport.values()].find(({type}) => type == "inbound-rtp");
+
+ const delay = ((inboundStats.jitterBufferDelay - oldInboundStats.jitterBufferDelay) /
+ (inboundStats.jitterBufferEmittedCount - oldInboundStats.jitterBufferEmittedCount) * 1000);
+
+ return delay;
+}
+</script>
+</body>
diff --git a/testing/web-platform/tests/webrtc-extensions/RTCRtpReceiver-jitterBufferTarget.html b/testing/web-platform/tests/webrtc-extensions/RTCRtpReceiver-jitterBufferTarget.html
new file mode 100644
index 0000000000..448162d3a2
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-extensions/RTCRtpReceiver-jitterBufferTarget.html
@@ -0,0 +1,130 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Tests for RTCRtpReceiver-jitterBufferTarget attribute</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<script>
+'use strict'
+
+test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const {receiver} = pc.addTransceiver('audio', {direction:'recvonly'});
+ assert_equals(receiver.jitterBufferTarget, null);
+}, 'audio jitterBufferTarget is null by default');
+
+test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const {receiver} = pc.addTransceiver('audio', {direction:'recvonly'});
+ assert_equals(receiver.jitterBufferTarget, null);
+ receiver.jitterBufferTarget = 500;
+ assert_equals(receiver.jitterBufferTarget, 500);
+}, 'audio jitterBufferTarget accepts posititve values');
+
+test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const {receiver} = pc.addTransceiver('audio', {direction:'recvonly'});
+ assert_equals(receiver.jitterBufferTarget, null);
+ receiver.jitterBufferTarget = 4000;
+ assert_throws_js(RangeError, () => {
+ receiver.jitterBufferTarget = 4001;
+ }, 'audio jitterBufferTarget doesn\'t accept values greater than 4000 milliseconds');
+ assert_equals(receiver.jitterBufferTarget, 4000);
+}, 'audio jitterBufferTarget accepts values up to 4000 milliseconds');
+
+test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const {receiver} = pc.addTransceiver('audio', {direction:'recvonly'});
+ assert_equals(receiver.jitterBufferTarget, null);
+ receiver.jitterBufferTarget = 700;
+ assert_throws_js(RangeError, () => {
+ receiver.jitterBufferTarget = -500;
+ }, 'audio jitterBufferTarget doesn\'t accept negative values');
+ assert_equals(receiver.jitterBufferTarget, 700);
+}, 'audio jitterBufferTarget returns last valid value on throw');
+
+test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const {receiver} = pc.addTransceiver('audio', {direction:'recvonly'});
+ assert_equals(receiver.jitterBufferTarget, null);
+ receiver.jitterBufferTarget = 0;
+ assert_equals(receiver.jitterBufferTarget, 0);
+}, 'audio jitterBufferTarget allows zero value');
+
+test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const {receiver} = pc.addTransceiver('audio', {direction:'recvonly'});
+ assert_equals(receiver.jitterBufferTarget, null);
+ receiver.jitterBufferTarget = 500;
+ assert_equals(receiver.jitterBufferTarget, 500);
+ receiver.jitterBufferTarget = null;
+ assert_equals(receiver.jitterBufferTarget, null);
+}, 'audio jitterBufferTarget allows to reset value to null');
+
+test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const {receiver} = pc.addTransceiver('video', {direction:'recvonly'});
+ assert_equals(receiver.jitterBufferTarget, null);
+}, 'video jitterBufferTarget is null by default');
+
+test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const {receiver} = pc.addTransceiver('video', {direction:'recvonly'});
+ assert_equals(receiver.jitterBufferTarget, null);
+ receiver.jitterBufferTarget = 500;
+ assert_equals(receiver.jitterBufferTarget, 500);
+}, 'video jitterBufferTarget accepts posititve values');
+
+test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const {receiver} = pc.addTransceiver('video', {direction:'recvonly'});
+ assert_equals(receiver.jitterBufferTarget, null);
+ receiver.jitterBufferTarget = 4000;
+ assert_throws_js(RangeError, () => {
+ receiver.jitterBufferTarget = 4001;
+ }, 'video jitterBufferTarget doesn\'t accept values greater than 4000 milliseconds');
+ assert_equals(receiver.jitterBufferTarget, 4000);
+}, 'video jitterBufferTarget accepts values up to 4000 milliseconds');
+
+test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const {receiver} = pc.addTransceiver('video', {direction:'recvonly'});
+ assert_equals(receiver.jitterBufferTarget, null);
+ receiver.jitterBufferTarget = 700;
+ assert_throws_js(RangeError, () => {
+ receiver.jitterBufferTarget = -500;
+ }, 'video jitterBufferTarget doesn\'t accept negative values');
+ assert_equals(receiver.jitterBufferTarget, 700);
+}, 'video jitterBufferTarget returns last valid value');
+
+test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const {receiver} = pc.addTransceiver('video', {direction:'recvonly'});
+ assert_equals(receiver.jitterBufferTarget, null);
+ receiver.jitterBufferTarget = 0;
+ assert_equals(receiver.jitterBufferTarget, 0);
+}, 'video jitterBufferTarget allows zero value');
+
+test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const {receiver} = pc.addTransceiver('video', {direction:'recvonly'});
+ assert_equals(receiver.jitterBufferTarget, null);
+ receiver.jitterBufferTarget = 500;
+ assert_equals(receiver.jitterBufferTarget, 500);
+ receiver.jitterBufferTarget = null;
+ assert_equals(receiver.jitterBufferTarget, null);
+}, 'video jitterBufferTarget allows to reset value to null');
+</script>
+</body>
diff --git a/testing/web-platform/tests/webrtc-extensions/RTCRtpSynchronizationSource-captureTimestamp.html b/testing/web-platform/tests/webrtc-extensions/RTCRtpSynchronizationSource-captureTimestamp.html
new file mode 100644
index 0000000000..60b4ed0a74
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-extensions/RTCRtpSynchronizationSource-captureTimestamp.html
@@ -0,0 +1,94 @@
+<!doctype html>
+<meta charset=utf-8>
+<!-- This file contains a test that waits for 2 seconds. -->
+<meta name="timeout" content="long">
+<title>captureTimestamp attribute in RTCRtpSynchronizationSource</title>
+<div><video id="remote" width="124" height="124" autoplay></video></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/webrtc/RTCPeerConnection-helper.js"></script>
+<script src="/webrtc/RTCStats-helper.js"></script>
+<script src="/webrtc-extensions/RTCRtpSynchronizationSource-helper.js"></script>
+<script>
+'use strict';
+
+function listenForCaptureTimestamp(t, receiver) {
+ return new Promise((resolve) => {
+ function listen() {
+ const ssrcs = receiver.getSynchronizationSources();
+ assert_true(ssrcs != undefined);
+ if (ssrcs.length > 0) {
+ assert_equals(ssrcs.length, 1);
+ if (ssrcs[0].captureTimestamp != undefined) {
+ resolve(ssrcs[0].captureTimestamp);
+ return true;
+ }
+ }
+ return false;
+ };
+ t.step_wait(listen, 'No abs-capture-time capture time header extension.');
+ });
+}
+
+// Passes if `getSynchronizationSources()` contains `captureTimestamp` if and
+// only if expected.
+for (const kind of ['audio', 'video']) {
+ promise_test(async t => {
+ const [caller, callee] = await initiateSingleTrackCall(
+ t, /* caps= */{[kind]: true}, /* absCaptureTimeOffered= */false,
+ /* absCaptureTimeAnswered= */false);
+ const receiver = callee.getReceivers()[0];
+
+ for (const ssrc of await listenForSSRCs(t, receiver)) {
+ assert_equals(typeof ssrc.captureTimestamp, 'undefined');
+ }
+ }, '[' + kind + '] getSynchronizationSources() should not contain ' +
+ 'captureTimestamp if absolute capture time RTP header extension is not ' +
+ 'offered');
+
+ promise_test(async t => {
+ const [caller, callee] = await initiateSingleTrackCall(
+ t, /* caps= */{[kind]: true}, /* absCaptureTimeOffered= */false,
+ /* absCaptureTimeAnswered= */false);
+ const receiver = callee.getReceivers()[0];
+
+ for (const ssrc of await listenForSSRCs(t, receiver)) {
+ assert_equals(typeof ssrc.captureTimestamp, 'undefined');
+ }
+ }, '[' + kind + '] getSynchronizationSources() should not contain ' +
+ 'captureTimestamp if absolute capture time RTP header extension is ' +
+ 'offered, but not answered');
+
+ promise_test(async t => {
+ const [caller, callee] = await initiateSingleTrackCall(
+ t, /* caps= */{[kind]: true}, /* absCaptureTimeOffered= */true,
+ /* absCaptureTimeAnswered= */true);
+ const receiver = callee.getReceivers()[0];
+ await listenForCaptureTimestamp(t, receiver);
+ }, '[' + kind + '] getSynchronizationSources() should contain ' +
+ 'captureTimestamp if absolute capture time RTP header extension is ' +
+ 'negotiated');
+}
+
+// Passes if `captureTimestamp` for audio and video are comparable, which is
+// expected since the test creates a local peer connection between `caller` and
+// `callee`.
+promise_test(async t => {
+ const [caller, callee] = await initiateSingleTrackCall(
+ t, /* caps= */{audio: true, video: true},
+ /* absCaptureTimeOffered= */true, /* absCaptureTimeAnswered= */true);
+ const receivers = callee.getReceivers();
+ assert_equals(receivers.length, 2);
+
+ let captureTimestamps = [undefined, undefined];
+ const t0 = performance.now();
+ for (let i = 0; i < 2; ++i) {
+ captureTimestamps[i] = await listenForCaptureTimestamp(t, receivers[i]);
+ }
+ const t1 = performance.now();
+ assert_less_than(Math.abs(captureTimestamps[0] - captureTimestamps[1]),
+ t1 - t0);
+}, 'Audio and video RTCRtpSynchronizationSource.captureTimestamp are ' +
+ 'comparable');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc-extensions/RTCRtpSynchronizationSource-helper.js b/testing/web-platform/tests/webrtc-extensions/RTCRtpSynchronizationSource-helper.js
new file mode 100644
index 0000000000..10cfd65155
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-extensions/RTCRtpSynchronizationSource-helper.js
@@ -0,0 +1,140 @@
+'use strict';
+
+// This file depends on `webrtc/RTCPeerConnection-helper.js`
+// which should be loaded from the main HTML file.
+
+var kAbsCaptureTime =
+ 'http://www.webrtc.org/experiments/rtp-hdrext/abs-capture-time';
+
+function addHeaderExtensionToSdp(sdp, uri) {
+ const extmap = new RegExp('a=extmap:(\\d+)');
+ let sdpLines = sdp.split('\r\n');
+
+ // This assumes at most one audio m= section and one video m= section.
+ // If more are present, only the first section of each kind is munged.
+ for (const section of ['audio', 'video']) {
+ let found_section = false;
+ let maxId = undefined;
+ let maxIdLine = undefined;
+ let extmapAllowMixed = false;
+
+ // find the largest header extension id for section.
+ for (let i = 0; i < sdpLines.length; ++i) {
+ if (!found_section) {
+ if (sdpLines[i].startsWith('m=' + section)) {
+ found_section = true;
+ }
+ continue;
+ } else {
+ if (sdpLines[i].startsWith('m=')) {
+ // end of section
+ break;
+ }
+ }
+
+ if (sdpLines[i] === 'a=extmap-allow-mixed') {
+ extmapAllowMixed = true;
+ }
+ let result = sdpLines[i].match(extmap);
+ if (result && result.length === 2) {
+ if (maxId == undefined || result[1] > maxId) {
+ maxId = parseInt(result[1]);
+ maxIdLine = i;
+ }
+ }
+ }
+
+ if (maxId == 14 && !extmapAllowMixed) {
+ // Reaching the limit of one byte header extension. Adding two byte header
+ // extension support.
+ sdpLines.splice(maxIdLine + 1, 0, 'a=extmap-allow-mixed');
+ }
+ if (maxIdLine !== undefined) {
+ sdpLines.splice(maxIdLine + 1, 0,
+ 'a=extmap:' + (maxId + 1).toString() + ' ' + uri);
+ }
+ }
+ return sdpLines.join('\r\n');
+}
+
+// TODO(crbug.com/1051821): Use RTP header extension API instead of munging
+// when the RTP header extension API is implemented.
+async function addAbsCaptureTimeAndExchangeOffer(caller, callee) {
+ let offer = await caller.createOffer();
+
+ // Absolute capture time header extension may not be offered by default,
+ // in such case, munge the SDP.
+ offer.sdp = addHeaderExtensionToSdp(offer.sdp, kAbsCaptureTime);
+
+ await caller.setLocalDescription(offer);
+ return callee.setRemoteDescription(offer);
+}
+
+// TODO(crbug.com/1051821): Use RTP header extension API instead of munging
+// when the RTP header extension API is implemented.
+async function checkAbsCaptureTimeAndExchangeAnswer(caller, callee,
+ absCaptureTimeAnswered) {
+ let answer = await callee.createAnswer();
+
+ const extmap = new RegExp('a=extmap:\\d+ ' + kAbsCaptureTime + '\r\n', 'g');
+ if (answer.sdp.match(extmap) == null) {
+ // We expect that absolute capture time RTP header extension is answered.
+ // But if not, there is no need to proceed with the test.
+ assert_false(absCaptureTimeAnswered, 'Absolute capture time RTP ' +
+ 'header extension is not answered');
+ } else {
+ if (!absCaptureTimeAnswered) {
+ // We expect that absolute capture time RTP header extension is not
+ // answered, but it is, then we munge the answer to remove it.
+ answer.sdp = answer.sdp.replace(extmap, '');
+ }
+ }
+
+ await callee.setLocalDescription(answer);
+ return caller.setRemoteDescription(answer);
+}
+
+async function exchangeOfferAndListenToOntrack(t, caller, callee,
+ absCaptureTimeOffered) {
+ const ontrackPromise = addEventListenerPromise(t, callee, 'track');
+ // Absolute capture time header extension is expected not offered by default,
+ // and thus munging is needed to enable it.
+ await absCaptureTimeOffered
+ ? addAbsCaptureTimeAndExchangeOffer(caller, callee)
+ : exchangeOffer(caller, callee);
+ return ontrackPromise;
+}
+
+async function initiateSingleTrackCall(t, cap, absCaptureTimeOffered,
+ absCaptureTimeAnswered) {
+ const caller = new RTCPeerConnection();
+ t.add_cleanup(() => caller.close());
+ const callee = new RTCPeerConnection();
+ t.add_cleanup(() => callee.close());
+
+ const stream = await getNoiseStream(cap);
+ stream.getTracks().forEach(track => {
+ caller.addTrack(track, stream);
+ t.add_cleanup(() => track.stop());
+ });
+
+ // TODO(crbug.com/988432): `getSynchronizationSources() on the audio side
+ // needs a hardware sink for the returned dictionary entries to get updated.
+ const remoteVideo = document.getElementById('remote');
+
+ callee.ontrack = e => {
+ remoteVideo.srcObject = e.streams[0];
+ }
+
+ exchangeIceCandidates(caller, callee);
+
+ await exchangeOfferAndListenToOntrack(t, caller, callee,
+ absCaptureTimeOffered);
+
+ // Exchange answer and check whether the absolute capture time RTP header
+ // extension is answered.
+ await checkAbsCaptureTimeAndExchangeAnswer(caller, callee,
+ absCaptureTimeAnswered);
+
+ return [caller, callee];
+}
diff --git a/testing/web-platform/tests/webrtc-extensions/RTCRtpSynchronizationSource-senderCaptureTimeOffset.html b/testing/web-platform/tests/webrtc-extensions/RTCRtpSynchronizationSource-senderCaptureTimeOffset.html
new file mode 100644
index 0000000000..63ad9bf888
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-extensions/RTCRtpSynchronizationSource-senderCaptureTimeOffset.html
@@ -0,0 +1,92 @@
+<!doctype html>
+<meta charset=utf-8>
+<!-- This file contains a test that waits for 2 seconds. -->
+<meta name="timeout" content="long">
+<title>senderCaptureTimeOffset attribute in RTCRtpSynchronizationSource</title>
+<div><video id="remote" width="124" height="124" autoplay></video></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/webrtc/RTCPeerConnection-helper.js"></script>
+<script src="/webrtc/RTCStats-helper.js"></script>
+<script src="/webrtc-extensions/RTCRtpSynchronizationSource-helper.js"></script>
+<script>
+'use strict';
+
+function listenForSenderCaptureTimeOffset(t, receiver) {
+ return new Promise((resolve) => {
+ function listen() {
+ const ssrcs = receiver.getSynchronizationSources();
+ assert_true(ssrcs != undefined);
+ if (ssrcs.length > 0) {
+ assert_equals(ssrcs.length, 1);
+ if (ssrcs[0].captureTimestamp != undefined) {
+ resolve(ssrcs[0].senderCaptureTimeOffset);
+ return true;
+ }
+ }
+ return false;
+ };
+ t.step_wait(listen, 'No abs-capture-time capture time header extension.');
+ });
+}
+
+// Passes if `getSynchronizationSources()` contains `senderCaptureTimeOffset` if
+// and only if expected.
+for (const kind of ['audio', 'video']) {
+ promise_test(async t => {
+ const [caller, callee] = await initiateSingleTrackCall(
+ t, /* caps= */{[kind]: true}, /* absCaptureTimeOffered= */false,
+ /* absCaptureTimeAnswered= */false);
+ const receiver = callee.getReceivers()[0];
+
+ for (const ssrc of await listenForSSRCs(t, receiver)) {
+ assert_equals(typeof ssrc.senderCaptureTimeOffset, 'undefined');
+ }
+ }, '[' + kind + '] getSynchronizationSources() should not contain ' +
+ 'senderCaptureTimeOffset if absolute capture time RTP header extension ' +
+ 'is not offered');
+
+ promise_test(async t => {
+ const [caller, callee] = await initiateSingleTrackCall(
+ t, /* caps= */{[kind]: true}, /* absCaptureTimeOffered= */false,
+ /* absCaptureTimeAnswered= */false);
+ const receiver = callee.getReceivers()[0];
+
+ for (const ssrc of await listenForSSRCs(t, receiver)) {
+ assert_equals(typeof ssrc.senderCaptureTimeOffset, 'undefined');
+ }
+ }, '[' + kind + '] getSynchronizationSources() should not contain ' +
+ 'senderCaptureTimeOffset if absolute capture time RTP header extension ' +
+ 'is offered, but not answered');
+
+ promise_test(async t => {
+ const [caller, callee] = await initiateSingleTrackCall(
+ t, /* caps= */{[kind]: true}, /* absCaptureTimeOffered= */true,
+ /* absCaptureTimeAnswered= */true);
+ const receiver = callee.getReceivers()[0];
+ let senderCaptureTimeOffset = await listenForSenderCaptureTimeOffset(
+ t, receiver);
+ assert_true(senderCaptureTimeOffset != undefined);
+ }, '[' + kind + '] getSynchronizationSources() should contain ' +
+ 'senderCaptureTimeOffset if absolute capture time RTP header extension ' +
+ 'is negotiated');
+}
+
+// Passes if `senderCaptureTimeOffset` is zero, which is expected since the test
+// creates a local peer connection between `caller` and `callee`.
+promise_test(async t => {
+ const [caller, callee] = await initiateSingleTrackCall(
+ t, /* caps= */{audio: true, video: true},
+ /* absCaptureTimeOffered= */true, /* absCaptureTimeAnswered= */true);
+ const receivers = callee.getReceivers();
+ assert_equals(receivers.length, 2);
+
+ for (let i = 0; i < 2; ++i) {
+ let senderCaptureTimeOffset = await listenForSenderCaptureTimeOffset(
+ t, receivers[i]);
+ assert_equals(senderCaptureTimeOffset, 0);
+ }
+}, 'Audio and video RTCRtpSynchronizationSource.senderCaptureTimeOffset must ' +
+ 'be zero');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc-extensions/RTCRtpTransceiver-headerExtensionControl.html b/testing/web-platform/tests/webrtc-extensions/RTCRtpTransceiver-headerExtensionControl.html
new file mode 100644
index 0000000000..79eba02727
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-extensions/RTCRtpTransceiver-headerExtensionControl.html
@@ -0,0 +1,295 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCRtpParameters encodings</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/webrtc/dictionary-helper.js"></script>
+<script src="/webrtc/RTCRtpParameters-helper.js"></script>
+<script src="/webrtc/third_party/sdp/sdp.js"></script>
+<script>
+'use strict';
+
+async function negotiate(pc1, pc2) {
+ await pc1.setLocalDescription();
+ await pc2.setRemoteDescription(pc1.localDescription);
+ await pc2.setLocalDescription();
+ await pc1.setRemoteDescription(pc2.localDescription);
+}
+
+['audio', 'video'].forEach(kind => {
+ test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const transceiver = pc.addTransceiver(kind);
+ const capabilities = transceiver.getHeaderExtensionsToNegotiate();
+ const capability = capabilities.find((capability) => {
+ return capability.uri === 'urn:ietf:params:rtp-hdrext:sdes:mid';
+ });
+ assert_not_equals(capability, undefined);
+ assert_equals(capability.direction, 'sendrecv');
+ }, `the ${kind} transceiver.getHeaderExtensionsToNegotiate() includes mandatory extensions`);
+});
+
+test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const transceiver = pc.addTransceiver('audio');
+ const capabilities = transceiver.getHeaderExtensionsToNegotiate();
+ capabilities[0].uri = '';
+ assert_throws_js(TypeError, () => {
+ transceiver.setHeaderExtensionsToNegotiate(capabilities);
+ }, 'transceiver should throw TypeError when setting an empty URI');
+}, `setHeaderExtensionsToNegotiate throws TypeError on encountering missing URI`);
+
+test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const transceiver = pc.addTransceiver('audio');
+ const capabilities = transceiver.getHeaderExtensionsToNegotiate();
+ capabilities[0].direction = '';
+ assert_throws_js(TypeError, () => {
+ transceiver.setHeaderExtensionsToNegotiate(capabilities);
+ }, 'transceiver should throw TypeError when setting an empty direction');
+}, `setHeaderExtensionsToNegotiate throws TypeError on encountering missing direction`);
+
+test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const transceiver = pc.addTransceiver('audio');
+ const capabilities = transceiver.getHeaderExtensionsToNegotiate();
+ capabilities[0].uri = '4711';
+ assert_throws_dom('InvalidModificationError', () => {
+ transceiver.setHeaderExtensionsToNegotiate(capabilities);
+ }, 'transceiver should throw InvalidModificationError when setting an unknown URI');
+}, `setHeaderExtensionsToNegotiate throws InvalidModificationError on encountering unknown URI`);
+
+test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const transceiver = pc.addTransceiver('video');
+ const capabilities = transceiver.getHeaderExtensionsToNegotiate().filter(capability => {
+ return capability.uri === 'urn:ietf:params:rtp-hdrext:sdes:mid';
+ });
+ assert_throws_dom('InvalidModificationError', () => {
+ transceiver.setHeaderExtensionsToNegotiate(capabilities);
+ }, 'transceiver should throw InvalidModificationError when removing elements from the list');
+}, `setHeaderExtensionsToNegotiate throws InvalidModificationError when removing elements from the list`);
+
+test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const transceiver = pc.addTransceiver('video');
+ const capabilities = transceiver.getHeaderExtensionsToNegotiate();
+ capabilities.push({
+ uri: '4711',
+ direction: 'recvonly',
+ });
+ assert_throws_dom('InvalidModificationError', () => {
+ transceiver.setHeaderExtensionsToNegotiate(capabilities);
+ }, 'transceiver should throw InvalidModificationError when adding elements to the list');
+}, `setHeaderExtensionsToNegotiate throws InvalidModificationError when adding elements to the list`);
+
+test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const transceiver = pc.addTransceiver('audio');
+ const capabilities = transceiver.getHeaderExtensionsToNegotiate();
+ let capability = capabilities.find((capability) => {
+ return capability.uri === 'urn:ietf:params:rtp-hdrext:sdes:mid';
+ });
+ ['sendonly', 'recvonly', 'inactive', 'stopped'].map(direction => {
+ capability.direction = direction;
+ assert_throws_dom('InvalidModificationError', () => {
+ transceiver.setHeaderExtensionsToNegotiate(capabilities);
+ }, `transceiver should throw InvalidModificationError when setting a mandatory header extension\'s direction to ${direction}`);
+ });
+}, `setHeaderExtensionsToNegotiate throws InvalidModificationError when setting a mandatory header extension\'s direction to something else than "sendrecv"`);
+
+test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const transceiver = pc.addTransceiver('audio');
+ let capabilities = transceiver.getHeaderExtensionsToNegotiate();
+ let selected_capability = capabilities.find((capability) => {
+ return capability.direction === 'sendrecv' &&
+ capability.uri !== 'urn:ietf:params:rtp-hdrext:sdes:mid';
+ });
+ selected_capability.direction = 'stopped';
+ const offered_capabilities = transceiver.getHeaderExtensionsToNegotiate();
+ let altered_capability = capabilities.find((capability) => {
+ return capability.uri === selected_capability.uri &&
+ capability.direction === 'stopped';
+ });
+ assert_not_equals(altered_capability, undefined);
+}, `modified direction set by setHeaderExtensionsToNegotiate is visible in subsequent getHeaderExtensionsToNegotiate`);
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const transceiver = pc.addTransceiver('video');
+ const capabilities = transceiver.getHeaderExtensionsToNegotiate();
+ const offer = await pc.createOffer();
+ const extensions = SDPUtils.matchPrefix(SDPUtils.splitSections(offer.sdp)[1], 'a=extmap:')
+ .map(line => SDPUtils.parseExtmap(line));
+ for (const capability of capabilities) {
+ if (capability.direction === 'stopped') {
+ assert_equals(undefined, extensions.find(e => e.uri === capability.uri));
+ } else {
+ assert_not_equals(undefined, extensions.find(e => e.uri === capability.uri));
+ }
+ }
+}, `Unstopped extensions turn up in offer`);
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const transceiver = pc.addTransceiver('video');
+ const capabilities = transceiver.getHeaderExtensionsToNegotiate();
+ const selected_capability = capabilities.find((capability) => {
+ return capability.direction === 'sendrecv' &&
+ capability.uri !== 'urn:ietf:params:rtp-hdrext:sdes:mid';
+ });
+ selected_capability.direction = 'stopped';
+ transceiver.setHeaderExtensionsToNegotiate(capabilities);
+ const offer = await pc.createOffer();
+ const extensions = SDPUtils.matchPrefix(SDPUtils.splitSections(offer.sdp)[1], 'a=extmap:')
+ .map(line => SDPUtils.parseExtmap(line));
+ for (const capability of capabilities) {
+ if (capability.direction === 'stopped') {
+ assert_equals(undefined, extensions.find(e => e.uri === capability.uri));
+ } else {
+ assert_not_equals(undefined, extensions.find(e => e.uri === capability.uri));
+ }
+ }
+}, `Stopped extensions do not turn up in offers`);
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ // Disable a non-mandatory extension before first negotiation.
+ const transceiver = pc1.addTransceiver('video');
+ const capabilities = transceiver.getHeaderExtensionsToNegotiate();
+ const selected_capability = capabilities.find((capability) => {
+ return capability.direction === 'sendrecv' &&
+ capability.uri !== 'urn:ietf:params:rtp-hdrext:sdes:mid';
+ });
+ selected_capability.direction = 'stopped';
+ transceiver.setHeaderExtensionsToNegotiate(capabilities);
+
+ await negotiate(pc1, pc2);
+ const negotiated_capabilites = transceiver.getNegotiatedHeaderExtensions();
+
+ assert_equals(capabilities.length, negotiated_capabilites.length);
+}, `The set of negotiated extensions has the same size as the set of extensions to negotiate`);
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ // Disable a non-mandatory extension before first negotiation.
+ const transceiver = pc1.addTransceiver('video');
+ const capabilities = transceiver.getHeaderExtensionsToNegotiate();
+ const selected_capability = capabilities.find((capability) => {
+ return capability.direction === 'sendrecv' &&
+ capability.uri !== 'urn:ietf:params:rtp-hdrext:sdes:mid';
+ });
+ selected_capability.direction = 'stopped';
+ transceiver.setHeaderExtensionsToNegotiate(capabilities);
+
+ await negotiate(pc1, pc2);
+ const negotiated_capabilites = transceiver.getNegotiatedHeaderExtensions();
+
+ // Attempt enabling the extension.
+ selected_capability.direction = 'sendrecv';
+
+ // The enabled extension should not be part of the negotiated set.
+ transceiver.setHeaderExtensionsToNegotiate(capabilities);
+ await negotiate(pc1, pc2);
+ assert_not_equals(
+ transceiver.getNegotiatedHeaderExtensions().find(capability => {
+ return capability.uri === selected_capability.uri &&
+ capability.direction === 'sendrecv';
+ }), undefined);
+}, `Header extensions can be reactivated in subsequent offers`);
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const t1 = pc.addTransceiver('video');
+ const t2 = pc.addTransceiver('video');
+ const extensionUri = 'urn:3gpp:video-orientation';
+
+ assert_true(!!t1.getHeaderExtensionsToNegotiate().find(ext => ext.uri === extensionUri));
+ const ext1 = t1.getHeaderExtensionsToNegotiate();
+ ext1.find(ext => ext.uri === extensionUri).direction = 'stopped';
+ t1.setHeaderExtensionsToNegotiate(ext1);
+
+ assert_true(!!t2.getHeaderExtensionsToNegotiate().find(ext => ext.uri === extensionUri));
+ const ext2 = t2.getHeaderExtensionsToNegotiate();
+ ext2.find(ext => ext.uri === extensionUri).direction = 'sendrecv';
+ t2.setHeaderExtensionsToNegotiate(ext2);
+
+ const offer = await pc.createOffer();
+ const sections = SDPUtils.splitSections(offer.sdp);
+ sections.shift();
+ const extensions = sections.map(section => {
+ return SDPUtils.matchPrefix(section, 'a=extmap:')
+ .map(SDPUtils.parseExtmap);
+ });
+ assert_equals(extensions.length, 2);
+ assert_false(!!extensions[0].find(extension => extension.uri === extensionUri));
+ assert_true(!!extensions[1].find(extension => extension.uri === extensionUri));
+}, 'Header extensions can be deactivated on a per-mline basis');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const t1 = pc1.addTransceiver('video');
+
+ await pc1.setLocalDescription();
+ await pc2.setRemoteDescription(pc1.localDescription);
+ // Get the transceiver after it is created by SRD.
+ const t2 = pc2.getTransceivers()[0];
+ const t2_capabilities = t2.getHeaderExtensionsToNegotiate();
+ const t2_capability_to_stop = t2_capabilities
+ .find(capability => capability.uri !== 'urn:ietf:params:rtp-hdrext:sdes:mid');
+ assert_not_equals(undefined, t2_capability_to_stop);
+ t2_capability_to_stop.direction = 'stopped';
+ t2.setHeaderExtensionsToNegotiate(t2_capabilities);
+
+ await pc2.setLocalDescription();
+ await pc1.setRemoteDescription(pc2.localDescription);
+
+ const t1_negotiated = t1.getNegotiatedHeaderExtensions()
+ .find(extension => extension.uri === t2_capability_to_stop.uri);
+ assert_not_equals(undefined, t1_negotiated);
+ assert_equals(t1_negotiated.direction, 'stopped');
+ const t1_capability = t1.getHeaderExtensionsToNegotiate()
+ .find(extension => extension.uri === t2_capability_to_stop.uri);
+ assert_not_equals(undefined, t1_capability);
+ assert_equals(t1_capability.direction, 'sendrecv');
+}, 'Extensions not negotiated by the peer are `stopped` in getNegotiatedHeaderExtensions');
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const transceiver = pc.addTransceiver('video');
+ const negotiated_capabilites = transceiver.getNegotiatedHeaderExtensions();
+ assert_equals(negotiated_capabilites.length,
+ transceiver.getHeaderExtensionsToNegotiate().length);
+ for (const capability of negotiated_capabilites) {
+ assert_equals(capability.direction, 'stopped');
+ }
+}, 'Prior to negotiation, getNegotiatedHeaderExtensions() returns `stopped` for all extensions.');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc-extensions/transfer-datachannel-service-worker.https.html b/testing/web-platform/tests/webrtc-extensions/transfer-datachannel-service-worker.https.html
new file mode 100644
index 0000000000..625fee4fe1
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-extensions/transfer-datachannel-service-worker.https.html
@@ -0,0 +1,83 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <script src="../service-workers/service-worker/resources/test-helpers.sub.js"></script>
+ <script>
+async function createConnections(test, firstConnectionCallback, secondConnectionCallback)
+{
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+
+ test.add_cleanup(() => pc1.close());
+ test.add_cleanup(() => pc2.close());
+
+ pc1.onicecandidate = (e) => pc2.addIceCandidate(e.candidate);
+ pc2.onicecandidate = (e) => pc1.addIceCandidate(e.candidate);
+
+ firstConnectionCallback(pc1);
+
+ const offer = await pc1.createOffer();
+ await pc1.setLocalDescription(offer);
+ await pc2.setRemoteDescription(offer);
+
+ secondConnectionCallback(pc2);
+
+ const answer = await pc2.createAnswer();
+ await pc2.setLocalDescription(answer);
+ await pc1.setRemoteDescription(answer);
+}
+
+async function waitForMessage(receiver, data)
+{
+ while (true) {
+ const received = await new Promise(resolve => receiver.onmessage = (event) => resolve(event.data));
+ if (data === received)
+ return;
+ }
+}
+
+promise_test(async (test) => {
+ let frame;
+ const scope = 'resources/';
+ const script = 'transfer-datachannel-service-worker.js';
+
+ await service_worker_unregister(test, scope);
+ const registration = await navigator.serviceWorker.register(script, {scope});
+ test.add_cleanup(async () => {
+ return service_worker_unregister(test, scope);
+ });
+ const worker = registration.installing;
+
+ const messageChannel = new MessageChannel();
+
+ let localChannel;
+ let remoteChannel;
+
+ await new Promise((resolve, reject) => {
+ createConnections(test, (firstConnection) => {
+ localChannel = firstConnection.createDataChannel('sendDataChannel');
+ worker.postMessage({channel: localChannel, port: messageChannel.port2}, [localChannel, messageChannel.port2]);
+ }, (secondConnection) => {
+ secondConnection.ondatachannel = (event) => {
+ remoteChannel = event.channel;
+ remoteChannel.onopen = resolve;
+ };
+ });
+ });
+
+ const promise = waitForMessage(messageChannel.port1, "OK");
+ remoteChannel.send("OK");
+ await promise;
+
+ const data = new Promise(resolve => remoteChannel.onmessage = (event) => resolve(event.data));
+ messageChannel.port1.postMessage({message: "OK2"});
+ assert_equals(await data, "OK2");
+}, "offerer data channel in service worker");
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/webrtc-extensions/transfer-datachannel-service-worker.js b/testing/web-platform/tests/webrtc-extensions/transfer-datachannel-service-worker.js
new file mode 100644
index 0000000000..c1919d0b9a
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-extensions/transfer-datachannel-service-worker.js
@@ -0,0 +1,15 @@
+let channel;
+let port;
+onmessage = (e) => {
+ if (e.data.port) {
+ port = e.data.port;
+ port.onmessage = (event) => channel.send(event.data.message);
+ }
+ if (e.data.channel) {
+ channel = e.data.channel;
+ channel.onopen = () => port.postMessage("opened");
+ channel.onerror = () => port.postMessage("errored");
+ channel.onclose = () => port.postMessage("closed");
+ channel.onmessage = (event) => port.postMessage(event.data);
+ }
+};
diff --git a/testing/web-platform/tests/webrtc-extensions/transfer-datachannel-worker.js b/testing/web-platform/tests/webrtc-extensions/transfer-datachannel-worker.js
new file mode 100644
index 0000000000..10d71f68f0
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-extensions/transfer-datachannel-worker.js
@@ -0,0 +1,19 @@
+let channel;
+onmessage = (event) => {
+ if (event.data.channel) {
+ channel = event.data.channel;
+ channel.onopen = () => self.postMessage("opened");
+ channel.onerror = () => self.postMessage("errored");
+ channel.onclose = () => self.postMessage("closed");
+ channel.onmessage = event => self.postMessage(event.data);
+ }
+ if (event.data.message) {
+ if (channel)
+ channel.send(event.data.message);
+ }
+ if (event.data.close) {
+ if (channel)
+ channel.close();
+ }
+};
+self.postMessage("registered");
diff --git a/testing/web-platform/tests/webrtc-extensions/transfer-datachannel.html b/testing/web-platform/tests/webrtc-extensions/transfer-datachannel.html
new file mode 100644
index 0000000000..9759a67a24
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-extensions/transfer-datachannel.html
@@ -0,0 +1,165 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <script src="../service-workers/service-worker/resources/test-helpers.sub.js"></script>
+ <script>
+async function createConnections(test, firstConnectionCallback, secondConnectionCallback)
+{
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+
+ test.add_cleanup(() => pc1.close());
+ test.add_cleanup(() => pc2.close());
+
+ pc1.onicecandidate = (e) => pc2.addIceCandidate(e.candidate);
+ pc2.onicecandidate = (e) => pc1.addIceCandidate(e.candidate);
+
+ firstConnectionCallback(pc1);
+
+ const offer = await pc1.createOffer();
+ await pc1.setLocalDescription(offer);
+ await pc2.setRemoteDescription(offer);
+
+ secondConnectionCallback(pc2);
+
+ const answer = await pc2.createAnswer();
+ await pc2.setLocalDescription(answer);
+ await pc1.setRemoteDescription(answer);
+}
+
+async function waitForMessage(receiver, data)
+{
+ while (true) {
+ const received = await new Promise(resolve => receiver.onmessage = (event) => resolve(event.data));
+ if (data === received)
+ return;
+ }
+}
+
+promise_test(async (test) => {
+ let localChannel;
+ let remoteChannel;
+
+ const worker = new Worker('transfer-datachannel-worker.js');
+ let data = new Promise(resolve => worker.onmessage = (event) => resolve(event.data));
+ assert_equals(await data, "registered");
+
+ await new Promise((resolve, reject) => {
+ createConnections(test, (firstConnection) => {
+ localChannel = firstConnection.createDataChannel('sendDataChannel');
+ worker.postMessage({channel: localChannel}, [localChannel]);
+ data = new Promise(resolve => worker.onmessage = (event) => resolve(event.data));
+ }, (secondConnection) => {
+ secondConnection.ondatachannel = (event) => {
+ remoteChannel = event.channel;
+ remoteChannel.onopen = resolve;
+ };
+ });
+ });
+
+ assert_equals(await data, "opened");
+
+ data = new Promise(resolve => worker.onmessage = (event) => resolve(event.data));
+ remoteChannel.send("OK");
+ assert_equals(await data, "OK");
+
+ data = new Promise(resolve => remoteChannel.onmessage = (event) => resolve(event.data));
+ worker.postMessage({message: "OK2"});
+ assert_equals(await data, "OK2");
+}, "offerer data channel in workers");
+
+
+promise_test(async (test) => {
+ let localChannel;
+ let remoteChannel;
+
+ const worker = new Worker('transfer-datachannel-worker.js');
+ let data = new Promise(resolve => worker.onmessage = (event) => resolve(event.data));
+ assert_equals(await data, "registered");
+
+ data = new Promise(resolve => worker.onmessage = (event) => resolve(event.data));
+ await new Promise((resolve, reject) => {
+ createConnections(test, (firstConnection) => {
+ localChannel = firstConnection.createDataChannel('sendDataChannel');
+ localChannel.onopen = resolve;
+ }, (secondConnection) => {
+ secondConnection.ondatachannel = (event) => {
+ remoteChannel = event.channel;
+ worker.postMessage({channel: remoteChannel}, [remoteChannel]);
+ };
+ });
+ });
+ assert_equals(await data, "opened");
+
+ data = new Promise(resolve => worker.onmessage = (event) => resolve(event.data));
+ localChannel.send("OK");
+ assert_equals(await data, "OK");
+
+ data = new Promise(resolve => localChannel.onmessage = (event) => resolve(event.data));
+ worker.postMessage({message: "OK2"});
+ assert_equals(await data, "OK2");
+}, "answerer data channel in workers");
+
+promise_test(async (test) => {
+ let localChannel;
+ let remoteChannel;
+
+ const worker = new Worker('transfer-datachannel-worker.js');
+ let data = new Promise(resolve => worker.onmessage = (event) => resolve(event.data));
+ assert_equals(await data, "registered");
+
+ data = new Promise(resolve => worker.onmessage = (event) => resolve(event.data));
+ await new Promise((resolve, reject) => {
+ createConnections(test, (firstConnection) => {
+ localChannel = firstConnection.createDataChannel('sendDataChannel');
+ worker.postMessage({channel: localChannel}, [localChannel]);
+
+ }, (secondConnection) => {
+ secondConnection.ondatachannel = (event) => {
+ remoteChannel = event.channel;
+ remoteChannel.onopen = resolve;
+ };
+ });
+ });
+ assert_equals(await data, "opened");
+
+ data = new Promise(resolve => worker.onmessage = (event) => resolve(event.data));
+ remoteChannel.close();
+ assert_equals(await data, "closed");
+
+}, "data channel close event in worker");
+
+promise_test(async (test) => {
+ let localChannel;
+ let remoteChannel;
+
+ const worker = new Worker('transfer-datachannel-worker.js');
+ let data = new Promise(resolve => worker.onmessage = (event) => resolve(event.data));
+ assert_equals(await data, "registered");
+
+ await new Promise((resolve, reject) => {
+ createConnections(test, (firstConnection) => {
+ localChannel = firstConnection.createDataChannel('sendDataChannel');
+ }, (secondConnection) => {
+ secondConnection.ondatachannel = (event) => {
+ remoteChannel = event.channel;
+ test.step_timeout(() => {
+ try {
+ worker.postMessage({channel: remoteChannel}, [remoteChannel]);
+ reject("postMessage ok");
+ } catch(e) {
+ resolve();
+ }
+ }, 0);
+ };
+ });
+ });
+}, "Failing to transfer a data channel");
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/webrtc-ice/META.yml b/testing/web-platform/tests/webrtc-ice/META.yml
new file mode 100644
index 0000000000..e683349e3c
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-ice/META.yml
@@ -0,0 +1,3 @@
+spec: https://w3c.github.io/webrtc-ice/
+suggested_reviewers:
+ - alvestrand
diff --git a/testing/web-platform/tests/webrtc-ice/RTCIceTransport-extension-helper.js b/testing/web-platform/tests/webrtc-ice/RTCIceTransport-extension-helper.js
new file mode 100644
index 0000000000..659ec59b8d
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-ice/RTCIceTransport-extension-helper.js
@@ -0,0 +1,42 @@
+'use strict';
+
+// Construct an RTCIceTransport instance. The instance will automatically be
+// cleaned up when the test finishes.
+function makeIceTransport(t) {
+ const iceTransport = new RTCIceTransport();
+ t.add_cleanup(() => iceTransport.stop());
+ return iceTransport;
+}
+
+// Construct two RTCIceTransport instances, configure them to exchange
+// candidates, then gather() them.
+// Returns a 2-list: [ RTCIceTransport, RTCIceTransport ]
+function makeAndGatherTwoIceTransports(t) {
+ const localTransport = makeIceTransport(t);
+ const remoteTransport = makeIceTransport(t);
+ localTransport.onicecandidate = e => {
+ if (e.candidate) {
+ remoteTransport.addRemoteCandidate(e.candidate);
+ }
+ };
+ remoteTransport.onicecandidate = e => {
+ if (e.candidate) {
+ localTransport.addRemoteCandidate(e.candidate);
+ }
+ };
+ localTransport.gather({});
+ remoteTransport.gather({});
+ return [ localTransport, remoteTransport ];
+}
+
+// Construct two RTCIceTransport instances, configure them to exchange
+// candidates and parameters, then gather() and start() them.
+// Returns a 2-list:
+// [ controlling RTCIceTransport,
+// controlled RTCIceTransport ]
+function makeGatherAndStartTwoIceTransports(t) {
+ const [ localTransport, remoteTransport ] = makeAndGatherTwoIceTransports(t);
+ localTransport.start(remoteTransport.getLocalParameters(), 'controlling');
+ remoteTransport.start(localTransport.getLocalParameters(), 'controlled');
+ return [ localTransport, remoteTransport ];
+}
diff --git a/testing/web-platform/tests/webrtc-ice/RTCIceTransport-extension.https.html b/testing/web-platform/tests/webrtc-ice/RTCIceTransport-extension.https.html
new file mode 100644
index 0000000000..bb4d52adce
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-ice/RTCIceTransport-extension.https.html
@@ -0,0 +1,362 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCIceTransport-extensions.https.html</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="RTCIceTransport-extension-helper.js"></script>
+<script>
+'use strict';
+
+// These tests are based on the following extension specification:
+// https://w3c.github.io/webrtc-ice/
+
+// The following helper functions are called from
+// RTCIceTransport-extension-helper.js:
+// makeIceTransport
+// makeGatherAndStartTwoIceTransports
+
+const ICE_UFRAG = 'u'.repeat(4);
+const ICE_PWD = 'p'.repeat(22);
+
+test(() => {
+ const iceTransport = new RTCIceTransport();
+}, 'RTCIceTransport constructor does not throw');
+
+test(() => {
+ const iceTransport = new RTCIceTransport();
+ assert_equals(iceTransport.role, null, 'Expect role to be null');
+ assert_equals(iceTransport.state, 'new', `Expect state to be 'new'`);
+ assert_equals(iceTransport.gatheringState, 'new',
+ `Expect gatheringState to be 'new'`);
+ assert_array_equals(iceTransport.getLocalCandidates(), [],
+ 'Expect no local candidates');
+ assert_array_equals(iceTransport.getRemoteCandidates(), [],
+ 'Expect no remote candidates');
+ assert_equals(iceTransport.getSelectedCandidatePair(), null,
+ 'Expect no selected candidate pair');
+ assert_not_equals(iceTransport.getLocalParameters(), null,
+ 'Expect local parameters generated');
+ assert_equals(iceTransport.getRemoteParameters(), null,
+ 'Expect no remote parameters');
+}, 'RTCIceTransport initial properties are set');
+
+test(t => {
+ const iceTransport = makeIceTransport(t);
+ assert_throws_js(TypeError, () =>
+ iceTransport.gather({ iceServers: null }));
+}, 'gather() with { iceServers: null } should throw TypeError');
+
+test(t => {
+ const iceTransport = makeIceTransport(t);
+ iceTransport.gather({ iceServers: undefined });
+}, 'gather() with { iceServers: undefined } should succeed');
+
+test(t => {
+ const iceTransport = makeIceTransport(t);
+ iceTransport.gather({ iceServers: [{
+ urls: ['turns:turn.example.org', 'turn:turn.example.net'],
+ username: 'user',
+ credential: 'cred',
+ }] });
+}, 'gather() with one turns server, one turn server, username, credential' +
+ ' should succeed');
+
+test(t => {
+ const iceTransport = makeIceTransport(t);
+ iceTransport.gather({ iceServers: [{
+ urls: ['stun:stun1.example.net', 'stun:stun2.example.net'],
+ }] });
+}, 'gather() with 2 stun servers should succeed');
+
+test(t => {
+ const iceTransport = makeIceTransport(t);
+ iceTransport.stop();
+ assert_throws_dom('InvalidStateError', () => iceTransport.gather({}));
+}, 'gather() throws if closed');
+
+test(t => {
+ const iceTransport = makeIceTransport(t);
+ iceTransport.gather({});
+ assert_equals(iceTransport.gatheringState, 'gathering');
+}, `gather() transitions gatheringState to 'gathering'`);
+
+test(t => {
+ const iceTransport = makeIceTransport(t);
+ iceTransport.gather({});
+ assert_throws_dom('InvalidStateError', () => iceTransport.gather({}));
+}, 'gather() throws if called twice');
+
+promise_test(async t => {
+ const iceTransport = makeIceTransport(t);
+ const watcher = new EventWatcher(t, iceTransport, 'gatheringstatechange');
+ iceTransport.gather({});
+ await watcher.wait_for('gatheringstatechange');
+ assert_equals(iceTransport.gatheringState, 'complete');
+}, `eventually transition gatheringState to 'complete'`);
+
+promise_test(async t => {
+ const iceTransport = makeIceTransport(t);
+ const watcher = new EventWatcher(t, iceTransport,
+ [ 'icecandidate', 'gatheringstatechange' ]);
+ iceTransport.gather({});
+ let candidate;
+ do {
+ (({ candidate } = await watcher.wait_for('icecandidate')));
+ } while (candidate !== null);
+ assert_equals(iceTransport.gatheringState, 'gathering');
+ await watcher.wait_for('gatheringstatechange');
+ assert_equals(iceTransport.gatheringState, 'complete');
+}, 'onicecandidate fires with null candidate before gatheringState' +
+ ` transitions to 'complete'`);
+
+promise_test(async t => {
+ const iceTransport = makeIceTransport(t);
+ const watcher = new EventWatcher(t, iceTransport, 'icecandidate');
+ iceTransport.gather({});
+ const { candidate } = await watcher.wait_for('icecandidate');
+ assert_not_equals(candidate.candidate, '');
+ assert_array_equals(iceTransport.getLocalCandidates(), [candidate]);
+}, 'gather() returns at least one host candidate');
+
+promise_test(async t => {
+ const iceTransport = makeIceTransport(t);
+ const watcher = new EventWatcher(t, iceTransport, 'icecandidate');
+ iceTransport.gather({ gatherPolicy: 'relay' });
+ const { candidate } = await watcher.wait_for('icecandidate');
+ assert_equals(candidate, null);
+ assert_array_equals(iceTransport.getLocalCandidates(), []);
+}, `gather() returns no candidates with { gatherPolicy: 'relay'} and no turn` +
+ ' servers');
+
+const dummyRemoteParameters = {
+ usernameFragment: ICE_UFRAG,
+ password: ICE_PWD,
+};
+
+test(() => {
+ const iceTransport = new RTCIceTransport();
+ iceTransport.stop();
+ assert_throws_dom('InvalidStateError',
+ () => iceTransport.start(dummyRemoteParameters));
+ assert_equals(iceTransport.getRemoteParameters(), null);
+}, `start() throws if closed`);
+
+test(() => {
+ const iceTransport = new RTCIceTransport();
+ assert_throws_js(TypeError, () => iceTransport.start({}));
+ assert_throws_js(TypeError,
+ () => iceTransport.start({ usernameFragment: ICE_UFRAG }));
+ assert_throws_js(TypeError,
+ () => iceTransport.start({ password: ICE_PWD }));
+ assert_equals(iceTransport.getRemoteParameters(), null);
+}, 'start() throws if usernameFragment or password not set');
+
+test(() => {
+ const TEST_CASES = [
+ {usernameFragment: '2sh', description: 'less than 4 characters long'},
+ {
+ usernameFragment: 'x'.repeat(257),
+ description: 'greater than 256 characters long',
+ },
+ {usernameFragment: '123\n', description: 'illegal character'},
+ ];
+ for (const {usernameFragment, description} of TEST_CASES) {
+ const iceTransport = new RTCIceTransport();
+ assert_throws_dom(
+ 'SyntaxError',
+ () => iceTransport.start({ usernameFragment, password: ICE_PWD }),
+ `illegal usernameFragment (${description}) should throw a SyntaxError`);
+ }
+}, 'start() throws if usernameFragment does not conform to syntax');
+
+test(() => {
+ const TEST_CASES = [
+ {password: 'x'.repeat(21), description: 'less than 22 characters long'},
+ {
+ password: 'x'.repeat(257),
+ description: 'greater than 256 characters long',
+ },
+ {password: ('x'.repeat(21) + '\n'), description: 'illegal character'},
+ ];
+ for (const {password, description} of TEST_CASES) {
+ const iceTransport = new RTCIceTransport();
+ assert_throws_dom(
+ 'SyntaxError',
+ () => iceTransport.start({ usernameFragment: ICE_UFRAG, password }),
+ `illegal password (${description}) should throw a SyntaxError`);
+ }
+}, 'start() throws if password does not conform to syntax');
+
+const assert_ice_parameters_equals = (a, b) => {
+ assert_equals(a.usernameFragment, b.usernameFragment,
+ 'usernameFragments are equal');
+ assert_equals(a.password, b.password, 'passwords are equal');
+};
+
+test(t => {
+ const iceTransport = makeIceTransport(t);
+ iceTransport.start(dummyRemoteParameters);
+ assert_equals(iceTransport.state, 'new');
+ assert_ice_parameters_equals(iceTransport.getRemoteParameters(),
+ dummyRemoteParameters);
+}, `start() does not transition state to 'checking' if no remote candidates ` +
+ 'added');
+
+test(t => {
+ const iceTransport = makeIceTransport(t);
+ iceTransport.start(dummyRemoteParameters);
+ assert_equals(iceTransport.role, 'controlled');
+}, `start() with default role sets role attribute to 'controlled'`);
+
+test(t => {
+ const iceTransport = makeIceTransport(t);
+ iceTransport.start(dummyRemoteParameters, 'controlling');
+ assert_equals(iceTransport.role, 'controlling');
+}, `start() sets role attribute to 'controlling'`);
+
+const candidate1 = new RTCIceCandidate({
+ candidate: 'candidate:1 1 udp 2113929471 203.0.113.100 10100 typ host',
+ sdpMid: '',
+});
+
+test(() => {
+ const iceTransport = new RTCIceTransport();
+ iceTransport.stop();
+ assert_throws_dom('InvalidStateError',
+ () => iceTransport.addRemoteCandidate(candidate1));
+ assert_array_equals(iceTransport.getRemoteCandidates(), []);
+}, 'addRemoteCandidate() throws if closed');
+
+test(() => {
+ const iceTransport = new RTCIceTransport();
+ assert_throws_dom('OperationError',
+ () => iceTransport.addRemoteCandidate(
+ new RTCIceCandidate({ candidate: 'invalid', sdpMid: '' })));
+ assert_array_equals(iceTransport.getRemoteCandidates(), []);
+}, 'addRemoteCandidate() throws on invalid candidate');
+
+test(t => {
+ const iceTransport = makeIceTransport(t);
+ iceTransport.addRemoteCandidate(candidate1);
+ iceTransport.start(dummyRemoteParameters);
+ assert_equals(iceTransport.state, 'checking');
+ assert_array_equals(iceTransport.getRemoteCandidates(), [candidate1]);
+}, `start() transitions state to 'checking' if one remote candidate had been ` +
+ 'added');
+
+test(t => {
+ const iceTransport = makeIceTransport(t);
+ iceTransport.start(dummyRemoteParameters);
+ iceTransport.addRemoteCandidate(candidate1);
+ assert_equals(iceTransport.state, 'checking');
+ assert_array_equals(iceTransport.getRemoteCandidates(), [candidate1]);
+}, `addRemoteCandidate() transitions state to 'checking' if start() had been ` +
+ 'called before');
+
+test(t => {
+ const iceTransport = makeIceTransport(t);
+ iceTransport.start(dummyRemoteParameters);
+ assert_throws_dom('InvalidStateError',
+ () => iceTransport.start(dummyRemoteParameters, 'controlling'));
+}, 'start() throws if later called with a different role');
+
+test(t => {
+ const iceTransport = makeIceTransport(t);
+ iceTransport.start({
+ usernameFragment: '1'.repeat(4),
+ password: '1'.repeat(22),
+ });
+ iceTransport.addRemoteCandidate(candidate1);
+ const changedRemoteParameters = {
+ usernameFragment: '2'.repeat(4),
+ password: '2'.repeat(22),
+ };
+ iceTransport.start(changedRemoteParameters);
+ assert_equals(iceTransport.state, 'new');
+ assert_array_equals(iceTransport.getRemoteCandidates(), []);
+ assert_ice_parameters_equals(iceTransport.getRemoteParameters(),
+ changedRemoteParameters);
+}, `start() flushes remote candidates and transitions state to 'new' if ` +
+ 'later called with different remote parameters');
+
+promise_test(async t => {
+ const [ localTransport, remoteTransport ] =
+ makeGatherAndStartTwoIceTransports(t);
+ const localWatcher = new EventWatcher(t, localTransport, 'statechange');
+ const remoteWatcher = new EventWatcher(t, remoteTransport, 'statechange');
+ await Promise.all([
+ localWatcher.wait_for('statechange').then(() => {
+ assert_equals(localTransport.state, 'connected');
+ }),
+ remoteWatcher.wait_for('statechange').then(() => {
+ assert_equals(remoteTransport.state, 'connected');
+ }),
+ ]);
+}, 'Two RTCIceTransports connect to each other');
+
+['controlling', 'controlled'].forEach(role => {
+ promise_test(async t => {
+ const [ localTransport, remoteTransport ] =
+ makeAndGatherTwoIceTransports(t);
+ localTransport.start(remoteTransport.getLocalParameters(), role);
+ remoteTransport.start(localTransport.getLocalParameters(), role);
+ const localWatcher = new EventWatcher(t, localTransport, 'statechange');
+ const remoteWatcher = new EventWatcher(t, remoteTransport, 'statechange');
+ await Promise.all([
+ localWatcher.wait_for('statechange').then(() => {
+ assert_equals(localTransport.state, 'connected');
+ }),
+ remoteWatcher.wait_for('statechange').then(() => {
+ assert_equals(remoteTransport.state, 'connected');
+ }),
+ ]);
+ }, `Two RTCIceTransports configured with the ${role} role resolve the ` +
+ 'conflict in band and still connect.');
+});
+
+promise_test(async t => {
+ async function waitForSelectedCandidatePairChangeThenConnected(t, transport,
+ transportName) {
+ const watcher = new EventWatcher(t, transport,
+ [ 'statechange', 'selectedcandidatepairchange' ]);
+ await watcher.wait_for('selectedcandidatepairchange');
+ const selectedCandidatePair = transport.getSelectedCandidatePair();
+ assert_not_equals(selectedCandidatePair, null,
+ `${transportName} selected candidate pair should not be null once ` +
+ 'the selectedcandidatepairchange event fires');
+ assert_true(
+ transport.getLocalCandidates().some(
+ ({ candidate }) =>
+ candidate === selectedCandidatePair.local.candidate),
+ `${transportName} selected candidate pair local should be in the ` +
+ 'list of local candidates');
+ assert_true(
+ transport.getRemoteCandidates().some(
+ ({ candidate }) =>
+ candidate === selectedCandidatePair.remote.candidate),
+ `${transportName} selected candidate pair local should be in the ` +
+ 'list of remote candidates');
+ await watcher.wait_for('statechange');
+ assert_equals(transport.state, 'connected',
+ `${transportName} state should be 'connected'`);
+ }
+ const [ localTransport, remoteTransport ] =
+ makeGatherAndStartTwoIceTransports(t);
+ await Promise.all([
+ waitForSelectedCandidatePairChangeThenConnected(t, localTransport,
+ 'local transport'),
+ waitForSelectedCandidatePairChangeThenConnected(t, remoteTransport,
+ 'remote transport'),
+ ]);
+}, 'Selected candidate pair changes once the RTCIceTransports connect.');
+
+promise_test(async t => {
+ const [ transport, ] = makeGatherAndStartTwoIceTransports(t);
+ const watcher = new EventWatcher(t, transport, 'selectedcandidatepairchange');
+ await watcher.wait_for('selectedcandidatepairchange');
+ transport.stop();
+ assert_equals(transport.getSelectedCandidatePair(), null);
+}, 'getSelectedCandidatePair() returns null once the RTCIceTransport is ' +
+ 'stopped.');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc-identity/META.yml b/testing/web-platform/tests/webrtc-identity/META.yml
new file mode 100644
index 0000000000..fb919db954
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-identity/META.yml
@@ -0,0 +1,4 @@
+spec: https://w3c.github.io/webrtc-identity/identity.html
+suggested_reviewers:
+ - martinthomson
+ - jan-ivar
diff --git a/testing/web-platform/tests/webrtc-identity/RTCPeerConnection-constructor.html b/testing/web-platform/tests/webrtc-identity/RTCPeerConnection-constructor.html
new file mode 100644
index 0000000000..e7b7016338
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-identity/RTCPeerConnection-constructor.html
@@ -0,0 +1,11 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerConnection constructor</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script>
+test(() => {
+ const toStringThrows = { toString: function() { throw new Error; } };
+ assert_throws_js(Error, () => new RTCPeerConnection({ peerIdentity: toStringThrows }));
+}, "RTCPeerConnection constructor throws if the given peerIdentity getter throws");
+</script>
diff --git a/testing/web-platform/tests/webrtc-identity/RTCPeerConnection-getIdentityAssertion.sub.https.html b/testing/web-platform/tests/webrtc-identity/RTCPeerConnection-getIdentityAssertion.sub.https.html
new file mode 100644
index 0000000000..57d7b16165
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-identity/RTCPeerConnection-getIdentityAssertion.sub.https.html
@@ -0,0 +1,397 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerConnection.prototype.getIdentityAssertion</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="identity-helper.sub.js"></script>
+<script>
+ 'use strict';
+
+ // Test is based on the following editor draft:
+ // https://w3c.github.io/webrtc-pc/archives/20170605/webrtc.html
+
+ // The tests here interacts with the mock identity provider located at
+ // /.well-known/idp-proxy/mock-idp.js
+
+ // The following helper functions are called from identity-helper.sub.js
+ // parseAssertionResult
+ // getIdpDomains
+ // assert_rtcerror_rejection
+ // hostString
+
+ /*
+ 9.6. RTCPeerConnection Interface Extensions
+ partial interface RTCPeerConnection {
+ void setIdentityProvider(DOMString provider,
+ optional RTCIdentityProviderOptions options);
+ Promise<DOMString> getIdentityAssertion();
+ readonly attribute Promise<RTCIdentityAssertion> peerIdentity;
+ readonly attribute DOMString? idpLoginUrl;
+ readonly attribute DOMString? idpErrorInfo;
+ };
+
+ dictionary RTCIdentityProviderOptions {
+ DOMString protocol = "default";
+ DOMString usernameHint;
+ DOMString peerIdentity;
+ };
+ */
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+ const port = window.location.port;
+
+ const [idpDomain] = getIdpDomains();
+ const idpHost = hostString(idpDomain, port);
+
+ pc.setIdentityProvider(idpHost, {
+ protocol: 'mock-idp.js?foo=bar',
+ usernameHint: `alice@${idpDomain}`,
+ peerIdentity: 'bob@example.org'
+ });
+
+ return pc.getIdentityAssertion()
+ .then(assertionResultStr => {
+ const { idp, assertion } = parseAssertionResult(assertionResultStr);
+
+ assert_equals(idp.domain, idpHost,
+ 'Expect mock-idp.js to construct domain from its location.host');
+
+ assert_equals(idp.protocol, 'mock-idp.js',
+ 'Expect mock-idp.js to return protocol of itself with no query string');
+
+ const {
+ watermark,
+ args,
+ env,
+ query,
+ } = assertion;
+
+ assert_equals(watermark, 'mock-idp.js.watermark',
+ 'Expect assertion result to contain watermark left by mock-idp.js');
+
+ assert_equals(args.origin, window.origin,
+ 'Expect args.origin argument to be the origin of this window');
+
+ assert_equals(env.location.href,
+ `https://${idpHost}/.well-known/idp-proxy/mock-idp.js?foo=bar`,
+ 'Expect IdP proxy to be loaded with full well-known URL constructed from provider and protocol');
+
+ assert_equals(env.location.origin, `https://${idpHost}`,
+ 'Expect IdP to have its own origin');
+
+ assert_equals(args.options.protocol, 'mock-idp.js?foo=bar',
+ 'Expect options.protocol to be the same value as being passed from here');
+
+ assert_equals(args.options.usernameHint, `alice@${idpDomain}`,
+ 'Expect options.usernameHint to be the same value as being passed from here');
+
+ assert_equals(args.options.peerIdentity, 'bob@example.org',
+ 'Expect options.peerIdentity to be the same value as being passed from here');
+
+ assert_equals(query.foo, 'bar',
+ 'Expect query string to be parsed by mock-idp.js and returned back');
+ });
+ }, 'getIdentityAssertion() should load IdP proxy and return assertion generated');
+
+ // When generating assertion, the RTCPeerConnection doesn't care if the returned assertion
+ // represents identity of different domain
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+ const port = window.location.port;
+
+ const [idpDomain1, idpDomain2] = getIdpDomains();
+ assert_not_equals(idpDomain1, idpDomain2,
+ 'Sanity check two idpDomains are different');
+
+ // Ask mock-idp.js to return a custom domain idpDomain2 and custom protocol foo
+ pc.setIdentityProvider(hostString(idpDomain1, port), {
+ protocol: `mock-idp.js?generatorAction=return-custom-idp&domain=${idpDomain2}&protocol=foo`,
+ usernameHint: `alice@${idpDomain2}`,
+ });
+
+ return pc.getIdentityAssertion()
+ .then(assertionResultStr => {
+ const { idp, assertion } = parseAssertionResult(assertionResultStr);
+ assert_equals(idp.domain, idpDomain2);
+ assert_equals(idp.protocol, 'foo');
+ assert_equals(assertion.args.options.usernameHint, `alice@${idpDomain2}`);
+ });
+ }, 'getIdentityAssertion() should succeed if mock-idp.js return different domain and protocol in assertion');
+
+ /*
+ 9.3. Requesting Identity Assertions
+ 4. If the IdP proxy produces an error or returns a promise that does not resolve to
+ a valid RTCIdentityValidationResult (see 9.5 IdP Error Handling), then identity
+ validation fails.
+
+ 9.5. IdP Error Handling
+ - If an identity provider throws an exception or returns a promise that is ultimately
+ rejected, then the procedure that depends on the IdP MUST also fail. These types of
+ errors will cause an IdP failure with an RTCError with errorDetail set to
+ "idp-execution-failure".
+
+ 9.6. RTCPeerConnection Interface Extensions
+ idpErrorInfo
+ An attribute that the IdP can use to pass additional information back to the
+ applications about the error. The format of this string is defined by the IdP and
+ may be JSON.
+ */
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+
+ assert_equals(pc.idpErrorInfo, null,
+ 'Expect initial pc.idpErrorInfo to be null');
+
+ const port = window.location.port;
+ const [idpDomain] = getIdpDomains();
+
+ // Ask mock-idp.js to throw an error with err.errorInfo set to bar
+ pc.setIdentityProvider(hostString(idpDomain, port), {
+ protocol: `mock-idp.js?generatorAction=throw-error&errorInfo=bar`,
+ usernameHint: `alice@${idpDomain}`,
+ });
+
+ return assert_rtcerror_rejection('idp-execution-failure',
+ pc.getIdentityAssertion())
+ .then(() => {
+ assert_equals(pc.idpErrorInfo, 'bar',
+ 'Expect pc.idpErrorInfo to be set to the err.idpErrorInfo thrown by mock-idp.js');
+ });
+ }, `getIdentityAssertion() should reject with RTCError('idp-execution-failure') if mock-idp.js throws error`);
+
+ /*
+ 9.5. IdP Error Handling
+ - If the script loaded from the identity provider is not valid JavaScript or does
+ not implement the correct interfaces, it causes an IdP failure with an RTCError
+ with errorDetail set to "idp-bad-script-failure".
+ */
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+
+ const port = window.location.port;
+ const [idpDomain] = getIdpDomains();
+
+ // Ask mock-idp.js to not register its callback to the
+ // RTCIdentityProviderRegistrar
+ pc.setIdentityProvider(hostString(idpDomain, port), {
+ protocol: `mock-idp.js?action=do-not-register`,
+ usernameHint: `alice@${idpDomain}`,
+ });
+
+ return assert_rtcerror_rejection('idp-bad-script-failure',
+ pc.getIdentityAssertion());
+
+ }, `getIdentityAssertion() should reject with RTCError('idp-bad-script-failure') if IdP proxy script do not register its callback`);
+
+ /*
+ 9.3. Requesting Identity Assertions
+ 4. If the IdP proxy produces an error or returns a promise that does not resolve
+ to a valid RTCIdentityAssertionResult (see 9.5 IdP Error Handling), then assertion
+ generation fails.
+ */
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+
+ const port = window.location.port;
+ const [idpDomain] = getIdpDomains();
+
+ // Ask mock-idp.js to return an invalid result that is not proper
+ // RTCIdentityAssertionResult
+ pc.setIdentityProvider(hostString(idpDomain, port), {
+ protocol: `mock-idp.js?generatorAction=return-invalid-result`,
+ usernameHint: `alice@${idpDomain}`,
+ });
+
+ return promise_rejects_dom(t, 'OperationError',
+ pc.getIdentityAssertion());
+ }, `getIdentityAssertion() should reject with OperationError if mock-idp.js return invalid result`);
+
+ /*
+ 9.5. IdP Error Handling
+ - A RTCPeerConnection might be configured with an identity provider, but loading of
+ the IdP URI fails. Any procedure that attempts to invoke such an identity provider
+ and cannot load the URI fails with an RTCError with errorDetail set to
+ "idp-load-failure" and the httpRequestStatusCode attribute of the error set to the
+ HTTP status code of the response.
+ */
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+
+ pc.setIdentityProvider('nonexistent.{{domains[]}}', {
+ protocol: `non-existent`,
+ usernameHint: `alice@example.org`,
+ });
+
+ return assert_rtcerror_rejection('idp-load-failure',
+ pc.getIdentityAssertion());
+ }, `getIdentityAssertion() should reject with RTCError('idp-load-failure') if IdP cannot be loaded`);
+
+ /*
+ 9.3.1. User Login Procedure
+ Rejecting the promise returned by generateAssertion will cause the error to
+ propagate to the application. Login errors are indicated by rejecting the
+ promise with an RTCError with errorDetail set to "idp-need-login".
+
+ The URL to login at will be passed to the application in the idpLoginUrl
+ attribute of the RTCPeerConnection.
+
+ 9.5. IdP Error Handling
+ - If the identity provider requires the user to login, the operation will fail
+ RTCError with errorDetail set to "idp-need-login" and the idpLoginUrl attribute
+ of the error set to the URL that can be used to login.
+ */
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+
+ assert_equals(pc.idpLoginUrl, null,
+ 'Expect initial pc.idpLoginUrl to be null');
+
+ const port = window.location.port;
+ const [idpDomain] = getIdpDomains();
+ const idpHost = hostString(idpDomain, port);
+
+ pc.setIdentityProvider(idpHost, {
+ protocol: `mock-idp.js?generatorAction=require-login`,
+ usernameHint: `alice@${idpDomain}`,
+ });
+
+ return assert_rtcerror_rejection('idp-need-login',
+ pc.getIdentityAssertion())
+ .then(err => {
+ assert_equals(err.idpLoginUrl, `https://${idpHost}/login`,
+ 'Expect err.idpLoginUrl to be set to url set by mock-idp.js');
+
+ assert_equals(pc.idpLoginUrl, `https://${idpHost}/login`,
+ 'Expect pc.idpLoginUrl to be set to url set by mock-idp.js');
+
+ assert_equals(pc.idpErrorInfo, 'login required',
+ 'Expect pc.idpErrorInfo to be set to info set by mock-idp.js');
+ });
+ }, `getIdentityAssertion() should reject with RTCError('idp-need-login') when mock-idp.js requires login`);
+
+ /*
+ RTCIdentityProviderOptions Members
+ peerIdentity
+ The identity of the peer. For identity providers that bind their assertions to a
+ particular pair of communication peers, this allows them to generate an assertion
+ that includes both local and remote identities. If this value is omitted, but a
+ value is provided for the peerIdentity member of RTCConfiguration, the value from
+ RTCConfiguration is used.
+ */
+ promise_test(t => {
+ const pc = new RTCPeerConnection({
+ peerIdentity: 'bob@example.net'
+ });
+
+ const port = window.location.port;
+ const [idpDomain] = getIdpDomains();
+ const idpHost = hostString(idpDomain, port);
+
+ pc.setIdentityProvider(idpHost, {
+ protocol: 'mock-idp.js'
+ });
+
+ return pc.getIdentityAssertion()
+ .then(assertionResultStr => {
+ const { assertion } = parseAssertionResult(assertionResultStr);
+ assert_equals(assertion.args.options.peerIdentity, 'bob@example.net');
+ });
+ }, 'setIdentityProvider() with no peerIdentity provided should use peerIdentity value from getConfiguration()');
+
+ /*
+ 9.6. setIdentityProvider
+ 3. If any identity provider value has changed, discard any stored identity assertion.
+ */
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+ const port = window.location.port;
+ const [idpDomain] = getIdpDomains();
+ const idpHost = hostString(idpDomain, port);
+
+ pc.setIdentityProvider(idpHost, {
+ protocol: 'mock-idp.js?mark=first'
+ });
+
+ return pc.getIdentityAssertion()
+ .then(assertionResultStr => {
+ const { assertion } = parseAssertionResult(assertionResultStr);
+ assert_equals(assertion.query.mark, 'first');
+
+ pc.setIdentityProvider(idpHost, {
+ protocol: 'mock-idp.js?mark=second'
+ });
+
+ return pc.getIdentityAssertion();
+ })
+ .then(assertionResultStr => {
+ const { assertion } = parseAssertionResult(assertionResultStr);
+ assert_equals(assertion.query.mark, 'second',
+ 'Expect generated assertion is from second IdP config');
+ });
+ }, `Calling setIdentityProvider() multiple times should reset identity assertions`);
+
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+ const port = window.location.port;
+ const [idpDomain] = getIdpDomains();
+
+ pc.setIdentityProvider(hostString(idpDomain, port), {
+ protocol: 'mock-idp.js',
+ usernameHint: `alice@${idpDomain}`
+ });
+
+ return pc.getIdentityAssertion()
+ .then(assertionResultStr =>
+ pc.createOffer()
+ .then(offer => {
+ assert_true(offer.sdp.includes(`\r\na=identity:${assertionResultStr}`,
+ 'Expect SDP to have a=identity line containing assertion string'));
+ }));
+ }, 'createOffer() should return SDP containing identity assertion string if identity provider is set');
+
+ /*
+ 6. Requesting Identity Assertions
+
+ The identity assertion request process is triggered by a call to
+ createOffer, createAnswer, or getIdentityAssertion. When these calls are
+ invoked and an identity provider has been set, the following steps are
+ executed:
+
+ ...
+
+ If assertion generation fails, then the promise for the corresponding
+ function call is rejected with a newly created OperationError. */
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+ const port = window.location.port;
+ const [idpDomain] = getIdpDomains();
+
+ pc.setIdentityProvider(hostString(idpDomain, port), {
+ protocol: 'mock-idp.js?generatorAction=throw-error',
+ usernameHint: `alice@${idpDomain}`
+ });
+
+ return promise_rejects_dom(t, 'OperationError',
+ pc.createOffer());
+ }, 'createOffer() should reject with OperationError if identity assertion request fails');
+
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+ const port = window.location.port;
+ const [idpDomain] = getIdpDomains();
+
+ pc.setIdentityProvider(hostString(idpDomain, port), {
+ protocol: 'mock-idp.js?generatorAction=throw-error',
+ usernameHint: `alice@${idpDomain}`
+ });
+
+ return new RTCPeerConnection()
+ .createOffer()
+ .then(offer => pc.setRemoteDescription(offer))
+ .then(() =>
+ promise_rejects_dom(t, 'OperationError',
+ pc.createAnswer()));
+
+ }, 'createAnswer() should reject with OperationError if identity assertion request fails');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc-identity/RTCPeerConnection-peerIdentity.https.html b/testing/web-platform/tests/webrtc-identity/RTCPeerConnection-peerIdentity.https.html
new file mode 100644
index 0000000000..268e406211
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-identity/RTCPeerConnection-peerIdentity.https.html
@@ -0,0 +1,328 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerConnection.prototype.peerIdentity</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="identity-helper.sub.js"></script>
+<script>
+ 'use strict';
+
+ // Test is based on the following editor draft:
+ // https://w3c.github.io/webrtc-identity/identity.html
+
+ // The tests here interacts with the mock identity provider located at
+ // /.well-known/idp-proxy/mock-idp.js
+
+ // The following helper functions are called from identity-helper.sub.js
+ // parseAssertionResult
+ // getIdpDomains
+ // assert_rtcerror_rejection
+ // hostString
+
+ /*
+ 9.6. RTCPeerConnection Interface Extensions
+ partial interface RTCPeerConnection {
+ void setIdentityProvider(DOMString provider,
+ optional RTCIdentityProviderOptions options);
+ Promise<DOMString> getIdentityAssertion();
+ readonly attribute Promise<RTCIdentityAssertion> peerIdentity;
+ readonly attribute DOMString? idpLoginUrl;
+ readonly attribute DOMString? idpErrorInfo;
+ };
+
+ dictionary RTCIdentityProviderOptions {
+ DOMString protocol = "default";
+ DOMString usernameHint;
+ DOMString peerIdentity;
+ };
+
+ [Constructor(DOMString idp, DOMString name)]
+ interface RTCIdentityAssertion {
+ attribute DOMString idp;
+ attribute DOMString name;
+ };
+ */
+
+ /*
+ 4.3.2. setRemoteDescription
+ If an a=identity attribute is present in the session description, the browser
+ validates the identity assertion..
+
+ If the "peerIdentity" configuration is applied to the RTCPeerConnection, this
+ establishes a target peer identity of the provided value. Alternatively, if the
+ RTCPeerConnection has previously authenticated the identity of the peer (that
+ is, there is a current value for peerIdentity ), then this also establishes a
+ target peer identity.
+ */
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+
+ t.add_cleanup(() => pc2.close());
+
+ const port = window.location.port;
+ const [idpDomain] = getIdpDomains();
+ const idpHost = hostString(idpDomain, port);
+
+ pc1.setIdentityProvider(idpHost, {
+ protocol: 'mock-idp.js',
+ usernameHint: `alice@${idpDomain}`
+ });
+
+ const peerIdentity = pc2.peerIdentity;
+ await pc2.setRemoteDescription(await pc1.createOffer());
+ const { idp, name } = await peerIdentity;
+ assert_equals(idp, idpHost, `Expect IdP to be ${idpHost}`);
+ assert_equals(name, `alice@${idpDomain}`,
+ `Expect validated identity from mock-idp.js to be same as specified in usernameHint`);
+ }, 'setRemoteDescription() on offer with a=identity should establish peerIdentity');
+
+ promise_test(async t => {
+ const port = window.location.port;
+ const [idpDomain] = getIdpDomains();
+ const idpHost = hostString(idpDomain, port);
+
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ pc1.setIdentityProvider(idpHost, {
+ protocol: 'mock-idp.js',
+ usernameHint: `doesnt_matter@${idpDomain}`
+ });
+
+ const pc2 = new RTCPeerConnection({
+ peerIdentity: `bob@${idpDomain}`
+ });
+
+ t.add_cleanup(() => pc2.close());
+
+ pc2.setIdentityProvider(idpHost, {
+ protocol: 'mock-idp.js',
+ usernameHint: `alice@${idpDomain}`
+ });
+
+ const offer = await pc1.createOffer();
+
+ await promise_rejects_dom(t, 'OperationError',
+ pc2.setRemoteDescription(offer));
+ await promise_rejects_dom(t, 'OperationError',
+ pc2.peerIdentity);
+ }, 'setRemoteDescription() on offer with a=identity that resolve to value different from target peer identity should reject with OperationError');
+
+ /*
+ 9.4. Verifying Identity Assertions
+ 8. The RTCPeerConnection decodes the contents and validates that it contains a
+ fingerprint value for every a=fingerprint attribute in the session description.
+ This ensures that the certificate used by the remote peer for communications
+ is covered by the identity assertion.
+
+ If identity validation fails, the peerIdentity promise is rejected with a newly
+ created OperationError.
+
+ If identity validation fails and there is a target peer identity for the
+ RTCPeerConnection, the promise returned by setRemoteDescription MUST be rejected
+ with the same DOMException.
+ */
+ promise_test(t => {
+ const port = window.location.port;
+ const [idpDomain] = getIdpDomains();
+ const idpHost = hostString(idpDomain, port);
+
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection({
+ peerIdentity: `alice@${idpDomain}`
+ });
+
+ t.add_cleanup(() => pc2.close());
+
+ // Ask mockidp.js to return custom contents in validation result
+ pc1.setIdentityProvider(idpHost, {
+ protocol: 'mock-idp.js?validatorAction=return-custom-contents&contents=bogus',
+ usernameHint: `alice@${idpDomain}`
+ });
+
+ const peerIdentityPromise = pc2.peerIdentity;
+
+ return pc1.createOffer()
+ .then(offer => Promise.all([
+ promise_rejects_dom(t, 'IdpError',
+ pc2.setRemoteDescription(offer)),
+ promise_rejects_dom(t, 'OperationError',
+ peerIdentityPromise)
+ ]));
+ }, 'setRemoteDescription() with peerIdentity set and with IdP proxy that return validationAssertion with mismatch contents should reject with OperationError');
+
+ /*
+ 9.4. Verifying Identity Assertions
+ 9. The RTCPeerConnection validates that the domain portion of the identity matches
+ the domain of the IdP as described in [RTCWEB-SECURITY-ARCH]. If this check
+ fails then the identity validation fails.
+ */
+ promise_test(t => {
+ const port = window.location.port;
+ const [idpDomain1, idpDomain2] = getIdpDomains();
+ assert_not_equals(idpDomain1, idpDomain2,
+ 'Sanity check two idpDomains are different');
+
+ const idpHost1 = hostString(idpDomain1, port);
+
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection({
+ peerIdentity: `alice@${idpDomain2}`
+ });
+
+ t.add_cleanup(() => pc2.close());
+
+ // mock-idp.js will return assertion of domain2 identity
+ // with domain1 in the idp.domain field
+ pc1.setIdentityProvider(idpHost1, {
+ protocol: 'mock-idp.js',
+ usernameHint: `alice@${idpDomain2}`
+ });
+
+ return pc1.getIdentityAssertion()
+ .then(assertionResultStr => {
+ const { idp, assertion } = parseAssertionResult(assertionResultStr);
+
+ assert_equals(idp.domain, idpHost1,
+ 'Sanity check domain of assertion is host1');
+
+ assert_equals(assertion.args.options.usernameHint, `alice@${idpDomain2}`,
+ 'Sanity check domain1 is going to validate a domain2 identity');
+
+ return pc1.createOffer();
+ })
+ .then(offer => Promise.all([
+ promise_rejects_dom(t, 'OperationError',
+ pc2.setRemoteDescription(offer)),
+ promise_rejects_dom(t, 'OperationError',
+ pc2.peerIdentity)
+ ]));
+ }, 'setRemoteDescription() and peerIdentity should reject with OperationError if IdP return validated identity that is different from its own domain');
+
+ /*
+ 9.4 Verifying Identity Assertions
+ If identity validation fails and there is a target peer identity for the
+ RTCPeerConnection, the promise returned by setRemoteDescription MUST be rejected
+ with the same DOMException.
+
+ 9.5 IdP Error Handling
+ - If an identity provider throws an exception or returns a promise that is ultimately
+ rejected, then the procedure that depends on the IdP MUST also fail. These types of
+ errors will cause an IdP failure with an RTCError with errorDetail set to
+ "idp-execution-failure".
+
+ Any error generated by the IdP MAY provide additional information in the
+ idpErrorInfo attribute. The information in this string is defined by the
+ IdP in use.
+ */
+ promise_test(t => {
+ const port = window.location.port;
+ const [idpDomain] = getIdpDomains();
+ const idpHost = hostString(idpDomain, port);
+
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection({
+ peerIdentity: `alice@${idpDomain}`
+ });
+
+ t.add_cleanup(() => pc2.close());
+
+ // Ask mock-idp.js to throw error during validation,
+ // i.e. during pc2.setRemoteDescription()
+ pc1.setIdentityProvider(idpHost, {
+ protocol: 'mock-idp.js?validatorAction=throw-error&errorInfo=bar',
+ usernameHint: `alice@${idpDomain}`
+ });
+
+ return pc1.createOffer()
+ .then(offer => Promise.all([
+ assert_rtcerror_rejection('idp-execution-failure',
+ pc2.setRemoteDescription(offer)),
+ assert_rtcerror_rejection('idp-execution-failure',
+ pc2.peerIdentity)
+ ]))
+ .then(() => {
+ assert_equals(pc2.idpErrorInfo, 'bar',
+ 'Expect pc2.idpErrorInfo to be set to the err.idpErrorInfo thrown by mock-idp.js');
+ });
+ }, `When IdP throws error and pc has target peer identity, setRemoteDescription() and peerIdentity rejected with RTCError('idp-execution-error')`);
+
+ /*
+ 4.3.2. setRemoteDescription
+ If there is no target peer identity, then setRemoteDescription does not await the
+ completion of identity validation.
+
+ 9.5. IdP Error Handling
+ - If an identity provider throws an exception or returns a promise that is
+ ultimately rejected, then the procedure that depends on the IdP MUST also fail.
+ These types of errors will cause an IdP failure with an RTCError with errorDetail
+ set to "idp-execution-failure".
+
+ 9.4. Verifying Identity Assertions
+ If identity validation fails and there is no a target peer identity, the value of
+ the peerIdentity MUST be set to a new, unresolved promise instance. This permits
+ the use of renegotiation (or a subsequent answer, if the session description was
+ a provisional answer) to resolve or reject the identity.
+ */
+ promise_test(t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+
+ t.add_cleanup(() => pc2.close());
+
+ const port = window.location.port;
+ const [idpDomain] = getIdpDomains();
+ const idpHost = hostString(idpDomain, port);
+
+ // Ask mock-idp.js to throw error during validation,
+ // i.e. during pc2.setRemoteDescription()
+ pc1.setIdentityProvider(idpHost, {
+ protocol: 'mock-idp.js?validatorAction=throw-error',
+ usernameHint: `alice@${idpDomain}`
+ });
+
+ const peerIdentityPromise1 = pc2.peerIdentity;
+
+ return pc1.createOffer()
+ .then(offer =>
+ // setRemoteDescription should succeed because there is no target peer identity set
+ pc2.setRemoteDescription(offer))
+ .then(() =>
+ assert_rtcerror_rejection('idp-execution-failure',
+ peerIdentityPromise1,
+ `Expect first peerIdentity promise to be rejected with RTCError('idp-execution-failure')`))
+ .then(() => {
+ const peerIdentityPromise2 = pc2.peerIdentity;
+ assert_not_equals(peerIdentityPromise2, peerIdentityPromise1,
+ 'Expect pc2.peerIdentity to be replaced with a fresh unresolved promise');
+
+ // regenerate an identity assertion with no test option to throw error
+ pc1.setIdentityProvider(idpHost, {
+ protocol: 'idp-test.js',
+ usernameHint: `alice@${idpDomain}`
+ });
+
+ return pc1.createOffer()
+ .then(offer => pc2.setRemoteDescription(offer))
+ .then(peerIdentityPromise2)
+ .then(identityAssertion => {
+ const { idp, name } = identityAssertion;
+
+ assert_equals(idp, idpDomain,
+ `Expect IdP domain to be ${idpDomain}`);
+
+ assert_equals(name, `alice@${idpDomain}`,
+ `Expect validated identity to be alice@${idpDomain}`);
+
+ assert_equals(pc2.peeridentity, peerIdentityPromise2,
+ 'Expect pc2.peerIdentity to stay fixed after identity is validated');
+ });
+ });
+ }, 'IdP failure with no target peer identity should have following setRemoteDescription() succeed and replace pc.peerIdentity with a new promise');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc-identity/identity-helper.sub.js b/testing/web-platform/tests/webrtc-identity/identity-helper.sub.js
new file mode 100644
index 0000000000..90363662f7
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-identity/identity-helper.sub.js
@@ -0,0 +1,70 @@
+'use strict';
+
+/*
+ In web-platform-test, a number of domains are required to be set up locally.
+ The list is available at docs/_writing-tests/server-features.md. The
+ appropriate hosts file entries can be generated with the WPT CLI via the
+ following command: `wpt make-hosts-file`.
+ */
+
+/*
+ dictionary RTCIdentityProviderDetails {
+ required DOMString domain;
+ DOMString protocol = "default";
+ };
+ */
+
+// Parse a base64 JSON encoded string returned from getIdentityAssertion().
+// This is also the string that is set in the a=identity line.
+// Returns a { idp, assertion } where idp is of type RTCIdentityProviderDetails
+// and assertion is the deserialized JSON that was returned by the
+// IdP proxy's generateAssertion() function.
+function parseAssertionResult(assertionResultStr) {
+ const assertionResult = JSON.parse(atob(assertionResultStr));
+
+ const { idp } = assertionResult;
+ const assertion = JSON.parse(assertionResult.assertion);
+
+ return { idp, assertion };
+}
+
+// Return two distinct IdP domains that are different from current domain
+function getIdpDomains() {
+ const domainA = '{{domains[www]}}';
+ const domainB = '{{domains[www1]}}';
+ const domainC = '{{domains[www2]}}';
+
+ if(window.location.hostname === domainA) {
+ return [domainB, domainC];
+ } else if(window.location.hostname === domainB) {
+ return [domainA, domainC];
+ } else {
+ return [domainA, domainB];
+ }
+}
+
+function assert_rtcerror_rejection(errorDetail, promise, desc) {
+ return promise.then(
+ res => {
+ assert_unreached(`Expect promise to be rejected with RTCError, but instead got ${res}`);
+ }, err => {
+ assert_true(err instanceof RTCError,
+ 'Expect error object to be instance of RTCError');
+
+ assert_equals(err.errorDetail, errorDetail,
+ `Expect RTCError object have errorDetail set to ${errorDetail}`);
+
+ return err;
+ });
+}
+
+// construct a host string consist of domain and optionally port
+// If the default HTTP/HTTPS port is used, window.location.port returns
+// empty string.
+function hostString(domain, port) {
+ if(port === '') {
+ return domain;
+ } else {
+ return `${domain}:${port}`;
+ }
+}
diff --git a/testing/web-platform/tests/webrtc-identity/idlharness.https.window.js b/testing/web-platform/tests/webrtc-identity/idlharness.https.window.js
new file mode 100644
index 0000000000..8eb60c960a
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-identity/idlharness.https.window.js
@@ -0,0 +1,24 @@
+// META: script=/resources/WebIDLParser.js
+// META: script=/resources/idlharness.js
+
+'use strict';
+
+idl_test(
+ ['webrtc-identity'],
+ ['webrtc', 'mediacapture-streams', 'html', 'dom', 'webidl'],
+ async idlArray => {
+ idlArray.add_objects({
+ RTCPeerConnection: [`new RTCPeerConnection()`],
+ RTCIdentityAssertion: [`new RTCIdentityAssertion('idp', 'name')`],
+ MediaStreamTrack: ['track'],
+ // TODO: RTCIdentityProviderGlobalScope
+ // TODO: RTCIdentityProviderRegistrar
+ });
+
+ try {
+ self.track = await navigator.mediaDevices
+ .getUserMedia({audio: true})
+ .then(m => m.getTracks()[0]);
+ } catch (e) {}
+ }
+);
diff --git a/testing/web-platform/tests/webrtc-priority/META.yml b/testing/web-platform/tests/webrtc-priority/META.yml
new file mode 100644
index 0000000000..a422e81447
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-priority/META.yml
@@ -0,0 +1 @@
+spec: https://w3c.github.io/webrtc-priority/
diff --git a/testing/web-platform/tests/webrtc-priority/RTCPeerConnection-ondatachannel.html b/testing/web-platform/tests/webrtc-priority/RTCPeerConnection-ondatachannel.html
new file mode 100644
index 0000000000..e4b1e8d58a
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-priority/RTCPeerConnection-ondatachannel.html
@@ -0,0 +1,66 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerConnection.prototype.ondatachannel</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../webrtc/RTCPeerConnection-helper.js"></script>
+<script>
+'use strict';
+
+promise_test(async (t) => {
+ const resolver = new Resolver();
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+
+ const dc1 = pc1.createDataChannel('test', {
+ ordered: false,
+ maxRetransmits: 1,
+ protocol: 'custom',
+ priority: 'high'
+ });
+
+ assert_equals(dc1.priority, 'high');
+
+ pc2.ondatachannel = t.step_func((event) => {
+ const dc2 = event.channel;
+
+ assert_equals(dc2.priority, 'high');
+
+ resolver.resolve();
+ });
+
+ exchangeIceCandidates(pc1, pc2);
+ await exchangeOfferAnswer(pc1, pc2);
+
+ await resolver;
+}, 'In-band negotiated channel created on remote peer should match the same configuration as local ' +
+ 'peer');
+
+promise_test(async (t) => {
+ const resolver = new Resolver();
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+
+ const dc1 = pc1.createDataChannel('');
+
+ assert_equals(dc1.priority, 'low');
+
+ pc2.ondatachannel = t.step_func((event) => {
+ const dc2 = event.channel;
+ assert_equals(dc2.priority, 'low');
+
+ resolver.resolve();
+ });
+
+ exchangeIceCandidates(pc1, pc2);
+ await exchangeOfferAnswer(pc1, pc2);
+
+ await resolver;
+}, 'In-band negotiated channel created on remote peer should match the same (default) ' +
+ 'configuration as local peer');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc-priority/RTCRtpParameters-encodings.html b/testing/web-platform/tests/webrtc-priority/RTCRtpParameters-encodings.html
new file mode 100644
index 0000000000..1519ee84f7
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-priority/RTCRtpParameters-encodings.html
@@ -0,0 +1,44 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCRtpParameters encodings</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../webrtc/dictionary-helper.js"></script>
+<script src="../webrtc/RTCRtpParameters-helper.js"></script>
+<script>
+ 'use strict';
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const { sender } = pc.addTransceiver('video', {
+ sendEncodings: [{
+ active: false,
+ priority: 'low',
+ networkPriority: 'low',
+ maxBitrate: 8,
+ maxFramerate: 25,
+ rid: 'foo'
+ }]
+ });
+ await doOfferAnswerExchange(t, pc);
+
+ const param = sender.getParameters();
+ validateSenderRtpParameters(param);
+ const encoding = param.encodings[0];
+
+ assert_equals(encoding.active, false);
+ assert_equals(encoding.priority, 'low');
+ assert_equals(encoding.networkPriority, 'low');
+ }, `sender.getParameters() should return sendEncodings set by addTransceiver()`);
+
+ test_modified_encoding('audio', 'active', false, true,
+ 'setParameters() with modified encoding.active should succeed');
+
+ test_modified_encoding('audio', 'priority', 'very-low', 'high',
+ 'setParameters() with modified encoding.priority should succeed');
+
+ test_modified_encoding('audio', 'networkPriority', 'very-low', 'high',
+ 'setParameters() with modified encoding.networkPriority should succeed');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc-stats/META.yml b/testing/web-platform/tests/webrtc-stats/META.yml
new file mode 100644
index 0000000000..10bcf856eb
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-stats/META.yml
@@ -0,0 +1,5 @@
+spec: https://w3c.github.io/webrtc-stats/
+suggested_reviewers:
+ - henbos
+ - vr000m
+ - jan-ivar
diff --git a/testing/web-platform/tests/webrtc-stats/README.md b/testing/web-platform/tests/webrtc-stats/README.md
new file mode 100644
index 0000000000..2b69372894
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-stats/README.md
@@ -0,0 +1,7 @@
+The following 4 test cases in the `webrtc/` directory test some of the mandatory-to-implement stats defined in WebRTC Statistics:
+
+* `getstats.html`
+* `RTCPeerConnection-getStats.https.html`
+* `RTCPeerConnection-track-stats.https.html`
+* `RTCRtpReceiver-getStats.https.html`
+* `RTCRtpSender-getStats.https.html`
diff --git a/testing/web-platform/tests/webrtc-stats/getStats-remote-candidate-address.html b/testing/web-platform/tests/webrtc-stats/getStats-remote-candidate-address.html
new file mode 100644
index 0000000000..08e2aec90e
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-stats/getStats-remote-candidate-address.html
@@ -0,0 +1,81 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Exposure or remote candidate address on stats</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../webrtc/RTCPeerConnection-helper.js"></script>
+<script src="../webrtc/RTCStats-helper.js"></script>
+<script>
+ 'use strict';
+
+promise_test(async (test) => {
+ const localPc = new RTCPeerConnection();
+ test.add_cleanup(() => localPc.close());
+ const remotePc = new RTCPeerConnection();
+ test.add_cleanup(() => remotePc.close());
+
+ const promiseDataChannel = new Promise(resolve => {
+ remotePc.addEventListener('datachannel', (event) => {
+ resolve(event.channel);
+ });
+ });
+
+ const localDataChannel = localPc.createDataChannel('test');
+
+ localPc.addEventListener('icecandidate', event => {
+ if (event.candidate)
+ remotePc.addIceCandidate(event.candidate);
+ });
+ exchangeOfferAnswer(localPc, remotePc);
+
+ const remoteDataChannel = await promiseDataChannel;
+
+ localDataChannel.send("test");
+
+ await new Promise(resolve => {
+ remoteDataChannel.onmessage = resolve;
+ });
+
+ const remoteCandidateStats = getRequiredStats(await localPc.getStats(), "remote-candidate");
+ assert_equals(remoteCandidateStats.address, null, "address should be null");
+}, "Do not expose in stats remote addresses that are not known to be already exposed to JS");
+
+
+promise_test(async (test) => {
+ const localPc = new RTCPeerConnection();
+ test.add_cleanup(() => localPc.close());
+ const remotePc = new RTCPeerConnection();
+ test.add_cleanup(() => remotePc.close());
+
+ const promiseDataChannel = new Promise(resolve => {
+ remotePc.addEventListener('datachannel', (event) => {
+ resolve(event.channel);
+ });
+ });
+
+ const localDataChannel = localPc.createDataChannel('test');
+
+ localPc.addEventListener('icecandidate', event => {
+ if (event.candidate)
+ remotePc.addIceCandidate(event.candidate);
+ });
+ remotePc.addEventListener('icecandidate', event => {
+ if (event.candidate)
+ localPc.addIceCandidate(event.candidate);
+ });
+ exchangeOfferAnswer(localPc, remotePc);
+
+ const remoteDataChannel = await promiseDataChannel;
+
+ localDataChannel.send("test");
+
+ await new Promise(resolve => {
+ remoteDataChannel.onmessage = resolve;
+ });
+
+ const remoteCandidateStats = getRequiredStats(await localPc.getStats(), "remote-candidate");
+ assert_not_equals(remoteCandidateStats.address, null, "address should not be null");
+
+}, "Expose in stats remote addresses that are already exposed to JS");
+
+</script>
diff --git a/testing/web-platform/tests/webrtc-stats/hardware-capability-stats.https.html b/testing/web-platform/tests/webrtc-stats/hardware-capability-stats.https.html
new file mode 100644
index 0000000000..49f80d4b65
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-stats/hardware-capability-stats.https.html
@@ -0,0 +1,107 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Stats exposing hardware capability</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="../webrtc/RTCPeerConnection-helper.js"></script>
+<script src="../webrtc/RTCStats-helper.js"></script>
+<script>
+/*
+ * Test stats that expose hardware capabilities are only exposed according to
+ * the conditions described in https://w3c.github.io/webrtc-stats/#limiting-exposure-of-hardware-capabilities.
+ */
+'use strict';
+
+function getStatEntry(report, type, kind) {
+ const values = [...report.values()];
+ const for_kind = values.filter(
+ stat => stat.type == type && stat.kind == kind);
+
+ assert_equals(1, for_kind.length,
+ "Expected report to have only 1 entry with type '" + type +
+ "' and kind '" + kind + "'. Found values " + for_kind);
+ return for_kind[0];
+}
+
+async function hasEncodedAndDecodedFrames(pc, t) {
+ while (true) {
+ const report = await pc.getStats();
+ const inboundRtp = getStatEntry(report, 'inbound-rtp', 'video');
+ const outboundRtp = getStatEntry(report, 'outbound-rtp', 'video');
+ if (inboundRtp.framesDecoded > 0 && outboundRtp.framesEncoded > 0) {
+ return;
+ }
+ // Avoid any stats caching, which can otherwise make this an infinite loop.
+ await (new Promise(r => t.step_timeout(r, 100)));
+ }
+}
+
+async function setupPcAndGetStatEntry(
+ t, stream, type, kind, stat) {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+ for (const track of stream.getTracks()) {
+ pc1.addTrack(track, stream);
+ pc2.addTrack(track, stream);
+ t.add_cleanup(() => track.stop());
+ }
+
+ exchangeIceCandidates(pc1, pc2);
+ await exchangeOfferAnswer(pc1, pc2);
+ await hasEncodedAndDecodedFrames(pc1, t);
+ const report = await pc1.getStats();
+ return getStatEntry(report, type, kind);
+}
+
+for (const args of [
+ // RTCOutboundRtpStreamStats.powerEfficientEncoder
+ ['outbound-rtp', 'video', 'powerEfficientEncoder'],
+ // RTCOutboundRtpStreamStats.encoderImplementation
+ ['outbound-rtp', 'video', 'encoderImplementation'],
+ // RTCInboundRtpStreamStats.powerEfficientDecoder
+ ['inbound-rtp', 'video', 'powerEfficientDecoder'],
+ // RTCOutboundRtpStreamStats.decoderImplementation
+ ['inbound-rtp', 'video', 'decoderImplementation'],
+]) {
+ const type = args[0];
+ const kind = args[1];
+ const stat = args[2];
+
+ promise_test(async (t) => {
+ const stream = await getNoiseStream({video: true, audio: true});
+ const statsEntry = await setupPcAndGetStatEntry(t, stream, type, kind, stat);
+ assert_not_own_property(statsEntry, stat);
+ }, stat + " not exposed when not capturing.");
+
+ // Exposing hardware capabilities when a there is a fullscreen element was
+ // removed with https://github.com/w3c/webrtc-stats/pull/713.
+ promise_test(async (t) => {
+ const stream = await getNoiseStream({video: true, audio: true});
+
+ const element = document.getElementById('elementToFullscreen');
+ await test_driver.bless("fullscreen", () => element.requestFullscreen());
+ t.add_cleanup(() => document.exitFullscreen());
+
+ const statsEntry = await setupPcAndGetStatEntry(
+ t, stream, type, kind, stat);
+ assert_not_own_property(statsEntry, stat);
+ }, stat + " not exposed when fullscreen and not capturing.");
+
+ promise_test(async (t) => {
+ const stream = await navigator.mediaDevices.getUserMedia(
+ {video: true, audio: true});
+ const statsEntry = await setupPcAndGetStatEntry(
+ t, stream, type, kind, stat);
+ assert_own_property(statsEntry, stat);
+ }, stat + " exposed when capturing.");
+}
+
+</script>
+<body>
+ <div id="elementToFullscreen"></div>
+</body>
diff --git a/testing/web-platform/tests/webrtc-stats/idlharness.window.js b/testing/web-platform/tests/webrtc-stats/idlharness.window.js
new file mode 100644
index 0000000000..d98712fc48
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-stats/idlharness.window.js
@@ -0,0 +1,14 @@
+// META: script=/resources/WebIDLParser.js
+// META: script=/resources/idlharness.js
+
+'use strict';
+
+// https://w3c.github.io/webrtc-stats/
+
+idl_test(
+ ['webrtc-stats'],
+ ['webrtc'],
+ idl_array => {
+ // No interfaces to test
+ }
+);
diff --git a/testing/web-platform/tests/webrtc-stats/outbound-rtp.https.html b/testing/web-platform/tests/webrtc-stats/outbound-rtp.https.html
new file mode 100644
index 0000000000..ff87d54256
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-stats/outbound-rtp.https.html
@@ -0,0 +1,49 @@
+<!doctype html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<title>RTCPeerConnection getStats test related to outbound-rtp stats</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../webrtc/RTCPeerConnection-helper.js"></script>
+<script>
+function extractOutboundRtpStats(stats) {
+ const wantedStats = [];
+ stats.forEach(report => {
+ if (report.type === 'outbound-rtp') {
+ wantedStats.push(report);
+ }
+ });
+ return wantedStats;
+}
+
+promise_test(async (test) => {
+ const pc1 = new RTCPeerConnection();
+ test.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ test.add_cleanup(() => pc2.close());
+
+ const stream = await getNoiseStream({audio: true, video: true});
+ stream.getTracks().forEach(t => pc1.addTrack(t, stream));
+ exchangeIceCandidates(pc1, pc2);
+ exchangeOfferAnswer(pc1, pc2);
+ const {track} = await new Promise(r => pc2.ontrack = r);
+ await new Promise(r => track.onunmute = r);
+ let outboundStats = extractOutboundRtpStats(await pc1.getStats());
+ assert_equals(outboundStats.length, 2);
+ assert_true(outboundStats[0].active);
+ assert_true(outboundStats[1].active);
+
+ pc1.getSenders().forEach(async sender => {
+ const parameters = sender.getParameters();
+ parameters.encodings[0].active = false;
+ await sender.setParameters(parameters);
+ });
+ // Avoid any stats caching.
+ await (new Promise(r => test.step_timeout(r, 100)));
+
+ outboundStats = extractOutboundRtpStats(await pc1.getStats());
+ assert_equals(outboundStats.length, 2);
+ assert_false(outboundStats[0].active);
+ assert_false(outboundStats[1].active);
+}, 'setting an encoding to false is reflected in outbound-rtp stats');
+</script>
diff --git a/testing/web-platform/tests/webrtc-stats/rtp-stats-creation.html b/testing/web-platform/tests/webrtc-stats/rtp-stats-creation.html
new file mode 100644
index 0000000000..7a6d9df456
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-stats/rtp-stats-creation.html
@@ -0,0 +1,110 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>No RTCRtpStreamStats should exist prior to RTP/RTCP packet flow</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../webrtc/RTCPeerConnection-helper.js"></script>
+<script>
+'use strict';
+
+promise_test(async (test) => {
+ const localPc = createPeerConnectionWithCleanup(test);
+ const remotePc = createPeerConnectionWithCleanup(test);
+
+ localPc.addTransceiver("audio");
+ localPc.addTransceiver("video");
+ await exchangeOfferAndListenToOntrack(test, localPc, remotePc);
+ const report = await remotePc.getStats();
+ const rtp = [...report.values()].filter(({type}) => type.endsWith("rtp"));
+ assert_equals(rtp.length, 0, "no rtp stats with only remote description");
+}, "No RTCRtpStreamStats exist when only remote description is set");
+
+promise_test(async (test) => {
+ const localPc = createPeerConnectionWithCleanup(test);
+ const remotePc = createPeerConnectionWithCleanup(test);
+
+ localPc.addTrack(...await createTrackAndStreamWithCleanup(test, "audio"));
+ localPc.addTrack(...await createTrackAndStreamWithCleanup(test, "video"));
+ await exchangeOfferAndListenToOntrack(test, localPc, remotePc);
+ const report = await localPc.getStats();
+ const rtp = [...report.values()].filter(({type}) => type.endsWith("rtp"));
+ assert_equals(rtp.length, 0, "no rtp stats with only local description");
+}, "No RTCRtpStreamStats exist when only local description is set");
+
+promise_test(async (test) => {
+ const localPc = createPeerConnectionWithCleanup(test);
+ const remotePc = createPeerConnectionWithCleanup(test);
+
+ localPc.addTrack(...await createTrackAndStreamWithCleanup(test, "audio"));
+ localPc.addTrack(...await createTrackAndStreamWithCleanup(test, "video"));
+ exchangeIceCandidates(localPc, remotePc);
+ await Promise.all([
+ exchangeOfferAnswer(localPc, remotePc),
+ new Promise(r => remotePc.ontrack = e => e.track.onunmute = r)
+ ]);
+ const start = performance.now();
+ while (true) {
+ const report = await localPc.getStats();
+ const outbound =
+ [...report.values()].filter(({type}) => type == "outbound-rtp");
+ assert_true(outbound.every(({packetsSent}) => packetsSent > 0),
+ "no outbound rtp stats before packets sent");
+ if (outbound.length == 2) {
+ // One outbound stat for each track is present. We're done.
+ break;
+ }
+ if (performance.now() > start + 5000) {
+ assert_unreached("outbound stats should become available");
+ }
+ await new Promise(r => test.step_timeout(r, 100));
+ }
+}, "No RTCOutboundRtpStreamStats exist until packets have been sent");
+
+promise_test(async (test) => {
+ const localPc = createPeerConnectionWithCleanup(test);
+ const remotePc = createPeerConnectionWithCleanup(test);
+
+ localPc.addTrack(...await createTrackAndStreamWithCleanup(test, "audio"));
+ localPc.addTrack(...await createTrackAndStreamWithCleanup(test, "video"));
+ exchangeIceCandidates(localPc, remotePc);
+ await exchangeOfferAnswer(localPc, remotePc);
+ const start = performance.now();
+ while (true) {
+ const report = await remotePc.getStats();
+ const inbound =
+ [...report.values()].filter(({type}) => type == "inbound-rtp");
+ assert_true(inbound.every(({packetsReceived}) => packetsReceived > 0),
+ "no inbound rtp stats before packets received");
+ if (inbound.length == 2) {
+ // One inbound stat for each track is present. We're done.
+ break;
+ }
+ if (performance.now() > start + 5000) {
+ assert_unreached("inbound stats should become available");
+ }
+ await new Promise(r => test.step_timeout(r, 100));
+ }
+}, "No RTCInboundRtpStreamStats exist until packets have been received");
+
+promise_test(async (test) => {
+ const localPc = createPeerConnectionWithCleanup(test);
+ const remotePc = createPeerConnectionWithCleanup(test);
+
+ localPc.addTrack(...await createTrackAndStreamWithCleanup(test, "audio"));
+ exchangeIceCandidates(localPc, remotePc);
+ await exchangeOfferAnswer(localPc, remotePc);
+ const start = performance.now();
+ while (true) {
+ const report = await remotePc.getStats();
+ const audioPlayout =
+ [...report.values()].filter(({type}) => type == "media-playout");
+ if (audioPlayout.length == 1) {
+ break;
+ }
+ if (performance.now() > start + 5000) {
+ assert_unreached("Audio playout stats should become available");
+ }
+ await new Promise(r => test.step_timeout(r, 100));
+ }
+}, "RTCAudioPlayoutStats should be present");
+</script>
diff --git a/testing/web-platform/tests/webrtc-stats/supported-stats.https.html b/testing/web-platform/tests/webrtc-stats/supported-stats.https.html
new file mode 100644
index 0000000000..24b4d3f06f
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-stats/supported-stats.https.html
@@ -0,0 +1,212 @@
+<!doctype html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<title>Support for all stats defined in WebRTC Stats</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src="../webrtc/RTCPeerConnection-helper.js"></script>
+<script src="../webrtc/dictionary-helper.js"></script>
+<script src="../webrtc/RTCStats-helper.js"></script>
+<script src="/resources/WebIDLParser.js"></script>
+<script>
+'use strict';
+
+// inspired from similar test for MTI stats in ../webrtc/RTCPeerConnection-mandatory-getStats.https.html
+
+
+
+// From https://w3c.github.io/webrtc-stats/webrtc-stats.html#rtcstatstype-str*
+
+const dictionaryNames = {
+ "codec": "RTCCodecStats",
+ "inbound-rtp": "RTCInboundRtpStreamStats",
+ "outbound-rtp": "RTCOutboundRtpStreamStats",
+ "remote-inbound-rtp": "RTCRemoteInboundRtpStreamStats",
+ "remote-outbound-rtp": "RTCRemoteOutboundRtpStreamStats",
+ "csrc": "RTCRtpContributingSourceStats",
+ "peer-connection": "RTCPeerConnectionStats",
+ "data-channel": "RTCDataChannelStats",
+ "media-source": {
+ audio: "RTCAudioSourceStats",
+ video: "RTCVideoSourceStats"
+ },
+ "media-playout": "RTCAudioPlayoutStats",
+ "sender": {
+ audio: "RTCAudioSenderStats",
+ video: "RTCVideoSenderStats"
+ },
+ "receiver": {
+ audio: "RTCAudioReceiverStats",
+ video: "RTCVideoReceiverStats",
+ },
+ "transport": "RTCTransportStats",
+ "candidate-pair": "RTCIceCandidatePairStats",
+ "local-candidate": "RTCIceCandidateStats",
+ "remote-candidate": "RTCIceCandidateStats",
+ "certificate": "RTCCertificateStats",
+};
+
+function isPropertyTestable(type, property) {
+ // List of properties which are not testable by this test.
+ // When adding something to this list, please explain why.
+ const untestablePropertiesByType = {
+ 'candidate-pair': [
+ 'availableIncomingBitrate', // requires REMB, no TWCC.
+ ],
+ 'certificate': [
+ 'issuerCertificateId', // we only use self-signed certificates.
+ ],
+ 'local-candidate': [
+ 'url', // requires a STUN/TURN server.
+ 'relayProtocol', // requires a TURN server.
+ 'relatedAddress', // requires a STUN/TURN server.
+ 'relatedPort', // requires a STUN/TURN server.
+ ],
+ 'remote-candidate': [
+ 'url', // requires a STUN/TURN server.
+ 'relayProtocol', // requires a TURN server.
+ 'relatedAddress', // requires a STUN/TURN server.
+ 'relatedPort', // requires a STUN/TURN server.
+ 'tcpType', // requires ICE-TCP connection.
+ ],
+ 'outbound-rtp': [
+ 'rid', // requires simulcast.
+ ],
+ 'media-source': [
+ 'echoReturnLoss', // requires gUM with an audio input device.
+ 'echoReturnLossEnhancement', // requires gUM with an audio input device.
+ ]
+ };
+ if (!untestablePropertiesByType[type]) {
+ return true;
+ }
+ return !untestablePropertiesByType[type].includes(property);
+}
+
+async function getAllStats(t, pc) {
+ // Try to obtain as many stats as possible, waiting up to 20 seconds for
+ // roundTripTime which can take several RTCP messages to calculate.
+ let stats;
+ for (let i = 0; i < 20; i++) {
+ stats = await pc.getStats();
+ const values = [...stats.values()];
+ const [remoteInboundAudio, remoteInboundVideo] =
+ ["audio", "video"].map(kind =>
+ values.find(s => s.type == "remote-inbound-rtp" && s.kind == kind));
+ const [remoteOutboundAudio, remoteOutboundVideo] =
+ ["audio", "video"].map(kind =>
+ values.find(s => s.type == "remote-outbound-rtp" && s.kind == kind));
+ // We expect both audio and video remote-inbound-rtp RTT.
+ const hasRemoteInbound =
+ remoteInboundAudio && "roundTripTime" in remoteInboundAudio &&
+ remoteInboundVideo && "roundTripTime" in remoteInboundVideo;
+ // Due to current implementation limitations, we don't put as hard
+ // requirements on remote-outbound-rtp as remote-inbound-rtp. It's enough if
+ // it is available for either kind and `roundTripTime` is not required. In
+ // Chromium, remote-outbound-rtp is only implemented for audio and
+ // `roundTripTime` is missing in this test, but awaiting for any
+ // remote-outbound-rtp avoids flaky failures.
+ const hasRemoteOutbound = remoteOutboundAudio || remoteOutboundVideo;
+ const hasMediaPlayout = values.find(({type}) => type == "media-playout") != undefined;
+ if (hasRemoteInbound && hasRemoteOutbound && hasMediaPlayout) {
+ return stats;
+ }
+ await new Promise(r => t.step_timeout(r, 1000));
+ }
+ return stats;
+}
+
+
+promise_test(async t => {
+ // load the IDL to know which members to be looking for
+ const idl = await fetch("/interfaces/webrtc-stats.idl").then(r => r.text());
+ // for RTCStats definition
+ const webrtcIdl = await fetch("/interfaces/webrtc.idl").then(r => r.text());
+ const astArray = WebIDL2.parse(idl + webrtcIdl);
+
+ let all = {};
+ for (let type in dictionaryNames) {
+ // TODO: make use of audio/video distinction
+ let dictionaries = dictionaryNames[type].audio ? Object.values(dictionaryNames[type]) : [dictionaryNames[type]];
+ all[type] = [];
+ let i = 0;
+ // Recursively collect members from inherited dictionaries
+ while (i < dictionaries.length) {
+ const dictName = dictionaries[i];
+ const dict = astArray.find(i => i.name === dictName && i.type === "dictionary");
+ if (dict && dict.members) {
+ all[type] = all[type].concat(dict.members.map(m => m.name));
+ if (dict.inheritance) {
+ dictionaries.push(dict.inheritance);
+ }
+ }
+ i++;
+ }
+ // Unique-ify
+ all[type] = [...new Set(all[type])];
+ }
+
+ const remaining = JSON.parse(JSON.stringify(all));
+ for (const type in remaining) {
+ remaining[type] = new Set(remaining[type]);
+ }
+
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const dc1 = pc1.createDataChannel("dummy", {negotiated: true, id: 0});
+ const dc2 = pc2.createDataChannel("dummy", {negotiated: true, id: 0});
+ // Use a real gUM to ensure that all stats exposing hardware capabilities are
+ // also exposed.
+ const stream = await navigator.mediaDevices.getUserMedia(
+ {video: true, audio: true});
+ for (const track of stream.getTracks()) {
+ pc1.addTrack(track, stream);
+ pc2.addTrack(track, stream);
+ t.add_cleanup(() => track.stop());
+ }
+
+ // Do a non-trickle ICE handshake to ensure that TCP candidates are gathered.
+ await pc1.setLocalDescription();
+ await waitForIceGatheringState(pc1, ['complete']);
+ await pc2.setRemoteDescription(pc1.localDescription);
+ await pc2.setLocalDescription();
+ await waitForIceGatheringState(pc2, ['complete']);
+ await pc1.setRemoteDescription(pc2.localDescription);
+
+ const stats = await getAllStats(t, pc1);
+
+ // The focus of this test is not API correctness, but rather to provide an
+ // accessible metric of implementation progress by dictionary member. We count
+ // whether we've seen each dictionary's members in getStats().
+
+ test(t => {
+ for (const stat of stats.values()) {
+ if (all[stat.type]) {
+ const memberNames = all[stat.type];
+ const remainingNames = remaining[stat.type];
+ assert_true(memberNames.length > 0, "Test error. No member found.");
+ for (const memberName of memberNames) {
+ if (memberName in stat) {
+ assert_not_equals(stat[memberName], undefined, "Not undefined");
+ remainingNames.delete(memberName);
+ }
+ }
+ }
+ }
+ }, "Validating stats");
+
+ for (const type in all) {
+ for (const memberName of all[type]) {
+ test(t => {
+ assert_implements_optional(isPropertyTestable(type, memberName),
+ `${type}.${memberName} marked as not testable.`);
+ assert_true(!remaining[type].has(memberName),
+ `Is ${memberName} present`);
+ }, `${type}'s ${memberName}`);
+ }
+ }
+}, 'getStats succeeds');
+</script>
diff --git a/testing/web-platform/tests/webrtc-svc/META.yml b/testing/web-platform/tests/webrtc-svc/META.yml
new file mode 100644
index 0000000000..17d93c51a9
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-svc/META.yml
@@ -0,0 +1 @@
+spec: https://w3c.github.io/webrtc-svc/
diff --git a/testing/web-platform/tests/webrtc-svc/RTCRtpParameters-scalability-av1.html b/testing/web-platform/tests/webrtc-svc/RTCRtpParameters-scalability-av1.html
new file mode 100644
index 0000000000..24cfcb8f4a
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-svc/RTCRtpParameters-scalability-av1.html
@@ -0,0 +1,43 @@
+<!doctype html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<title>AV1 scalabilityMode</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/webrtc/RTCRtpParameters-helper.js"></script>
+<script src="/webrtc/RTCPeerConnection-helper.js"></script>
+<script src="/webrtc-svc/svc-helper.js"></script>
+<script>
+ 'use strict';
+
+ createScalabilityTest('video/AV1', [
+ "L1T1",
+ "L1T2",
+ "L1T3",
+ "L2T1",
+ "L2T2",
+ "L2T3",
+ "L3T1",
+ "L3T2",
+ "L3T3",
+ "L2T1h",
+ "L2T2h",
+ "L2T3h",
+ "S2T1",
+ "S2T2",
+ "S2T3",
+ "S2T1h",
+ "S2T2h",
+ "S2T3h",
+ "S3T1",
+ "S3T2",
+ "S3T3",
+ "S3T1h",
+ "S3T2h",
+ "S3T3h",
+ "L2T2_KEY",
+ "L2T3_KEY",
+ "L3T2_KEY",
+ "L3T3_KEY"
+ ]);
+</script>
diff --git a/testing/web-platform/tests/webrtc-svc/RTCRtpParameters-scalability-h264.html b/testing/web-platform/tests/webrtc-svc/RTCRtpParameters-scalability-h264.html
new file mode 100644
index 0000000000..2a595a8169
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-svc/RTCRtpParameters-scalability-h264.html
@@ -0,0 +1,18 @@
+<!doctype html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<title>H264 scalabilityMode</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/webrtc/RTCRtpParameters-helper.js"></script>
+<script src="/webrtc/RTCPeerConnection-helper.js"></script>
+<script src="/webrtc-svc/svc-helper.js"></script>
+<script>
+ 'use strict';
+
+ createScalabilityTest('video/H264', [
+ "L1T1",
+ "L1T2",
+ "L1T3"
+ ]);
+</script>
diff --git a/testing/web-platform/tests/webrtc-svc/RTCRtpParameters-scalability-vp8.html b/testing/web-platform/tests/webrtc-svc/RTCRtpParameters-scalability-vp8.html
new file mode 100644
index 0000000000..1708ab1017
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-svc/RTCRtpParameters-scalability-vp8.html
@@ -0,0 +1,18 @@
+<!doctype html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<title>VP8 scalabilityMode</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/webrtc/RTCRtpParameters-helper.js"></script>
+<script src="/webrtc/RTCPeerConnection-helper.js"></script>
+<script src="/webrtc-svc/svc-helper.js"></script>
+<script>
+ 'use strict';
+
+ createScalabilityTest('video/VP8', [
+ "L1T1",
+ "L1T2",
+ "L1T3"
+ ]);
+</script>
diff --git a/testing/web-platform/tests/webrtc-svc/RTCRtpParameters-scalability-vp9.html b/testing/web-platform/tests/webrtc-svc/RTCRtpParameters-scalability-vp9.html
new file mode 100644
index 0000000000..f1f4923868
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-svc/RTCRtpParameters-scalability-vp9.html
@@ -0,0 +1,43 @@
+<!doctype html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<title>VP9 scalabilityMode</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/webrtc/RTCRtpParameters-helper.js"></script>
+<script src="/webrtc/RTCPeerConnection-helper.js"></script>
+<script src="/webrtc-svc/svc-helper.js"></script>
+<script>
+ 'use strict';
+
+ createScalabilityTest('video/VP9', [
+ "L1T1",
+ "L1T2",
+ "L1T3",
+ "L2T1",
+ "L2T2",
+ "L2T3",
+ "L3T1",
+ "L3T2",
+ "L3T3",
+ "L2T1h",
+ "L2T2h",
+ "L2T3h",
+ "S2T1",
+ "S2T2",
+ "S2T3",
+ "S2T1h",
+ "S2T2h",
+ "S2T3h",
+ "S3T1",
+ "S3T2",
+ "S3T3",
+ "S3T1h",
+ "S3T2h",
+ "S3T3h",
+ "L2T2_KEY",
+ "L2T3_KEY",
+ "L3T2_KEY",
+ "L3T3_KEY"
+ ]);
+</script>
diff --git a/testing/web-platform/tests/webrtc-svc/RTCRtpParameters-scalability.html b/testing/web-platform/tests/webrtc-svc/RTCRtpParameters-scalability.html
new file mode 100644
index 0000000000..ff28c2b5e9
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-svc/RTCRtpParameters-scalability.html
@@ -0,0 +1,93 @@
+<!doctype html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<title>RTCRtpParameters encodings</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/webrtc/dictionary-helper.js"></script>
+<script src="/webrtc/RTCRtpParameters-helper.js"></script>
+<script src="../webrtc/RTCPeerConnection-helper.js"></script>
+<script>
+ 'use strict';
+
+ // Test is based on the following editor draft:
+ // https://w3c.github.io/webrtc-svc/
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const { sender } = pc.addTransceiver('video', {
+ sendEncodings: [{scalabilityMode: 'L1T3'}],
+ });
+
+ const param = sender.getParameters();
+ const encoding = param.encodings[0];
+
+ assert_equals(encoding.scalabilityMode, 'L1T3');
+
+ encoding.scalabilityMode = 'L1T2';
+ await sender.setParameters(param);
+
+ const updatedParam = sender.getParameters();
+ const updatedEncoding = updatedParam.encodings[0];
+
+ assert_equals(updatedEncoding.scalabilityMode, 'L1T2');
+ }, `Setting and updating scalabilityMode to a legal value should be accepted`);
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const { sender } = pc.addTransceiver('video');
+ const param = sender.getParameters();
+ const encoding = param.encodings[0];
+ assert_true(!('scalabilityMode' in encoding));
+ }, 'Not setting sendEncodings results in no mode info before negotiation');
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const { sender } = pc.addTransceiver('video', {
+ sendEncodings: [{}],
+ });
+ const param = sender.getParameters();
+ const encoding = param.encodings[0];
+ assert_true(!('scalabilityMode' in encoding));
+ }, 'Not setting a scalability mode results in no mode set before negotiation');
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ assert_throws_dom('OperationError', () => {
+ pc.addTransceiver('video', {
+ sendEncodings: [{scalabilityMode: 'TotalNonsense'}],
+ });
+ });
+ }, 'Setting a scalability mode to nonsense throws an exception');
+
+ promise_test(async t => {
+ const v = document.createElement('video');
+ v.autoplay = true;
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+ const transceiver = pc1.addTransceiver('video', {
+ sendEncodings: [{ scalabilityMode: 'L3T3' }],
+ });
+ // Before negotiation, the mode should be preserved.
+ const param = transceiver.sender.getParameters();
+ const encoding = param.encodings[0];
+ assert_true('scalabilityMode' in encoding);
+ // If L3T3 is not supported at all, abort test.
+ assert_implements_optional(encoding.scalabilityMode === 'L3T3');
+ // Pick a codec known to not have L3T3 support
+ const capabilities = RTCRtpSender.getCapabilities('video');
+ const codec = capabilities.codecs.find(c => c.mimeType === 'video/VP8');
+ assert_true(codec !== undefined);
+ transceiver.setCodecPreferences([codec]);
+ exchangeIceCandidates(pc1, pc2);
+ await exchangeOfferAnswer(pc1, pc2);
+ const sendParams = pc1.getSenders()[0].getParameters();
+ assert_not_equals(sendParams.encodings[0].scalabilityMode, 'L3T3');
+ }, 'L3T3 on VP8 should return something other than L3T3');
+</script>
diff --git a/testing/web-platform/tests/webrtc-svc/svc-helper.js b/testing/web-platform/tests/webrtc-svc/svc-helper.js
new file mode 100644
index 0000000000..e73ccfa750
--- /dev/null
+++ b/testing/web-platform/tests/webrtc-svc/svc-helper.js
@@ -0,0 +1,50 @@
+function supportsCodec(mimeType) {
+ return RTCRtpSender.getCapabilities('video').codecs.filter(c => c.mimeType == mimeType).length() > 0;
+}
+
+async function supportsScalabilityMode(mimeType, scalabilityMode) {
+ let result = await navigator.mediaCapabilities.encodingInfo({
+ type: 'webrtc',
+ video: {
+ contentType: mimeType,
+ width: 60,
+ height: 60,
+ bitrate: 10000,
+ framerate: 30,
+ scalabilityMode: scalabilityMode
+ }
+ });
+ return result.supported;
+}
+
+function createScalabilityTest(mimeType, scalabilityModes) {
+ for (const scalabilityMode of scalabilityModes) {
+ promise_test(async t => {
+ assert_implements_optional(
+ supportsScalabilityMode(mimeType, scalabilityMode),
+ `${mimeType} supported`
+ );
+ const v = document.createElement('video');
+ v.autoplay = true;
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+ const stream1 = await getNoiseStream({ video: { signal: 100, width: 60, height: 60 } });
+ const [track1] = stream1.getTracks();
+ t.add_cleanup(() => track1.stop());
+ const transceiver = pc1.addTransceiver(track1, {
+ sendEncodings: [{ scalabilityMode: scalabilityMode }],
+ });
+ transceiver.setCodecPreferences(RTCRtpSender.getCapabilities('video').codecs.filter(c => c.mimeType == mimeType));
+ const haveTrackEvent = new Promise(r => pc2.ontrack = r);
+ exchangeIceCandidates(pc1, pc2);
+ await exchangeOfferAnswer(pc1, pc2);
+ v.srcObject = new MediaStream([(await haveTrackEvent).track]);
+ await new Promise(r => v.onloadedmetadata = r);
+ await detectSignal(t, v, 100);
+ const sendParams = pc1.getSenders()[0].getParameters();
+ assert_equals(sendParams.encodings[0].scalabilityMode, scalabilityMode);
+ }, `${mimeType} - ${scalabilityMode} should produce valid video content`);
+ }
+}
diff --git a/testing/web-platform/tests/webrtc/META.yml b/testing/web-platform/tests/webrtc/META.yml
new file mode 100644
index 0000000000..69fcad76f1
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/META.yml
@@ -0,0 +1,9 @@
+spec: https://w3c.github.io/webrtc-pc/
+suggested_reviewers:
+ - snuggs
+ - alvestrand
+ - guidou
+ - henbos
+ - youennf
+ - rwaldron
+ - jan-ivar
diff --git a/testing/web-platform/tests/webrtc/README.md b/testing/web-platform/tests/webrtc/README.md
new file mode 100644
index 0000000000..4477e4f375
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/README.md
@@ -0,0 +1,12 @@
+# WebRTC
+
+This directory contains the WebRTC test suite.
+
+## Acknowledgements
+
+Some data channel tests are based on the [data channel conformance test
+suite][nplab-webrtc-dc-playground] of the Network Programming Lab of the MÞnster
+University of Applied Sciences. We would like to thank Peter Titz, Felix Weinrank and Timo
+VÃķlker for agreeing to contribute their test cases to this repository.
+
+[nplab-webrtc-dc-playground]: https://github.com/nplab/WebRTC-Data-Channel-Playground/tree/master/conformance-tests
diff --git a/testing/web-platform/tests/webrtc/RTCCertificate-postMessage.html b/testing/web-platform/tests/webrtc/RTCCertificate-postMessage.html
new file mode 100644
index 0000000000..6cca240057
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCCertificate-postMessage.html
@@ -0,0 +1,78 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>RTCCertificate persistent Tests</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<body>
+<script>
+ function findMatchingFingerprint(fingerprints, fingerprint) {
+ for (let f of fingerprints) {
+ if (f.value == fingerprint.value && f.algorithm == fingerprint.algorithm)
+ return true;
+ }
+ return false;
+ }
+
+ function with_iframe(url) {
+ return new Promise(function(resolve) {
+ var frame = document.createElement('iframe');
+ frame.src = url;
+ frame.onload = function() { resolve(frame); };
+ document.body.appendChild(frame);
+ });
+ }
+
+ function testPostMessageCertificate(isCrossOrigin) {
+ promise_test(async t => {
+ let certificate = await RTCPeerConnection.generateCertificate({ name: 'ECDSA', namedCurve: 'P-256' });
+
+ let url = "resources/RTCCertificate-postMessage-iframe.html";
+ if (isCrossOrigin)
+ url = get_host_info().HTTP_REMOTE_ORIGIN + "/webrtc/" + url;
+
+ let iframe = await with_iframe(url);
+
+ let promise = new Promise((resolve, reject) => {
+ window.onmessage = (event) => {
+ resolve(event.data);
+ };
+ t.step_timeout(() => reject("Timed out waiting for frame to send back certificate"), 5000);
+ });
+ iframe.contentWindow.postMessage(certificate, "*");
+ let certificate2 = await promise;
+
+ const pc1 = new RTCPeerConnection({certificates: [certificate]});
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection({certificates: [certificate2]});
+ t.add_cleanup(() => pc2.close());
+
+ assert_equals(certificate.expires, certificate2.expires);
+ for (let fingerprint of certificate2.getFingerprints())
+ assert_true(findMatchingFingerprint(certificate.getFingerprints(), fingerprint), "check fingerprints");
+
+ iframe.remove();
+ }, "Check " + (isCrossOrigin ? "cross-origin" : "same-origin") + " RTCCertificate serialization");
+ }
+
+ testPostMessageCertificate(false);
+ testPostMessageCertificate(true);
+
+ promise_test(async t => {
+ let url = get_host_info().HTTP_REMOTE_ORIGIN + "/webrtc/resources/RTCCertificate-postMessage-iframe.html";
+ let iframe = await with_iframe(url);
+
+ let promise = new Promise((resolve, reject) => {
+ window.onmessage = (event) => {
+ resolve(event.data);
+ };
+ t.step_timeout(() => reject("Timed out waiting for frame to send back certificate"), 5000);
+ });
+ iframe.contentWindow.postMessage(null, "*");
+ let certificate2 = await promise;
+
+ assert_throws_dom("InvalidAccessError", () => { new RTCPeerConnection({certificates: [certificate2]}) });
+ iframe.remove();
+ }, "Check cross-origin created RTCCertificate");
+</script>
+</body>
diff --git a/testing/web-platform/tests/webrtc/RTCCertificate.html b/testing/web-platform/tests/webrtc/RTCCertificate.html
new file mode 100644
index 0000000000..6b7626c92e
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCCertificate.html
@@ -0,0 +1,283 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>RTCCertificate Tests</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+ 'use strict';
+
+ // Test is based on the Candidate Recommendation:
+ // https://www.w3.org/TR/webrtc/
+
+ /*
+ 4.2.1. RTCConfiguration Dictionary
+ dictionary RTCConfiguration {
+ sequence<RTCCertificate> certificates;
+ ...
+ };
+
+ certificates of type sequence<RTCCertificate>
+ If this value is absent, then a default set of certificates is
+ generated for each RTCPeerConnection instance.
+
+ The value for this configuration option cannot change after its
+ value is initially selected.
+
+ 4.10.2. RTCCertificate Interface
+ interface RTCCertificate {
+ readonly attribute DOMTimeStamp expires;
+ static sequence<AlgorithmIdentifier> getSupportedAlgorithms();
+ sequence<RTCDtlsFingerprint> getFingerprints();
+ };
+
+ 5.5.1 The RTCDtlsFingerprint Dictionary
+ dictionary RTCDtlsFingerprint {
+ DOMString algorithm;
+ DOMString value;
+ };
+
+ [RFC4572] Comedia over TLS in SDP
+ 5. Fingerprint Attribute
+ Figure 2. Augmented Backus-Naur Syntax for the Fingerprint Attribute
+
+ attribute =/ fingerprint-attribute
+
+ fingerprint-attribute = "fingerprint" ":" hash-func SP fingerprint
+
+ hash-func = "sha-1" / "sha-224" / "sha-256" /
+ "sha-384" / "sha-512" /
+ "md5" / "md2" / token
+ ; Additional hash functions can only come
+ ; from updates to RFC 3279
+
+ fingerprint = 2UHEX *(":" 2UHEX)
+ ; Each byte in upper-case hex, separated
+ ; by colons.
+
+ UHEX = DIGIT / %x41-46 ; A-F uppercase
+ */
+
+ // Helper function to generate certificate with a set of
+ // default parameters
+ function generateCertificate() {
+ return RTCPeerConnection.generateCertificate({
+ name: 'ECDSA',
+ namedCurve: 'P-256'
+ });
+ }
+
+ // Helper function that takes in an RTCDtlsFingerprint
+ // and return an a=fingerprint SDP line
+ function fingerprintToSdpLine(fingerprint) {
+ return `\r\na=fingerprint:${fingerprint.algorithm} ${fingerprint.value.toUpperCase()}\r\n`;
+ }
+
+ // Assert that an SDP string has fingerprint line for all the cert's fingerprints
+ function assert_sdp_has_cert_fingerprints(sdp, cert) {
+ for(const fingerprint of cert.getFingerprints()) {
+ const fingerprintLine = fingerprintToSdpLine(fingerprint);
+ assert_true(sdp.includes(fingerprintLine),
+ 'Expect fingerprint line to be found in SDP');
+ }
+ }
+
+ /*
+ 4.3.1. Operation
+ When the RTCPeerConnection() constructor is invoked
+ 2. If the certificates value in configuration is non-empty,
+ check that the expires on each value is in the future.
+ If a certificate has expired, throw an InvalidAccessError;
+ otherwise, store the certificates. If no certificates value
+ was specified, one or more new RTCCertificate instances are
+ generated for use with this RTCPeerConnection instance.
+ This may happen asynchronously and the value of certificates
+ remains undefined for the subsequent steps.
+ */
+ promise_test(t => {
+ return RTCPeerConnection.generateCertificate({
+ name: 'ECDSA',
+ namedCurve: 'P-256',
+ expires: 0
+ }).then(cert => {
+ assert_less_than_equal(cert.expires, Date.now());
+ assert_throws_dom('InvalidAccessError', () =>
+ new RTCPeerConnection({ certificates: [cert] }));
+ });
+ }, 'Constructing RTCPeerConnection with expired certificate should reject with InvalidAccessError');
+
+ /*
+ 4.3.2 Interface Definition
+ setConfiguration
+ 4. If configuration.certificates is set and the set of
+ certificates differs from the ones used when connection
+ was constructed, throw an InvalidModificationError.
+ */
+ promise_test(t => {
+ return Promise.all([
+ generateCertificate(),
+ generateCertificate()
+ ]).then(([cert1, cert2]) => {
+ const pc = new RTCPeerConnection({
+ certificates: [cert1]
+ });
+
+ // should not throw
+ pc.setConfiguration({
+ certificates: [cert1]
+ });
+
+ assert_throws_dom('InvalidModificationError', () =>
+ pc.setConfiguration({
+ certificates: [cert2]
+ }));
+
+ assert_throws_dom('InvalidModificationError', () =>
+ pc.setConfiguration({
+ certificates: [cert1, cert2]
+ }));
+ });
+ }, 'Calling setConfiguration with different set of certs should reject with InvalidModificationError');
+
+ /*
+ 4.10.2. RTCCertificate Interface
+ getFingerprints
+ Returns the list of certificate fingerprints, one of which is
+ computed with the digest algorithm used in the certificate signature.
+
+ 5.5.1 The RTCDtlsFingerprint Dictionary
+ algorithm of type DOMString
+ One of the the hash function algorithms defined in the 'Hash function
+ Textual Names' registry, initially specified in [RFC4572] Section 8.
+ As noted in [JSEP] Section 5.2.1, the digest algorithm used for the
+ fingerprint matches that used in the certificate signature.
+
+ value of type DOMString
+ The value of the certificate fingerprint in lowercase hex string as
+ expressed utilizing the syntax of 'fingerprint' in [ RFC4572] Section 5.
+
+ */
+ promise_test(t => {
+ return generateCertificate()
+ .then(cert => {
+ assert_idl_attribute(cert, 'getFingerprints');
+
+ const fingerprints = cert.getFingerprints();
+ assert_true(Array.isArray(fingerprints),
+ 'Expect fingerprints to return an array');
+
+ assert_greater_than_equal(fingerprints.length, 1,
+ 'Expect at last one fingerprint in array');
+
+ for(const fingerprint of fingerprints) {
+ assert_equals(typeof fingerprint, 'object',
+ 'Expect fingerprint to be an object (dictionary)');
+
+ // https://www.iana.org/assignments/hash-function-text-names/hash-function-text-names.xml
+ const algorithms = ['md2', 'md5', 'sha-1', 'sha-224', 'sha-256', 'sha-384', 'sha-512'];
+ assert_in_array(fingerprint.algorithm, algorithms,
+ 'Expect fingerprint.algorithm to be string of algorithm identifier');
+
+ assert_true(/^([0-9a-f]{2}\:)+[0-9a-f]{2}$/.test(fingerprint.value),
+ 'Expect fingerprint.value to be lowercase hexadecimal separated by colon');
+ }
+ });
+ }, 'RTCCertificate should have at least one fingerprint');
+
+ /*
+ 4.3.2 Interface Definition
+ createOffer
+ The value for certificates in the RTCConfiguration for the
+ RTCPeerConnection is used to produce a set of certificate
+ fingerprints. These certificate fingerprints are used in the
+ construction of SDP and as input to requests for identity
+ assertions.
+
+ [JSEP]
+ 5.2.1. Initial Offers
+ For DTLS, all m= sections MUST use all the certificate(s) that have
+ been specified for the PeerConnection; as a result, they MUST all
+ have the same [I-D.ietf-mmusic-4572-update] fingerprint value(s), or
+ these value(s) MUST be session-level attributes.
+
+ The following attributes, which are of category IDENTICAL or
+ TRANSPORT, MUST appear only in "m=" sections which either have a
+ unique address or which are associated with the bundle-tag. (In
+ initial offers, this means those "m=" sections which do not contain
+ an "a=bundle-only" attribute.)
+
+ - An "a=fingerprint" line for each of the endpoint's certificates,
+ as specified in [RFC4572], Section 5; the digest algorithm used
+ for the fingerprint MUST match that used in the certificate
+ signature.
+
+ Each m= section which is not bundled into another m= section, MUST
+ contain the following attributes (which are of category IDENTICAL or
+ TRANSPORT):
+
+ - An "a=fingerprint" line for each of the endpoint's certificates,
+ as specified in [RFC4572], Section 5; the digest algorithm used
+ for the fingerprint MUST match that used in the certificate
+ signature.
+ */
+ promise_test(t => {
+ return generateCertificate()
+ .then(cert => {
+ const pc = new RTCPeerConnection({
+ certificates: [cert]
+ });
+ pc.createDataChannel('test');
+
+ return pc.createOffer()
+ .then(offer => {
+ assert_sdp_has_cert_fingerprints(offer.sdp, cert);
+ });
+ });
+ }, 'RTCPeerConnection({ certificates }) should generate offer SDP with fingerprint of provided certificate');
+
+ promise_test(t => {
+ return Promise.all([
+ generateCertificate(),
+ generateCertificate()
+ ]).then(certs => {
+ const pc = new RTCPeerConnection({
+ certificates: certs
+ });
+ pc.createDataChannel('test');
+
+ return pc.createOffer()
+ .then(offer => {
+ for(const cert of certs) {
+ assert_sdp_has_cert_fingerprints(offer.sdp, cert);
+ }
+ });
+ });
+ }, 'RTCPeerConnection({ certificates }) should generate offer SDP with fingerprint of all provided certificates');
+
+ /*
+ TODO
+
+ 4.10.2. RTCCertificate Interface
+ getSupportedAlgorithms
+ Returns a sequence providing a representative set of supported
+ certificate algorithms. At least one algorithm MUST be returned.
+
+ The RTCCertificate object can be stored and retrieved from persistent
+ storage by an application. When a user agent is required to obtain a
+ structured clone [HTML5] of a RTCCertificate object, it performs the
+ following steps:
+ 1. Let input and memory be the corresponding inputs defined by the
+ internal structured cloning algorithm, where input represents a
+ RTCCertificate object to be cloned.
+ 2. Let output be a newly constructed RTCCertificate object.
+ 3. Copy the value of the expires attribute from input to output.
+ 4. Let the [[certificate]] internal slot of output be set to the
+ result of invoking the internal structured clone algorithm
+ recursively on the corresponding internal slots of input, with
+ the slot contents as the new " input" argument and memory as
+ the new " memory" argument.
+ 5. Let the [[handle]] internal slot of output refer to the same
+ private keying material represented by the [[handle]] internal
+ slot of input.
+ */
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCConfiguration-bundlePolicy.html b/testing/web-platform/tests/webrtc/RTCConfiguration-bundlePolicy.html
new file mode 100644
index 0000000000..e825d7b402
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCConfiguration-bundlePolicy.html
@@ -0,0 +1,128 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCConfiguration bundlePolicy</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+ 'use strict';
+
+ // Test is based on the following editor draft:
+ // https://w3c.github.io/webrtc-pc/archives/20170605/webrtc.html
+
+ /*
+ 4.3.2. Interface Definition
+ [Constructor(optional RTCConfiguration configuration)]
+ interface RTCPeerConnection : EventTarget {
+ ...
+ RTCConfiguration getConfiguration();
+ void setConfiguration(RTCConfiguration configuration);
+ };
+
+ 4.2.1. RTCConfiguration Dictionary
+ dictionary RTCConfiguration {
+ RTCBundlePolicy bundlePolicy = "balanced";
+ ...
+ };
+
+ 4.2.6. RTCBundlePolicy Enum
+ enum RTCBundlePolicy {
+ "balanced",
+ "max-compat",
+ "max-bundle"
+ };
+ */
+
+ test(() => {
+ const pc = new RTCPeerConnection();
+ assert_equals(pc.getConfiguration().bundlePolicy, 'balanced');
+ }, 'Default bundlePolicy should be balanced');
+
+ test(() => {
+ const pc = new RTCPeerConnection({ bundlePolicy: undefined });
+ assert_equals(pc.getConfiguration().bundlePolicy, 'balanced');
+ }, `new RTCPeerConnection({ bundlePolicy: undefined }) should have bundlePolicy balanced`);
+
+ test(() => {
+ const pc = new RTCPeerConnection({ bundlePolicy: 'balanced' });
+ assert_equals(pc.getConfiguration().bundlePolicy, 'balanced');
+ }, `new RTCPeerConnection({ bundlePolicy: 'balanced' }) should succeed`);
+
+ test(() => {
+ const pc = new RTCPeerConnection({ bundlePolicy: 'max-compat' });
+ assert_equals(pc.getConfiguration().bundlePolicy, 'max-compat');
+ }, `new RTCPeerConnection({ bundlePolicy: 'max-compat' }) should succeed`);
+
+ test(() => {
+ const pc = new RTCPeerConnection({ bundlePolicy: 'max-bundle' });
+ assert_equals(pc.getConfiguration().bundlePolicy, 'max-bundle');
+ }, `new RTCPeerConnection({ bundlePolicy: 'max-bundle' }) should succeed`);
+
+ test(() => {
+ const pc = new RTCPeerConnection();
+ pc.setConfiguration({});
+ }, 'setConfiguration({}) with initial default bundlePolicy balanced should succeed');
+
+ test(() => {
+ const pc = new RTCPeerConnection({ bundlePolicy: 'balanced' });
+ pc.setConfiguration({});
+ }, 'setConfiguration({}) with initial bundlePolicy balanced should succeed');
+
+ test(() => {
+ const pc = new RTCPeerConnection();
+ pc.setConfiguration({ bundlePolicy: 'balanced' });
+ }, 'setConfiguration({ bundlePolicy: balanced }) with initial default bundlePolicy balanced should succeed');
+
+ test(() => {
+ const pc = new RTCPeerConnection({ bundlePolicy: 'balanced' });
+ pc.setConfiguration({ bundlePolicy: 'balanced' });
+ }, `setConfiguration({ bundlePolicy: 'balanced' }) with initial bundlePolicy balanced should succeed`);
+
+ test(() => {
+ const pc = new RTCPeerConnection({ bundlePolicy: 'max-compat' });
+ pc.setConfiguration({ bundlePolicy: 'max-compat' });
+ }, `setConfiguration({ bundlePolicy: 'max-compat' }) with initial bundlePolicy max-compat should succeed`);
+
+ test(() => {
+ const pc = new RTCPeerConnection({ bundlePolicy: 'max-bundle' });
+ pc.setConfiguration({ bundlePolicy: 'max-bundle' });
+ }, `setConfiguration({ bundlePolicy: 'max-bundle' }) with initial bundlePolicy max-bundle should succeed`);
+
+ test(() => {
+ assert_throws_js(TypeError, () =>
+ new RTCPeerConnection({ bundlePolicy: null }));
+ }, `new RTCPeerConnection({ bundlePolicy: null }) should throw TypeError`);
+
+ test(() => {
+ assert_throws_js(TypeError, () =>
+ new RTCPeerConnection({ bundlePolicy: 'invalid' }));
+ }, `new RTCPeerConnection({ bundlePolicy: 'invalid' }) should throw TypeError`);
+
+ /*
+ 4.3.2. Interface Definition
+ To set a configuration
+ 5. If configuration.bundlePolicy is set and its value differs from the
+ connection's bundle policy, throw an InvalidModificationError.
+ */
+ test(() => {
+ const pc = new RTCPeerConnection({ bundlePolicy: 'max-bundle' });
+ assert_idl_attribute(pc, 'setConfiguration');
+
+ assert_throws_dom('InvalidModificationError', () =>
+ pc.setConfiguration({ bundlePolicy: 'max-compat' }));
+ }, `setConfiguration({ bundlePolicy: 'max-compat' }) with initial bundlePolicy max-bundle should throw InvalidModificationError`);
+
+ test(() => {
+ const pc = new RTCPeerConnection({ bundlePolicy: 'max-bundle' });
+ assert_idl_attribute(pc, 'setConfiguration');
+
+ // the default value for bundlePolicy is balanced
+ assert_throws_dom('InvalidModificationError', () =>
+ pc.setConfiguration({}));
+ }, `setConfiguration({}) with initial bundlePolicy max-bundle should throw InvalidModificationError`);
+
+ /*
+ Coverage Report
+ Tested 2
+ Total 2
+ */
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCConfiguration-helper.js b/testing/web-platform/tests/webrtc/RTCConfiguration-helper.js
new file mode 100644
index 0000000000..fb8eb50995
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCConfiguration-helper.js
@@ -0,0 +1,24 @@
+'use strict';
+
+// Run a test function as two test cases.
+// The first test case test the configuration by passing a given config
+// to the constructor.
+// The second test case create an RTCPeerConnection object with default
+// configuration, then call setConfiguration with the provided config.
+// The test function is given a constructor function to create
+// a new instance of RTCPeerConnection with given config,
+// either directly as constructor parameter or through setConfiguration.
+function config_test(test_func, desc) {
+ test(() => {
+ test_func(config => new RTCPeerConnection(config));
+ }, `new RTCPeerConnection(config) - ${desc}`);
+
+ test(() => {
+ test_func(config => {
+ const pc = new RTCPeerConnection();
+ assert_idl_attribute(pc, 'setConfiguration');
+ pc.setConfiguration(config);
+ return pc;
+ })
+ }, `setConfiguration(config) - ${desc}`);
+}
diff --git a/testing/web-platform/tests/webrtc/RTCConfiguration-iceCandidatePoolSize.html b/testing/web-platform/tests/webrtc/RTCConfiguration-iceCandidatePoolSize.html
new file mode 100644
index 0000000000..495b043e12
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCConfiguration-iceCandidatePoolSize.html
@@ -0,0 +1,117 @@
+<!doctype html>
+<meta charset="utf-8">
+<!--
+4.2.1 RTCConfiguration Dictionary
+
+ The RTCConfiguration defines a set of parameters to configure how the peer to peer communication established via RTCPeerConnection is established or re-established.
+
+ ...
+
+ iceCandidatePoolSize of type octet, defaulting to 0
+ Size of the prefetched ICE pool as defined in [JSEP] (section 3.5.4. and section 4.1.1.).
+-->
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+
+/*
+
+dictionary RTCConfiguration {
+ ...
+ [EnforceRange]
+ octet iceCandidatePoolSize = 0;
+};
+
+... of type octet
+*/
+test(() => {
+ const pc = new RTCPeerConnection();
+ assert_idl_attribute(pc, "getConfiguration");
+ assert_equals(pc.getConfiguration().iceCandidatePoolSize, 0);
+}, "Initialize a new RTCPeerConnection with no iceCandidatePoolSize");
+
+test(() => {
+ const pc = new RTCPeerConnection({
+ iceCandidatePoolSize: 0
+ });
+ assert_idl_attribute(pc, "getConfiguration");
+ assert_equals(pc.getConfiguration().iceCandidatePoolSize, 0);
+}, "Initialize a new RTCPeerConnection with iceCandidatePoolSize: 0");
+
+test(() => {
+ const pc = new RTCPeerConnection({
+ iceCandidatePoolSize: 255
+ });
+ assert_idl_attribute(pc, "getConfiguration");
+ assert_equals(pc.getConfiguration().iceCandidatePoolSize, 255);
+}, "Initialize a new RTCPeerConnection with iceCandidatePoolSize: 255");
+
+test(() => {
+ assert_throws_js(TypeError, () => {
+ new RTCPeerConnection({
+ iceCandidatePoolSize: -1
+ });
+ });
+}, "Initialize a new RTCPeerConnection with iceCandidatePoolSize: -1 (Out Of Range)");
+
+test(() => {
+ assert_throws_js(TypeError, () => {
+ new RTCPeerConnection({
+ iceCandidatePoolSize: 256
+ });
+ });
+}, "Initialize a new RTCPeerConnection with iceCandidatePoolSize: 256 (Out Of Range)");
+
+
+/*
+Reconfiguration
+*/
+
+test(() => {
+ const pc = new RTCPeerConnection();
+ assert_idl_attribute(pc, "getConfiguration");
+ assert_idl_attribute(pc, "setConfiguration");
+ pc.setConfiguration({
+ iceCandidatePoolSize: 0
+ });
+ assert_equals(pc.getConfiguration().iceCandidatePoolSize, 0);
+}, "Reconfigure RTCPeerConnection instance iceCandidatePoolSize to 0");
+
+test(() => {
+ const pc = new RTCPeerConnection();
+ assert_idl_attribute(pc, "getConfiguration");
+ assert_idl_attribute(pc, "setConfiguration");
+ pc.setConfiguration({
+ iceCandidatePoolSize: 255
+ });
+ assert_equals(pc.getConfiguration().iceCandidatePoolSize, 255);
+}, "Reconfigure RTCPeerConnection instance iceCandidatePoolSize to 255");
+
+/*
+The following tests include an explicit assertion for the existence of a
+setConfiguration function to prevent the assert_throws_js from catching the
+TypeError object that will be thrown when attempting to call the
+non-existent setConfiguration method (in cases where it has not yet
+been implemented). Without this check, these tests will pass incorrectly.
+*/
+
+test(() => {
+ const pc = new RTCPeerConnection();
+ assert_equals(typeof pc.setConfiguration, "function", "RTCPeerConnection.prototype.setConfiguration is not implemented");
+ assert_throws_js(TypeError, () => {
+ pc.setConfiguration({
+ iceCandidatePoolSize: -1
+ });
+ });
+}, "Reconfigure RTCPeerConnection instance iceCandidatePoolSize to -1 (Out Of Range)");
+
+test(() => {
+ const pc = new RTCPeerConnection();
+ assert_equals(typeof pc.setConfiguration, "function", "RTCPeerConnection.prototype.setConfiguration is not implemented");
+ assert_throws_js(TypeError, () => {
+ pc.setConfiguration({
+ iceCandidatePoolSize: 256
+ });
+ });
+}, "Reconfigure RTCPeerConnection instance iceCandidatePoolSize to 256 (Out Of Range)");
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCConfiguration-iceServers.html b/testing/web-platform/tests/webrtc/RTCConfiguration-iceServers.html
new file mode 100644
index 0000000000..1893ba02f3
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCConfiguration-iceServers.html
@@ -0,0 +1,330 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCConfiguration iceServers</title>
+<script src='/resources/testharness.js'></script>
+<script src='/resources/testharnessreport.js'></script>
+<script src='RTCConfiguration-helper.js'></script>
+<script>
+ 'use strict';
+
+ // Test is based on the following editor's draft:
+ // https://w3c.github.io/webrtc-pc/archives/20170605/webrtc.html
+
+ // The following helper function is called from
+ // RTCConfiguration-helper.js:
+ // config_test
+
+ /*
+ 4.3.2. Interface Definition
+ [Constructor(optional RTCConfiguration configuration)]
+ interface RTCPeerConnection : EventTarget {
+ ...
+ };
+
+ 4.2.1. RTCConfiguration Dictionary
+ dictionary RTCConfiguration {
+ sequence<RTCIceServer> iceServers = [];
+ ...
+ };
+
+ 4.2.4. RTCIceServer Dictionary
+ dictionary RTCIceServer {
+ required (DOMString or sequence<DOMString>) urls;
+ DOMString username;
+ DOMString credential;
+ };
+ */
+
+ test(() => {
+ const pc = new RTCPeerConnection();
+ assert_array_equals(pc.getConfiguration().iceServers, []);
+ }, 'new RTCPeerConnection() should have default configuration.iceServers of undefined');
+
+ config_test(makePc => {
+ makePc({});
+ }, '{} should succeed');
+
+ config_test(makePc => {
+ assert_throws_js(TypeError, () =>
+ makePc({ iceServers: null }));
+ }, '{ iceServers: null } should throw TypeError');
+
+ config_test(makePc => {
+ const pc = makePc({ iceServers: undefined });
+ assert_array_equals(pc.getConfiguration().iceServers, []);
+ }, '{ iceServers: undefined } should succeed');
+
+ config_test(makePc => {
+ const pc = makePc({ iceServers: [] });
+ assert_array_equals(pc.getConfiguration().iceServers, []);
+ }, '{ iceServers: [] } should succeed');
+
+ config_test(makePc => {
+ assert_throws_js(TypeError, () =>
+ makePc({ iceServers: [null] }));
+ }, '{ iceServers: [null] } should throw TypeError');
+
+ config_test(makePc => {
+ assert_throws_js(TypeError, () =>
+ makePc({ iceServers: [undefined] }));
+ }, '{ iceServers: [undefined] } should throw TypeError');
+
+ config_test(makePc => {
+ assert_throws_js(TypeError, () =>
+ makePc({ iceServers: [{}] }));
+ }, '{ iceServers: [{}] } should throw TypeError');
+
+ config_test(makePc => {
+ const pc = makePc({ iceServers: [{
+ urls: 'stun:stun1.example.net'
+ }] });
+
+ const { iceServers } = pc.getConfiguration();
+ assert_equals(iceServers.length, 1);
+
+ const server = iceServers[0];
+ assert_array_equals(server.urls, ['stun:stun1.example.net']);
+
+ }, `with stun server should succeed`);
+
+ config_test(makePc => {
+ const pc = makePc({ iceServers: [{
+ urls: ['stun:stun1.example.net']
+ }] });
+
+ const { iceServers } = pc.getConfiguration();
+ assert_equals(iceServers.length, 1);
+
+ const server = iceServers[0];
+ assert_array_equals(server.urls, ['stun:stun1.example.net']);
+
+ }, `with stun server array should succeed`);
+
+ config_test(makePc => {
+ const pc = makePc({ iceServers: [{
+ urls: ['stun:stun1.example.net', 'stun:stun2.example.net']
+ }] });
+
+ const { iceServers } = pc.getConfiguration();
+ assert_equals(iceServers.length, 1);
+
+ const server = iceServers[0];
+ assert_array_equals(server.urls, ['stun:stun1.example.net', 'stun:stun2.example.net']);
+
+ }, `with 2 stun servers should succeed`);
+
+ config_test(makePc => {
+ const pc = makePc({ iceServers: [{
+ urls: 'turn:turn.example.org',
+ username: 'user',
+ credential: 'cred'
+ }] });
+
+ const { iceServers } = pc.getConfiguration();
+ assert_equals(iceServers.length, 1);
+
+ const server = iceServers[0];
+ assert_array_equals(server.urls, ['turn:turn.example.org']);
+ assert_equals(server.username, 'user');
+ assert_equals(server.credential, 'cred');
+
+ }, `with turn server, username, credential should succeed`);
+
+ config_test(makePc => {
+ const pc = makePc({ iceServers: [{
+ urls: 'turns:turn.example.org',
+ username: '',
+ credential: ''
+ }] });
+
+ const { iceServers } = pc.getConfiguration();
+ assert_equals(iceServers.length, 1);
+
+ const server = iceServers[0];
+ assert_array_equals(server.urls, ['turns:turn.example.org']);
+ assert_equals(server.username, '');
+ assert_equals(server.credential, '');
+
+ }, `with turns server and empty string username, credential should succeed`);
+
+ config_test(makePc => {
+ const pc = makePc({ iceServers: [{
+ urls: 'turn:turn.example.org',
+ username: '',
+ credential: ''
+ }] });
+
+ const { iceServers } = pc.getConfiguration();
+ assert_equals(iceServers.length, 1);
+
+ const server = iceServers[0];
+ assert_array_equals(server.urls, ['turn:turn.example.org']);
+ assert_equals(server.username, '');
+ assert_equals(server.credential, '');
+
+ }, `with turn server and empty string username, credential should succeed`);
+
+ config_test(makePc => {
+ const pc = makePc({ iceServers: [{
+ urls: ['turns:turn.example.org', 'turn:turn.example.net'],
+ username: 'user',
+ credential: 'cred'
+ }] });
+
+ const { iceServers } = pc.getConfiguration();
+ assert_equals(iceServers.length, 1);
+
+ const server = iceServers[0];
+ assert_array_equals(server.urls, ['turns:turn.example.org', 'turn:turn.example.net']);
+ assert_equals(server.username, 'user');
+ assert_equals(server.credential, 'cred');
+
+ }, `with one turns server, one turn server, username, credential should succeed`);
+
+ /*
+ 4.3.2. To set a configuration
+ 11.4. If scheme name is turn or turns, and either of server.username or
+ server.credential are omitted, then throw an InvalidAccessError.
+ */
+ config_test(makePc => {
+ assert_throws_dom('InvalidAccessError', () =>
+ makePc({ iceServers: [{
+ urls: 'turn:turn.example.net'
+ }] }));
+ }, 'with turn server and no credentials should throw InvalidAccessError');
+
+ config_test(makePc => {
+ assert_throws_dom('InvalidAccessError', () =>
+ makePc({ iceServers: [{
+ urls: 'turn:turn.example.net',
+ username: 'user'
+ }] }));
+ }, 'with turn server and only username should throw InvalidAccessError');
+
+ config_test(makePc => {
+ assert_throws_dom('InvalidAccessError', () =>
+ makePc({ iceServers: [{
+ urls: 'turn:turn.example.net',
+ credential: 'cred'
+ }] }));
+ }, 'with turn server and only credential should throw InvalidAccessError');
+
+ config_test(makePc => {
+ assert_throws_dom('InvalidAccessError', () =>
+ makePc({ iceServers: [{
+ urls: 'turns:turn.example.net'
+ }] }));
+ }, 'with turns server and no credentials should throw InvalidAccessError');
+
+ config_test(makePc => {
+ assert_throws_dom('InvalidAccessError', () =>
+ makePc({ iceServers: [{
+ urls: 'turns:turn.example.net',
+ username: 'user'
+ }] }));
+ }, 'with turns server and only username should throw InvalidAccessError');
+
+ config_test(makePc => {
+ assert_throws_dom('InvalidAccessError', () =>
+ makePc({ iceServers: [{
+ urls: 'turns:turn.example.net',
+ credential: 'cred'
+ }] }));
+ }, 'with turns server and only credential should throw InvalidAccessError');
+
+ /*
+ 4.3.2. To set a configuration
+ 11.3. For each url in server.urls parse url and obtain scheme name.
+ - If the scheme name is not implemented by the browser, throw a SyntaxError.
+ - or if parsing based on the syntax defined in [ RFC7064] and [RFC7065] fails,
+ throw a SyntaxError.
+
+ [RFC7064] URI Scheme for the Session Traversal Utilities for NAT (STUN) Protocol
+ 3.1. URI Scheme Syntax
+ stunURI = scheme ":" host [ ":" port ]
+ scheme = "stun" / "stuns"
+
+ [RFC7065] Traversal Using Relays around NAT (TURN) Uniform Resource Identifiers
+ 3.1. URI Scheme Syntax
+ turnURI = scheme ":" host [ ":" port ]
+ [ "?transport=" transport ]
+ scheme = "turn" / "turns"
+ transport = "udp" / "tcp" / transport-ext
+ transport-ext = 1*unreserved
+ */
+ config_test(makePc => {
+ assert_throws_dom("SyntaxError", () =>
+ makePc({ iceServers: [{
+ urls: ''
+ }] }));
+ }, 'with "" url should throw SyntaxError');
+
+ config_test(makePc => {
+ assert_throws_dom("SyntaxError", () =>
+ makePc({ iceServers: [{
+ urls: ['stun:stun1.example.net', '']
+ }] }));
+ }, 'with ["stun:stun1.example.net", ""] url should throw SyntaxError');
+
+ config_test(makePc => {
+ assert_throws_dom("SyntaxError", () =>
+ makePc({ iceServers: [{
+ urls: 'relative-url'
+ }] }));
+ }, 'with relative url should throw SyntaxError');
+
+ config_test(makePc => {
+ assert_throws_dom("SyntaxError", () =>
+ makePc({ iceServers: [{
+ urls: 'http://example.com'
+ }] }));
+ }, 'with http url should throw SyntaxError');
+
+ config_test(makePc => {
+ assert_throws_dom("SyntaxError", () =>
+ makePc({ iceServers: [{
+ urls: 'turn://example.org/foo?x=y'
+ }] }));
+ }, 'with invalid turn url should throw SyntaxError');
+
+ config_test(makePc => {
+ assert_throws_dom("SyntaxError", () =>
+ makePc({ iceServers: [{
+ urls: 'stun://example.org/foo?x=y'
+ }] }));
+ }, 'with invalid stun url should throw SyntaxError');
+
+ config_test(makePc => {
+ assert_throws_dom("SyntaxError", () =>
+ makePc({ iceServers: [{
+ urls: []
+ }] }));
+ }, `with empty urls should throw SyntaxError`);
+
+ // Blink and Gecko fall back to url, but it's not in the spec.
+ config_test(makePc => {
+ assert_throws_js(TypeError, () =>
+ makePc({ iceServers: [{
+ url: 'stun:stun1.example.net'
+ }] }));
+ }, 'with url field should throw TypeError');
+
+ /*
+ 4.3.2. To set a configuration
+ 11.5. If scheme name is turn or turns,
+ and server.credential is not a DOMString, then throw an InvalidAccessError
+ and abort these steps.
+ */
+ config_test(makePc => {
+ assert_throws_dom('InvalidAccessError', () =>
+ makePc({ iceServers: [{
+ urls: 'turns:turn.example.org',
+ username: 'user',
+ credential: {
+ macKey: '',
+ accessToken: ''
+ }
+ }] }));
+ }, 'with turns server, and object credential should throw InvalidAccessError');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCConfiguration-iceTransportPolicy.html b/testing/web-platform/tests/webrtc/RTCConfiguration-iceTransportPolicy.html
new file mode 100644
index 0000000000..ebc79048a3
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCConfiguration-iceTransportPolicy.html
@@ -0,0 +1,306 @@
+<!doctype html>
+<meta name="timeout" content="long">
+<title>RTCConfiguration iceTransportPolicy</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="RTCConfiguration-helper.js"></script>
+<script src="RTCPeerConnection-helper.js"></script>
+<script>
+ 'use strict';
+
+ // Test is based on the following editor draft:
+ // https://w3c.github.io/webrtc-pc/archives/20170605/webrtc.html
+
+ // The following helper function is called from RTCConfiguration-helper.js:
+ // config_test
+
+ /*
+ [Constructor(optional RTCConfiguration configuration)]
+ interface RTCPeerConnection : EventTarget {
+ RTCConfiguration getConfiguration();
+ void setConfiguration(RTCConfiguration configuration);
+ ...
+ };
+
+ dictionary RTCConfiguration {
+ sequence<RTCIceServer> iceServers;
+ RTCIceTransportPolicy iceTransportPolicy = "all";
+ };
+
+ enum RTCIceTransportPolicy {
+ "relay",
+ "all"
+ };
+ */
+
+ test(() => {
+ const pc = new RTCPeerConnection();
+ assert_equals(pc.getConfiguration().iceTransportPolicy, 'all');
+ }, `new RTCPeerConnection() should have default iceTransportPolicy all`);
+
+ test(() => {
+ const pc = new RTCPeerConnection({ iceTransportPolicy: undefined });
+ assert_equals(pc.getConfiguration().iceTransportPolicy, 'all');
+ }, `new RTCPeerConnection({ iceTransportPolicy: undefined }) should have default iceTransportPolicy all`);
+
+ test(() => {
+ const pc = new RTCPeerConnection({ iceTransportPolicy: 'all' });
+ assert_equals(pc.getConfiguration().iceTransportPolicy, 'all');
+ }, `new RTCPeerConnection({ iceTransportPolicy: 'all' }) should succeed`);
+
+ test(() => {
+ const pc = new RTCPeerConnection({ iceTransportPolicy: 'relay' });
+ assert_equals(pc.getConfiguration().iceTransportPolicy, 'relay');
+ }, `new RTCPeerConnection({ iceTransportPolicy: 'relay' }) should succeed`);
+
+ /*
+ 4.3.2. Set a configuration
+ 8. Set the ICE Agent's ICE transports setting to the value of
+ configuration.iceTransportPolicy. As defined in [JSEP] (section 4.1.16.),
+ if the new ICE transports setting changes the existing setting, no action
+ will be taken until the next gathering phase. If a script wants this to
+ happen immediately, it should do an ICE restart.
+ */
+ test(() => {
+ const pc = new RTCPeerConnection({ iceTransportPolicy: 'all' });
+ assert_equals(pc.getConfiguration().iceTransportPolicy, 'all');
+
+ pc.setConfiguration({ iceTransportPolicy: 'relay' });
+ assert_equals(pc.getConfiguration().iceTransportPolicy, 'relay');
+ }, `setConfiguration({ iceTransportPolicy: 'relay' }) with initial iceTransportPolicy all should succeed`);
+
+ test(() => {
+ const pc = new RTCPeerConnection({ iceTransportPolicy: 'relay' });
+ assert_equals(pc.getConfiguration().iceTransportPolicy, 'relay');
+
+ pc.setConfiguration({ iceTransportPolicy: 'all' });
+ assert_equals(pc.getConfiguration().iceTransportPolicy, 'all');
+ }, `setConfiguration({ iceTransportPolicy: 'all' }) with initial iceTransportPolicy relay should succeed`);
+
+ test(() => {
+ const pc = new RTCPeerConnection({ iceTransportPolicy: 'relay' });
+ assert_equals(pc.getConfiguration().iceTransportPolicy, 'relay');
+
+ // default value for iceTransportPolicy is all
+ pc.setConfiguration({});
+ assert_equals(pc.getConfiguration().iceTransportPolicy, 'all');
+ }, `setConfiguration({}) with initial iceTransportPolicy relay should set new value to all`);
+
+ config_test(makePc => {
+ assert_throws_js(TypeError, () =>
+ makePc({ iceTransportPolicy: 'invalid' }));
+ }, `with invalid iceTransportPolicy should throw TypeError`);
+
+ // "none" is in Blink and Gecko's IDL, but not in the spec.
+ config_test(makePc => {
+ assert_throws_js(TypeError, () =>
+ makePc({ iceTransportPolicy: 'none' }));
+ }, `with none iceTransportPolicy should throw TypeError`);
+
+ config_test(makePc => {
+ assert_throws_js(TypeError, () =>
+ makePc({ iceTransportPolicy: null }));
+ }, `with null iceTransportPolicy should throw TypeError`);
+
+ // iceTransportPolicy is called iceTransports in Blink.
+ test(() => {
+ const pc = new RTCPeerConnection({ iceTransports: 'relay' });
+ assert_equals(pc.getConfiguration().iceTransportPolicy, 'all');
+ }, `new RTCPeerConnection({ iceTransports: 'relay' }) should have no effect`);
+
+ test(() => {
+ const pc = new RTCPeerConnection({ iceTransports: 'invalid' });
+ assert_equals(pc.getConfiguration().iceTransportPolicy, 'all');
+ }, `new RTCPeerConnection({ iceTransports: 'invalid' }) should have no effect`);
+
+ test(() => {
+ const pc = new RTCPeerConnection({ iceTransports: null });
+ assert_equals(pc.getConfiguration().iceTransportPolicy, 'all');
+ }, `new RTCPeerConnection({ iceTransports: null }) should have no effect`);
+
+ const getLines = (sdp, startsWith) =>
+ sdp.split('\r\n').filter(l => l.startsWith(startsWith));
+
+ const getUfrags = ({sdp}) => getLines(sdp, 'a=ice-ufrag:');
+
+ promise_test(async t => {
+ const offerer = new RTCPeerConnection({iceTransportPolicy: 'relay'});
+ t.add_cleanup(() => offerer.close());
+
+ offerer.addEventListener('icecandidate',
+ e => assert_equals(e.candidate, null, 'Should get no ICE candidates'));
+
+ offerer.addTransceiver('audio');
+ await offerer.setLocalDescription();
+
+ await waitForIceGatheringState(offerer, ['complete']);
+ }, `iceTransportPolicy "relay" on offerer should prevent candidate gathering`);
+
+ promise_test(async t => {
+ const offerer = new RTCPeerConnection();
+ const answerer = new RTCPeerConnection({iceTransportPolicy: 'relay'});
+ t.add_cleanup(() => offerer.close());
+ t.add_cleanup(() => answerer.close());
+
+ answerer.addEventListener('icecandidate',
+ e => assert_equals(e.candidate, null, 'Should get no ICE candidates'));
+
+ offerer.addTransceiver('audio');
+ const offer = await offerer.createOffer();
+ await answerer.setRemoteDescription(offer);
+ await answerer.setLocalDescription(await answerer.createAnswer());
+ await waitForIceGatheringState(answerer, ['complete']);
+ }, `iceTransportPolicy "relay" on answerer should prevent candidate gathering`);
+
+ promise_test(async t => {
+ const offerer = new RTCPeerConnection();
+ const answerer = new RTCPeerConnection();
+ t.add_cleanup(() => offerer.close());
+ t.add_cleanup(() => answerer.close());
+
+ offerer.addTransceiver('audio');
+
+ exchangeIceCandidates(offerer, answerer);
+
+ await Promise.all([
+ exchangeOfferAnswer(offerer, answerer),
+ listenToIceConnected(offerer),
+ listenToIceConnected(answerer),
+ waitForIceGatheringState(offerer, ['complete']),
+ waitForIceGatheringState(answerer, ['complete'])
+ ]);
+
+ const [oldUfrag] = getUfrags(offerer.localDescription);
+
+ offerer.setConfiguration({iceTransportPolicy: 'relay'});
+
+ offerer.addEventListener('icecandidate',
+ e => assert_equals(e.candidate, null, 'Should get no ICE candidates'));
+
+ await Promise.all([
+ exchangeOfferAnswer(offerer, answerer),
+ waitForIceStateChange(offerer, ['failed']),
+ waitForIceStateChange(answerer, ['failed']),
+ waitForIceGatheringState(offerer, ['complete']),
+ waitForIceGatheringState(answerer, ['complete'])
+ ]);
+
+ const [newUfrag] = getUfrags(offerer.localDescription);
+ assert_not_equals(oldUfrag, newUfrag,
+ 'Changing iceTransportPolicy should prompt an ICE restart');
+ }, `Changing iceTransportPolicy from "all" to "relay" causes an ICE restart which should fail, with no new candidates`);
+
+ promise_test(async t => {
+ const offerer = new RTCPeerConnection({iceTransportPolicy: 'relay'});
+ const answerer = new RTCPeerConnection();
+ t.add_cleanup(() => offerer.close());
+ t.add_cleanup(() => answerer.close());
+
+ offerer.addTransceiver('audio');
+
+ exchangeIceCandidates(offerer, answerer);
+
+ const checkNoCandidate =
+ e => assert_equals(e.candidate, null, 'Should get no ICE candidates');
+
+ offerer.addEventListener('icecandidate', checkNoCandidate);
+
+ await Promise.all([
+ exchangeOfferAnswer(offerer, answerer),
+ waitForIceStateChange(offerer, ['failed']),
+ waitForIceStateChange(answerer, ['failed']),
+ waitForIceGatheringState(offerer, ['complete']),
+ waitForIceGatheringState(answerer, ['complete'])
+ ]);
+
+ const [oldUfrag] = getUfrags(offerer.localDescription);
+
+ offerer.setConfiguration({iceTransportPolicy: 'all'});
+
+ offerer.removeEventListener('icecandidate', checkNoCandidate);
+
+ await Promise.all([
+ exchangeOfferAnswer(offerer, answerer),
+ listenToIceConnected(offerer),
+ listenToIceConnected(answerer),
+ waitForIceGatheringState(offerer, ['complete']),
+ waitForIceGatheringState(answerer, ['complete'])
+ ]);
+
+ const [newUfrag] = getUfrags(offerer.localDescription);
+ assert_not_equals(oldUfrag, newUfrag,
+ 'Changing iceTransportPolicy should prompt an ICE restart');
+ }, `Changing iceTransportPolicy from "relay" to "all" causes an ICE restart which should succeed`);
+
+ promise_test(async t => {
+ const offerer = new RTCPeerConnection();
+ const answerer = new RTCPeerConnection();
+ t.add_cleanup(() => offerer.close());
+ t.add_cleanup(() => answerer.close());
+
+ offerer.addTransceiver('audio');
+
+ exchangeIceCandidates(offerer, answerer);
+
+ await Promise.all([
+ exchangeOfferAnswer(offerer, answerer),
+ listenToIceConnected(offerer),
+ listenToIceConnected(answerer),
+ waitForIceGatheringState(offerer, ['complete']),
+ waitForIceGatheringState(answerer, ['complete'])
+ ]);
+
+ const [oldUfrag] = getUfrags(offerer.localDescription);
+
+ offerer.setConfiguration({iceTransportPolicy: 'relay'});
+ offerer.setConfiguration({iceTransportPolicy: 'all'});
+
+ await Promise.all([
+ exchangeOfferAnswer(offerer, answerer),
+ listenToIceConnected(offerer),
+ listenToIceConnected(answerer),
+ waitForIceGatheringState(offerer, ['complete']),
+ waitForIceGatheringState(answerer, ['complete'])
+ ]);
+
+ const [newUfrag] = getUfrags(offerer.localDescription);
+ assert_not_equals(oldUfrag, newUfrag,
+ 'Changing iceTransportPolicy should prompt an ICE restart');
+ }, `Changing iceTransportPolicy from "all" to "relay", and back to "all" prompts an ICE restart`);
+
+ promise_test(async t => {
+ const offerer = new RTCPeerConnection();
+ const answerer = new RTCPeerConnection();
+ t.add_cleanup(() => offerer.close());
+ t.add_cleanup(() => answerer.close());
+
+ offerer.addTransceiver('audio');
+
+ exchangeIceCandidates(offerer, answerer);
+
+ await Promise.all([
+ exchangeOfferAnswer(offerer, answerer),
+ listenToIceConnected(offerer),
+ listenToIceConnected(answerer),
+ waitForIceGatheringState(offerer, ['complete']),
+ waitForIceGatheringState(answerer, ['complete'])
+ ]);
+
+ const [oldUfrag] = getUfrags(answerer.localDescription);
+
+ answerer.setConfiguration({iceTransportPolicy: 'relay'});
+
+ await Promise.all([
+ exchangeOfferAnswer(offerer, answerer),
+ listenToIceConnected(offerer),
+ listenToIceConnected(answerer),
+ waitForIceGatheringState(offerer, ['complete']),
+ waitForIceGatheringState(answerer, ['complete'])
+ ]);
+
+ const [newUfrag] = getUfrags(answerer.localDescription);
+ assert_equals(oldUfrag, newUfrag,
+ 'Changing iceTransportPolicy on answerer should not effect ufrag');
+ }, `Changing iceTransportPolicy from "all" to "relay" on the answerer has no effect on a subsequent offer/answer`);
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCConfiguration-rtcpMuxPolicy.html b/testing/web-platform/tests/webrtc/RTCConfiguration-rtcpMuxPolicy.html
new file mode 100644
index 0000000000..48e772fb51
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCConfiguration-rtcpMuxPolicy.html
@@ -0,0 +1,196 @@
+<!doctype html>
+<title>RTCConfiguration rtcpMuxPolicy</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="RTCConfiguration-helper.js"></script>
+<script src="RTCPeerConnection-helper.js"></script>
+<script>
+ 'use strict';
+
+ // Test is based on the following editor draft:
+ // https://w3c.github.io/webrtc-pc/archives/20170605/webrtc.html
+
+ // The following helper function is called from RTCConfiguration-helper.js:
+ // config_test
+
+ /*
+ [Constructor(optional RTCConfiguration configuration)]
+ interface RTCPeerConnection : EventTarget {
+ RTCConfiguration getConfiguration();
+ void setConfiguration(RTCConfiguration configuration);
+ ...
+ };
+
+ dictionary RTCConfiguration {
+ RTCRtcpMuxPolicy rtcpMuxPolicy = "require";
+ ...
+ };
+
+ enum RTCRtcpMuxPolicy {
+ "negotiate",
+ "require"
+ };
+ */
+
+ test(() => {
+ const pc = new RTCPeerConnection();
+ assert_equals(pc.getConfiguration().rtcpMuxPolicy, 'require');
+ }, `new RTCPeerConnection() should have default rtcpMuxPolicy require`);
+
+ test(() => {
+ const pc = new RTCPeerConnection({ rtcpMuxPolicy: undefined });
+ assert_equals(pc.getConfiguration().rtcpMuxPolicy, 'require');
+ }, `new RTCPeerConnection({ rtcpMuxPolicy: undefined }) should have default rtcpMuxPolicy require`);
+
+ test(() => {
+ const pc = new RTCPeerConnection({ rtcpMuxPolicy: 'require' });
+ assert_equals(pc.getConfiguration().rtcpMuxPolicy, 'require');
+ }, `new RTCPeerConnection({ rtcpMuxPolicy: 'require' }) should succeed`);
+
+ /*
+ 4.3.1.1. Constructor
+ 3. If configuration.rtcpMuxPolicy is negotiate, and the user agent does not
+ implement non-muxed RTCP, throw a NotSupportedError.
+ */
+ test(() => {
+ let pc;
+ try {
+ pc = new RTCPeerConnection({ rtcpMuxPolicy: 'negotiate' });
+ } catch(err) {
+ // NotSupportedError is a DOMException with code 9
+ if(err.code === 9 && err.name === 'NotSupportedError') {
+ // ignore error and pass test if negotiate is not supported
+ return;
+ } else {
+ throw err;
+ }
+ }
+
+ assert_equals(pc.getConfiguration().rtcpMuxPolicy, 'negotiate');
+
+ }, `new RTCPeerConnection({ rtcpMuxPolicy: 'negotiate' }) may succeed or throw NotSupportedError`);
+
+ config_test(makePc => {
+ assert_throws_js(TypeError, () =>
+ makePc({ rtcpMuxPolicy: null }));
+ }, `with { rtcpMuxPolicy: null } should throw TypeError`);
+
+ config_test(makePc => {
+ assert_throws_js(TypeError, () =>
+ makePc({ rtcpMuxPolicy: 'invalid' }));
+ }, `with { rtcpMuxPolicy: 'invalid' } should throw TypeError`);
+
+ /*
+ 4.3.2. Set a configuration
+ 6. If configuration.rtcpMuxPolicy is set and its value differs from the
+ connection's rtcpMux policy, throw an InvalidModificationError.
+ */
+
+ test(() => {
+ const pc = new RTCPeerConnection({ rtcpMuxPolicy: 'require' });
+ assert_idl_attribute(pc, 'setConfiguration');
+ assert_throws_dom('InvalidModificationError', () =>
+ pc.setConfiguration({ rtcpMuxPolicy: 'negotiate' }));
+
+ }, `setConfiguration({ rtcpMuxPolicy: 'negotiate' }) with initial rtcpMuxPolicy require should throw InvalidModificationError`);
+
+ test(() => {
+ let pc;
+ try {
+ pc = new RTCPeerConnection({ rtcpMuxPolicy: 'negotiate' });
+ } catch(err) {
+ // NotSupportedError is a DOMException with code 9
+ if(err.code === 9 && err.name === 'NotSupportedError') {
+ // ignore error and pass test if negotiate is not supported
+ return;
+ } else {
+ throw err;
+ }
+ }
+
+ assert_idl_attribute(pc, 'setConfiguration');
+ assert_throws_dom('InvalidModificationError', () =>
+ pc.setConfiguration({ rtcpMuxPolicy: 'require' }));
+
+ }, `setConfiguration({ rtcpMuxPolicy: 'require' }) with initial rtcpMuxPolicy negotiate should throw InvalidModificationError`);
+
+ test(() => {
+ let pc;
+ try {
+ pc = new RTCPeerConnection({ rtcpMuxPolicy: 'negotiate' });
+ } catch(err) {
+ // NotSupportedError is a DOMException with code 9
+ if(err.code === 9 && err.name === 'NotSupportedError') {
+ // ignore error and pass test if negotiate is not supported
+ return;
+ } else {
+ throw err;
+ }
+ }
+
+ assert_idl_attribute(pc, 'setConfiguration');
+ // default value for rtcpMuxPolicy is require
+ assert_throws_dom('InvalidModificationError', () =>
+ pc.setConfiguration({}));
+
+ }, `setConfiguration({}) with initial rtcpMuxPolicy negotiate should throw InvalidModificationError`);
+
+ /*
+ Coverage Report
+
+ Tested 2
+ Total 2
+ */
+ const FINGERPRINT_SHA256 = '00:00:00:00:00:00:00:00:00:00:00:00:00' +
+ ':00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00';
+ const ICEUFRAG = 'someufrag';
+ const ICEPWD = 'somelongpwdwithenoughrandomness';
+
+ promise_test(async t => {
+ // audio-only SDP offer without BUNDLE and rtcp-mux.
+ const sdp = 'v=0\r\n' +
+ 'o=- 166855176514521964 2 IN IP4 127.0.0.1\r\n' +
+ 's=-\r\n' +
+ 't=0 0\r\n' +
+ 'm=audio 9 UDP/TLS/RTP/SAVPF 111\r\n' +
+ 'c=IN IP4 0.0.0.0\r\n' +
+ 'a=rtcp:9 IN IP4 0.0.0.0\r\n' +
+ 'a=ice-ufrag:' + ICEUFRAG + '\r\n' +
+ 'a=ice-pwd:' + ICEPWD + '\r\n' +
+ 'a=fingerprint:sha-256 ' + FINGERPRINT_SHA256 + '\r\n' +
+ 'a=setup:actpass\r\n' +
+ 'a=mid:audio1\r\n' +
+ 'a=sendonly\r\n' +
+ 'a=rtcp-rsize\r\n' +
+ 'a=rtpmap:111 opus/48000/2\r\n';
+ const pc = new RTCPeerConnection({rtcpMuxPolicy: 'require'});
+ t.add_cleanup(() => pc.close());
+
+ return promise_rejects_dom(t, 'InvalidAccessError', pc.setRemoteDescription({type: 'offer', sdp}));
+ }, 'setRemoteDescription throws InvalidAccessError when called with an offer without rtcp-mux and rtcpMuxPolicy is set to require');
+
+ promise_test(async t => {
+ // audio-only SDP answer without BUNDLE and rtcp-mux.
+ // Also omitting a=mid in order to avoid parsing it from the offer as this needs to match.
+ const sdp = 'v=0\r\n' +
+ 'o=- 166855176514521964 2 IN IP4 127.0.0.1\r\n' +
+ 's=-\r\n' +
+ 't=0 0\r\n' +
+ 'm=audio 9 UDP/TLS/RTP/SAVPF 111\r\n' +
+ 'c=IN IP4 0.0.0.0\r\n' +
+ 'a=rtcp:9 IN IP4 0.0.0.0\r\n' +
+ 'a=ice-ufrag:' + ICEUFRAG + '\r\n' +
+ 'a=ice-pwd:' + ICEPWD + '\r\n' +
+ 'a=fingerprint:sha-256 ' + FINGERPRINT_SHA256 + '\r\n' +
+ 'a=setup:active\r\n' +
+ 'a=sendonly\r\n' +
+ 'a=rtcp-rsize\r\n' +
+ 'a=rtpmap:111 opus/48000/2\r\n';
+ const pc = new RTCPeerConnection({rtcpMuxPolicy: 'require'});
+ t.add_cleanup(() => pc.close());
+
+ const offer = await generateAudioReceiveOnlyOffer(pc);
+ await pc.setLocalDescription(offer);
+ return promise_rejects_dom(t, 'InvalidAccessError', pc.setRemoteDescription({type: 'answer', sdp}));
+ }, 'setRemoteDescription throws InvalidAccessError when called with an answer without rtcp-mux and rtcpMuxPolicy is set to require');
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCDTMFSender-helper.js b/testing/web-platform/tests/webrtc/RTCDTMFSender-helper.js
new file mode 100644
index 0000000000..4316c3804a
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCDTMFSender-helper.js
@@ -0,0 +1,149 @@
+'use strict';
+
+// Test is based on the following editor draft:
+// https://w3c.github.io/webrtc-pc/archives/20170605/webrtc.html
+
+// Code using this helper should also include RTCPeerConnection-helper.js
+// in the main HTML file
+
+// The following helper functions are called from RTCPeerConnection-helper.js:
+// getTrackFromUserMedia
+// exchangeOfferAnswer
+
+// Create a RTCDTMFSender using getUserMedia()
+// Connect the PeerConnection to another PC and wait until it is
+// properly connected, so that DTMF can be sent.
+function createDtmfSender(pc = new RTCPeerConnection()) {
+ let dtmfSender;
+ return getTrackFromUserMedia('audio')
+ .then(([track, mediaStream]) => {
+ const sender = pc.addTrack(track, mediaStream);
+ dtmfSender = sender.dtmf;
+ assert_true(dtmfSender instanceof RTCDTMFSender,
+ 'Expect audio sender.dtmf to be set to a RTCDTMFSender');
+ // Note: spec bug open - https://github.com/w3c/webrtc-pc/issues/1774
+ // on whether sending should be possible before negotiation.
+ const pc2 = new RTCPeerConnection();
+ Object.defineProperty(pc, 'otherPc', { value: pc2 });
+ exchangeIceCandidates(pc, pc2);
+ return exchangeOfferAnswer(pc, pc2);
+ }).then(() => {
+ if (!('canInsertDTMF' in dtmfSender)) {
+ return Promise.resolve();
+ }
+ // Wait until dtmfSender.canInsertDTMF becomes true.
+ // Up to 150 ms has been observed in test. Wait 1 second
+ // in steps of 10 ms.
+ // Note: Using a short timeout and rejected promise in order to
+ // make test return a clear error message on failure.
+ return new Promise((resolve, reject) => {
+ let counter = 0;
+ step_timeout(function checkCanInsertDTMF() {
+ if (dtmfSender.canInsertDTMF) {
+ resolve();
+ } else {
+ if (counter >= 100) {
+ reject('Waited too long for canInsertDTMF');
+ return;
+ }
+ ++counter;
+ step_timeout(checkCanInsertDTMF, 10);
+ }
+ }, 0);
+ });
+ }).then(() => {
+ return dtmfSender;
+ });
+}
+
+/*
+ Create an RTCDTMFSender and test tonechange events on it.
+ testFunc
+ Test function that is going to manipulate the DTMFSender.
+ It will be called with:
+ t - the test object
+ sender - the created RTCDTMFSender
+ pc - the associated RTCPeerConnection as second argument.
+ toneChanges
+ Array of expected tonechange events fired. The elements
+ are array of 3 items:
+ expectedTone
+ The expected character in event.tone
+ expectedToneBuffer
+ The expected new value of dtmfSender.toneBuffer
+ expectedDuration
+ The rough time since beginning or last tonechange event
+ was fired.
+ desc
+ Test description.
+ */
+function test_tone_change_events(testFunc, toneChanges, desc) {
+ // Convert to cumulative time
+ let cumulativeTime = 0;
+ const cumulativeToneChanges = toneChanges.map(c => {
+ cumulativeTime += c[2];
+ return [c[0], c[1], cumulativeTime];
+ });
+
+ // Wait for same duration as last expected duration + 100ms
+ // before passing test in case there are new tone events fired,
+ // in which case the test should fail.
+ const lastWait = toneChanges.pop()[2] + 100;
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ const dtmfSender = await createDtmfSender(pc);
+ const start = Date.now();
+
+ const allEventsReceived = new Promise(resolve => {
+ const onToneChange = t.step_func(ev => {
+ assert_true(ev instanceof RTCDTMFToneChangeEvent,
+ 'Expect tone change event object to be an RTCDTMFToneChangeEvent');
+
+ const { tone } = ev;
+ assert_equals(typeof tone, 'string',
+ 'Expect event.tone to be the tone string');
+
+ assert_greater_than(cumulativeToneChanges.length, 0,
+ 'More tonechange event is fired than expected');
+
+ const [
+ expectedTone, expectedToneBuffer, expectedTime
+ ] = cumulativeToneChanges.shift();
+
+ assert_equals(tone, expectedTone,
+ `Expect current event.tone to be ${expectedTone}`);
+
+ assert_equals(dtmfSender.toneBuffer, expectedToneBuffer,
+ `Expect dtmfSender.toneBuffer to be updated to ${expectedToneBuffer}`);
+
+ // We check that the cumulative delay is at least the expected one, but
+ // system load may cause random delays, so we do not put any
+ // realistic upper bound on the timing of the events.
+ assert_between_inclusive(Date.now() - start, expectedTime,
+ expectedTime + 4000,
+ `Expect tonechange event for "${tone}" to be fired approximately after ${expectedTime} milliseconds`);
+ if (cumulativeToneChanges.length === 0) {
+ resolve();
+ }
+ });
+
+ dtmfSender.addEventListener('tonechange', onToneChange);
+ });
+
+ testFunc(t, dtmfSender, pc);
+ await allEventsReceived;
+ const wait = ms => new Promise(resolve => t.step_timeout(resolve, ms));
+ await wait(lastWait);
+ }, desc);
+}
+
+// Get the one and only tranceiver from pc.getTransceivers().
+// Assumes that there is only one tranceiver in pc.
+function getTransceiver(pc) {
+ const transceivers = pc.getTransceivers();
+ assert_equals(transceivers.length, 1,
+ 'Expect there to be only one tranceiver in pc');
+
+ return transceivers[0];
+}
diff --git a/testing/web-platform/tests/webrtc/RTCDTMFSender-insertDTMF.https.html b/testing/web-platform/tests/webrtc/RTCDTMFSender-insertDTMF.https.html
new file mode 100644
index 0000000000..71cfe70171
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCDTMFSender-insertDTMF.https.html
@@ -0,0 +1,176 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCDTMFSender.prototype.insertDTMF</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="RTCPeerConnection-helper.js"></script>
+<script src="RTCDTMFSender-helper.js"></script>
+<script>
+ 'use strict';
+
+ // Test is based on the following editor draft:
+ // https://w3c.github.io/webrtc-pc/archives/20170605/webrtc.html
+
+ // The following helper functions are called from RTCPeerConnection-helper.js
+ // generateAnswer
+
+ // The following helper functions are called from RTCDTMFSender-helper.js
+ // createDtmfSender
+ // test_tone_change_events
+ // getTransceiver
+
+ /*
+ 7. Peer-to-peer DTMF
+ partial interface RTCRtpSender {
+ readonly attribute RTCDTMFSender? dtmf;
+ };
+
+ interface RTCDTMFSender : EventTarget {
+ void insertDTMF(DOMString tones,
+ optional unsigned long duration = 100,
+ optional unsigned long interToneGap = 70);
+ attribute EventHandler ontonechange;
+ readonly attribute DOMString toneBuffer;
+ };
+ */
+
+ /*
+ 7.2. insertDTMF
+ The tones parameter is treated as a series of characters.
+
+ The characters 0 through 9, A through D, #, and * generate the associated
+ DTMF tones.
+
+ The characters a to d MUST be normalized to uppercase on entry and are
+ equivalent to A to D.
+
+ As noted in [RTCWEB-AUDIO] Section 3, support for the characters 0 through 9,
+ A through D, #, and * are required.
+
+ The character ',' MUST be supported, and indicates a delay of 2 seconds
+ before processing the next character in the tones parameter.
+
+ All other characters (and only those other characters) MUST be considered
+ unrecognized.
+ */
+ promise_test(async t => {
+ const dtmfSender = await createDtmfSender();
+ dtmfSender.insertDTMF('');
+ dtmfSender.insertDTMF('012345689');
+ dtmfSender.insertDTMF('ABCD');
+ dtmfSender.insertDTMF('abcd');
+ dtmfSender.insertDTMF('#*');
+ dtmfSender.insertDTMF(',');
+ dtmfSender.insertDTMF('0123456789ABCDabcd#*,');
+ }, 'insertDTMF() should succeed if tones contains valid DTMF characters');
+
+
+ /*
+ 7.2. insertDTMF
+ 6. If tones contains any unrecognized characters, throw an
+ InvalidCharacterError.
+ */
+ promise_test(async t => {
+ const dtmfSender = await createDtmfSender();
+ assert_throws_dom('InvalidCharacterError', () =>
+ // 'F' is invalid
+ dtmfSender.insertDTMF('123FFABC'));
+
+ assert_throws_dom('InvalidCharacterError', () =>
+ // 'E' is invalid
+ dtmfSender.insertDTMF('E'));
+
+ assert_throws_dom('InvalidCharacterError', () =>
+ // ' ' is invalid
+ dtmfSender.insertDTMF('# *'));
+ }, 'insertDTMF() should throw InvalidCharacterError if tones contains invalid DTMF characters');
+
+ /*
+ 7.2. insertDTMF
+ 3. If transceiver.stopped is true, throw an InvalidStateError.
+ */
+ test(t => {
+ const pc = new RTCPeerConnection();
+ const transceiver = pc.addTransceiver('audio');
+ const dtmfSender = transceiver.sender.dtmf;
+
+ transceiver.stop();
+ assert_throws_dom('InvalidStateError', () => dtmfSender.insertDTMF(''));
+
+ }, 'insertDTMF() should throw InvalidStateError if transceiver is stopped');
+
+ /*
+ 7.2. insertDTMF
+ 4. If transceiver.currentDirection is recvonly or inactive, throw an InvalidStateError.
+ */
+ promise_test(async t => {
+ const caller = new RTCPeerConnection();
+ t.add_cleanup(() => caller.close());
+ const callee = new RTCPeerConnection();
+ t.add_cleanup(() => callee.close());
+ const transceiver =
+ caller.addTransceiver('audio', { direction: 'recvonly' });
+ const dtmfSender = transceiver.sender.dtmf;
+
+ const offer = await caller.createOffer();
+ await caller.setLocalDescription(offer);
+ await callee.setRemoteDescription(offer);
+ const stream = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const [track] = stream.getTracks();
+ callee.addTrack(track, stream);
+ const answer = await callee.createAnswer();
+ await callee.setLocalDescription(answer);
+ await caller.setRemoteDescription(answer);
+ assert_equals(transceiver.currentDirection, 'recvonly');
+ assert_throws_dom('InvalidStateError', () => dtmfSender.insertDTMF(''));
+ }, 'insertDTMF() should throw InvalidStateError if transceiver.currentDirection is recvonly');
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const transceiver =
+ pc.addTransceiver('audio', { direction: 'inactive' });
+ const dtmfSender = transceiver.sender.dtmf;
+
+ const offer = await pc.createOffer();
+ await pc.setLocalDescription(offer);
+ const answer = await generateAnswer(offer);
+ await pc.setRemoteDescription(answer);
+ assert_equals(transceiver.currentDirection, 'inactive');
+ assert_throws_dom('InvalidStateError', () => dtmfSender.insertDTMF(''));
+ }, 'insertDTMF() should throw InvalidStateError if transceiver.currentDirection is inactive');
+
+ /*
+ 7.2. insertDTMF
+ The characters a to d MUST be normalized to uppercase on entry and are
+ equivalent to A to D.
+
+ 7. Set the object's toneBuffer attribute to tones.
+ */
+ promise_test(async t => {
+ const dtmfSender = await createDtmfSender();
+ dtmfSender.insertDTMF('123');
+ assert_equals(dtmfSender.toneBuffer, '123');
+
+ dtmfSender.insertDTMF('ABC');
+ assert_equals(dtmfSender.toneBuffer, 'ABC');
+
+ dtmfSender.insertDTMF('bcd');
+ assert_equals(dtmfSender.toneBuffer, 'BCD');
+ }, 'insertDTMF() should set toneBuffer to provided tones normalized, with old tones overridden');
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const [track, mediaStream] = await getTrackFromUserMedia('audio');
+ const sender = pc.addTrack(track, mediaStream);
+ await pc.setLocalDescription(await pc.createOffer());
+ const dtmfSender = sender.dtmf;
+ pc.removeTrack(sender);
+ pc.close();
+ assert_throws_dom('InvalidStateError', () =>
+ dtmfSender.insertDTMF('123'));
+ }, 'insertDTMF() after remove and close should reject');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCDTMFSender-ontonechange-long.https.html b/testing/web-platform/tests/webrtc/RTCDTMFSender-ontonechange-long.https.html
new file mode 100644
index 0000000000..852194d024
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCDTMFSender-ontonechange-long.https.html
@@ -0,0 +1,50 @@
+<!doctype html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<title>RTCDTMFSender.prototype.ontonechange (Long Timeout)</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="RTCPeerConnection-helper.js"></script>
+<script src="RTCDTMFSender-helper.js"></script>
+<script>
+ 'use strict';
+
+ // Test is based on the following editor draft:
+ // https://w3c.github.io/webrtc-pc/archives/20170605/webrtc.html
+
+ // The following helper functions are called from RTCDTMFSender-helper.js
+ // test_tone_change_events
+
+ /*
+ 7. Peer-to-peer DTMF
+ partial interface RTCRtpSender {
+ readonly attribute RTCDTMFSender? dtmf;
+ };
+
+ interface RTCDTMFSender : EventTarget {
+ void insertDTMF(DOMString tones,
+ optional unsigned long duration = 100,
+ optional unsigned long interToneGap = 70);
+ attribute EventHandler ontonechange;
+ readonly attribute DOMString toneBuffer;
+ };
+
+ [Constructor(DOMString type, RTCDTMFToneChangeEventInit eventInitDict)]
+ interface RTCDTMFToneChangeEvent : Event {
+ readonly attribute DOMString tone;
+ };
+ */
+
+ /*
+ 7.2. insertDTMF
+ 8. If the value of the duration parameter is less than 40, set it to 40.
+ If, on the other hand, the value is greater than 6000, set it to 6000.
+ */
+ test_tone_change_events((t, dtmfSender) => {
+ dtmfSender.insertDTMF('A', 8000, 70);
+ }, [
+ ['A', '', 0],
+ ['', '', 6070]
+ ],'insertDTMF with duration greater than 6000 should be clamped to 6000');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCDTMFSender-ontonechange.https.html b/testing/web-platform/tests/webrtc/RTCDTMFSender-ontonechange.https.html
new file mode 100644
index 0000000000..08dd6ada32
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCDTMFSender-ontonechange.https.html
@@ -0,0 +1,294 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCDTMFSender.prototype.ontonechange</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="RTCPeerConnection-helper.js"></script>
+<script src="RTCDTMFSender-helper.js"></script>
+<script>
+ 'use strict';
+
+ // Test is based on the following editor draft:
+ // https://w3c.github.io/webrtc-pc/archives/20170605/webrtc.html
+
+ // The following helper functions are called from RTCPeerConnection-helper.js
+ // generateAnswer
+
+ // The following helper functions are called from RTCDTMFSender-helper.js
+ // test_tone_change_events
+ // getTransceiver
+
+ /*
+ 7. Peer-to-peer DTMF
+ partial interface RTCRtpSender {
+ readonly attribute RTCDTMFSender? dtmf;
+ };
+
+ interface RTCDTMFSender : EventTarget {
+ void insertDTMF(DOMString tones,
+ optional unsigned long duration = 100,
+ optional unsigned long interToneGap = 70);
+ attribute EventHandler ontonechange;
+ readonly attribute DOMString toneBuffer;
+ };
+
+ [Constructor(DOMString type, RTCDTMFToneChangeEventInit eventInitDict)]
+ interface RTCDTMFToneChangeEvent : Event {
+ readonly attribute DOMString tone;
+ };
+ */
+
+ /*
+ 7.2. insertDTMF
+ 11. If a Playout task is scheduled to be run; abort these steps; otherwise queue
+ a task that runs the following steps (Playout task):
+ 3. If toneBuffer is an empty string, fire an event named tonechange with an
+ empty string at the RTCDTMFSender object and abort these steps.
+ 4. Remove the first character from toneBuffer and let that character be tone.
+ 6. Queue a task to be executed in duration + interToneGap ms from now that
+ runs the steps labelled Playout task.
+ 7. Fire an event named tonechange with a string consisting of tone at the
+ RTCDTMFSender object.
+ */
+ test_tone_change_events((t, dtmfSender) => {
+ dtmfSender.insertDTMF('123');
+ }, [
+ ['1', '23', 0],
+ ['2', '3', 170],
+ ['3', '', 170],
+ ['', '', 170]
+ ], 'insertDTMF() with default duration and intertoneGap should fire tonechange events at the expected time');
+
+ test_tone_change_events((t, dtmfSender) => {
+ dtmfSender.insertDTMF('abc', 100, 70);
+ }, [
+ ['A', 'BC', 0],
+ ['B', 'C', 170],
+ ['C', '', 170],
+ ['', '', 170]
+ ], 'insertDTMF() with explicit duration and intertoneGap should fire tonechange events at the expected time');
+
+ /*
+ 7.2. insertDTMF
+ 10. If toneBuffer is an empty string, abort these steps.
+ */
+ async_test(t => {
+ createDtmfSender()
+ .then(dtmfSender => {
+ dtmfSender.addEventListener('tonechange',
+ t.unreached_func('Expect no tonechange event to be fired'));
+
+ dtmfSender.insertDTMF('', 100, 70);
+
+ t.step_timeout(t.step_func_done(), 300);
+ })
+ .catch(t.step_func(err => {
+ assert_unreached(`Unexpected promise rejection: ${err}`);
+ }));
+ }, `insertDTMF('') should not fire any tonechange event, including for '' tone`);
+
+ /*
+ 7.2. insertDTMF
+ 8. If the value of the duration parameter is less than 40, set it to 40.
+ If, on the other hand, the value is greater than 6000, set it to 6000.
+ */
+ test_tone_change_events((t, dtmfSender) => {
+ dtmfSender.insertDTMF('ABC', 10, 70);
+ }, [
+ ['A', 'BC', 0],
+ ['B', 'C', 110],
+ ['C', '', 110],
+ ['', '', 110]
+ ], 'insertDTMF() with duration less than 40 should be clamped to 40');
+
+ /*
+ 7.2. insertDTMF
+ 9. If the value of the interToneGap parameter is less than 30, set it to 30.
+ */
+ test_tone_change_events((t, dtmfSender) => {
+ dtmfSender.insertDTMF('ABC', 100, 10);
+ }, [
+ ['A', 'BC', 0],
+ ['B', 'C', 130],
+ ['C', '', 130],
+ ['', '', 130]
+ ],
+ 'insertDTMF() with interToneGap less than 30 should be clamped to 30');
+
+ /*
+ [w3c/webrtc-pc#1373]
+ This step is added to handle the "," character correctly. "," supposed to delay the next
+ tonechange event by 2000ms.
+
+ 7.2. insertDTMF
+ 11.5. If tone is "," delay sending tones for 2000 ms on the associated RTP media
+ stream, and queue a task to be executed in 2000 ms from now that runs the
+ steps labelled Playout task.
+ */
+ test_tone_change_events((t, dtmfSender) => {
+ dtmfSender.insertDTMF('A,B', 100, 70);
+
+ }, [
+ ['A', ',B', 0],
+ [',', 'B', 170],
+ ['B', '', 2000],
+ ['', '', 170]
+ ], 'insertDTMF with comma should delay next tonechange event for a constant 2000ms');
+
+ /*
+ 7.2. insertDTMF
+ 11.1. If transceiver.stopped is true, abort these steps.
+ */
+ test_tone_change_events((t, dtmfSender, pc) => {
+ const transceiver = getTransceiver(pc);
+ dtmfSender.addEventListener('tonechange', ev => {
+ if(ev.tone === 'B') {
+ transceiver.stop();
+ }
+ });
+
+ dtmfSender.insertDTMF('ABC', 100, 70);
+ }, [
+ ['A', 'BC', 0],
+ ['B', 'C', 170]
+ ], 'insertDTMF() with transceiver stopped in the middle should stop future tonechange events from firing');
+
+ /*
+ 7.2. insertDTMF
+ 3. If a Playout task is scheduled to be run, abort these steps;
+ otherwise queue a task that runs the following steps (Playout task):
+ */
+ test_tone_change_events((t, dtmfSender) => {
+ dtmfSender.addEventListener('tonechange', ev => {
+ if(ev.tone === 'B') {
+ dtmfSender.insertDTMF('12', 100, 70);
+ }
+ });
+
+ dtmfSender.insertDTMF('ABC', 100, 70);
+ }, [
+ ['A', 'BC', 0],
+ ['B', 'C', 170],
+ ['1', '2', 170],
+ ['2', '', 170],
+ ['', '', 170]
+ ], 'Calling insertDTMF() in the middle of tonechange events should cause future tonechanges to be updated to new tones');
+
+
+ /*
+ 7.2. insertDTMF
+ 3. If a Playout task is scheduled to be run, abort these steps;
+ otherwise queue a task that runs the following steps (Playout task):
+ */
+ test_tone_change_events((t, dtmfSender) => {
+ dtmfSender.addEventListener('tonechange', ev => {
+ if(ev.tone === 'B') {
+ dtmfSender.insertDTMF('12', 100, 70);
+ dtmfSender.insertDTMF('34', 100, 70);
+ }
+ });
+
+ dtmfSender.insertDTMF('ABC', 100, 70);
+ }, [
+ ['A', 'BC', 0],
+ ['B', 'C', 170],
+ ['3', '4', 170],
+ ['4', '', 170],
+ ['', '', 170]
+ ], 'Calling insertDTMF() multiple times in the middle of tonechange events should cause future tonechanges to be updated the last provided tones');
+
+ /*
+ 7.2. insertDTMF
+ 3. If a Playout task is scheduled to be run, abort these steps;
+ otherwise queue a task that runs the following steps (Playout task):
+ */
+ test_tone_change_events((t, dtmfSender) => {
+ dtmfSender.addEventListener('tonechange', ev => {
+ if(ev.tone === 'B') {
+ dtmfSender.insertDTMF('');
+ }
+ });
+
+ dtmfSender.insertDTMF('ABC', 100, 70);
+ }, [
+ ['A', 'BC', 0],
+ ['B', 'C', 170],
+ ['', '', 170]
+ ], `Calling insertDTMF('') in the middle of tonechange events should stop future tonechange events from firing`);
+
+ /*
+ 7.2. insertDTMF
+ 11.2. If transceiver.currentDirection is recvonly or inactive, abort these steps.
+ */
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const dtmfSender = await createDtmfSender(pc);
+ const pc2 = pc.otherPc;
+ assert_true(pc2 instanceof RTCPeerConnection,
+ 'Expect pc2 to be a RTCPeerConnection');
+ t.add_cleanup(() => pc2.close());
+ const transceiver = pc.getTransceivers()[0];
+ assert_equals(transceiver.sender.dtmf, dtmfSender);
+
+ // Since setRemoteDescription happens in parallel with tonechange event,
+ // We use a flag and allow tonechange events to be fired as long as
+ // the promise returned by setRemoteDescription is not yet resolved.
+ let remoteDescriptionIsSet = false;
+
+ // We only do basic tone verification and not check timing here
+ let expectedTones = ['A', 'B', 'C', 'D', ''];
+
+ const firstTone = new Promise(resolve => {
+ const onToneChange = t.step_func(ev => {
+ assert_false(remoteDescriptionIsSet,
+ 'Expect no tonechange event to be fired after currentDirection is changed to recvonly');
+
+ const { tone } = ev;
+ const expectedTone = expectedTones.shift();
+ assert_equals(tone, expectedTone,
+ `Expect fired event.tone to be ${expectedTone}`);
+
+ if(tone === 'A') {
+ resolve();
+ }
+ });
+ dtmfSender.addEventListener('tonechange', onToneChange);
+ });
+
+ dtmfSender.insertDTMF('ABCD', 100, 70);
+ await firstTone;
+
+ // Only change transceiver.direction after the first
+ // tonechange event, to make sure that tonechange is triggered
+ // then stopped
+ transceiver.direction = 'recvonly';
+ await exchangeOfferAnswer(pc, pc2);
+ assert_equals(transceiver.currentDirection, 'inactive');
+ remoteDescriptionIsSet = true;
+
+ await new Promise(resolve => t.step_timeout(resolve, 300));
+ }, `Setting transceiver.currentDirection to recvonly in the middle of tonechange events should stop future tonechange events from firing`);
+
+ /* Section 7.3 - Tone change event */
+ test(t => {
+ let ev = new RTCDTMFToneChangeEvent('tonechange', {'tone': '1'});
+ assert_equals(ev.type, 'tonechange');
+ assert_equals(ev.tone, '1');
+ }, 'Tone change event constructor works');
+
+ test(t => {
+ let ev = new RTCDTMFToneChangeEvent('worngname', {});
+ }, 'Tone change event with unexpected name should not crash');
+
+ test(t => {
+ const ev1 = new RTCDTMFToneChangeEvent('tonechange', {});
+ assert_equals(ev1.tone, '');
+
+ assert_equals(RTCDTMFToneChangeEvent.constructor.length, 1);
+ const ev2 = new RTCDTMFToneChangeEvent('tonechange');
+ assert_equals(ev2.tone, '');
+ }, 'Tone change event init optional parameters');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCDataChannel-binaryType.window.js b/testing/web-platform/tests/webrtc/RTCDataChannel-binaryType.window.js
new file mode 100644
index 0000000000..c63281bd51
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCDataChannel-binaryType.window.js
@@ -0,0 +1,27 @@
+'use strict';
+
+const validBinaryTypes = ['blob', 'arraybuffer'];
+const invalidBinaryTypes = ['jellyfish', 'arraybuffer ', '', null, undefined];
+
+for (const binaryType of validBinaryTypes) {
+ test((t) => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const dc = pc.createDataChannel('test-binary-type');
+
+ dc.binaryType = binaryType;
+ assert_equals(dc.binaryType, binaryType, `dc.binaryType should be '${binaryType}'`);
+ }, `Setting binaryType to '${binaryType}' should succeed`);
+}
+
+for (const binaryType of invalidBinaryTypes) {
+ test((t) => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const dc = pc.createDataChannel('test-binary-type');
+
+ assert_throws_dom('SyntaxError', () => {
+ dc.binaryType = binaryType;
+ });
+ }, `Setting invalid binaryType '${binaryType}' should throw SyntaxError`);
+}
diff --git a/testing/web-platform/tests/webrtc/RTCDataChannel-bufferedAmount.html b/testing/web-platform/tests/webrtc/RTCDataChannel-bufferedAmount.html
new file mode 100644
index 0000000000..b1b793206c
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCDataChannel-bufferedAmount.html
@@ -0,0 +1,287 @@
+<!doctype html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<title>RTCDataChannel.prototype.bufferedAmount</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="RTCPeerConnection-helper.js"></script>
+<script>
+'use strict';
+
+// Test is based on the following revision:
+// https://rawgit.com/w3c/webrtc-pc/1cc5bfc3ff18741033d804c4a71f7891242fb5b3/webrtc.html
+
+// The following helper functions are called from RTCPeerConnection-helper.js:
+// createDataChannelPair
+// awaitMessage
+
+/*
+ 6.2. RTCDataChannel
+ interface RTCDataChannel : EventTarget {
+ ...
+ readonly attribute unsigned long bufferedAmount;
+ void send(USVString data);
+ void send(Blob data);
+ void send(ArrayBuffer data);
+ void send(ArrayBufferView data);
+ };
+
+ bufferedAmount
+ The bufferedAmount attribute must return the number of bytes of application
+ data (UTF-8 text and binary data) that have been queued using send() but that,
+ as of the last time the event loop started executing a task, had not yet been
+ transmitted to the network. (This thus includes any text sent during the
+ execution of the current task, regardless of whether the user agent is able
+ to transmit text asynchronously with script execution.) This does not include
+ framing overhead incurred by the protocol, or buffering done by the operating
+ system or network hardware. The value of the [[BufferedAmount]] slot will only
+ increase with each call to the send() method as long as the [[ReadyState]] slot
+ is open; however, the slot does not reset to zero once the channel closes. When
+ the underlying data transport sends data from its queue, the user agent MUST
+ queue a task that reduces [[BufferedAmount]] with the number of bytes that was
+ sent.
+
+
+ [WebMessaging]
+ interface MessageEvent : Event {
+ readonly attribute any data;
+ ...
+ };
+ */
+
+// Simple ASCII encoded string
+const helloString = 'hello';
+// ASCII encoded buffer representation of the string
+const helloBuffer = Uint8Array.of(0x68, 0x65, 0x6c, 0x6c, 0x6f);
+const helloBlob = new Blob([helloBuffer]);
+
+const emptyBuffer = Uint8Array.of();
+const emptyBlob = new Blob([emptyBuffer]);
+
+// Unicode string with multiple code units
+const unicodeString = 'äļ–į•Œä― åĨ―';
+// UTF-8 encoded buffer representation of the string
+const unicodeBuffer = Uint8Array.of(
+ 0xe4, 0xb8, 0x96, 0xe7, 0x95, 0x8c,
+ 0xe4, 0xbd, 0xa0, 0xe5, 0xa5, 0xbd);
+
+for (const options of [{}, {negotiated: true, id: 0}]) {
+ const mode = `${options.negotiated? "negotiated " : ""}datachannel`;
+
+ /*
+ Ensure .bufferedAmount is 0 initially for both sides.
+ */
+ promise_test(async (t) => {
+ const [dc1, dc2] = await createDataChannelPair(t, options);
+
+ assert_equals(dc1.bufferedAmount, 0, 'Expect bufferedAmount to be 0');
+ assert_equals(dc2.bufferedAmount, 0, 'Expect bufferedAmount to be 0');
+ }, `${mode} bufferedAmount initial value should be 0 for both peers`);
+
+ /*
+ 6.2. send()
+ 3. Execute the sub step that corresponds to the type of the methods argument:
+
+ string object
+ Let data be the object and increase the bufferedAmount attribute
+ by the number of bytes needed to express data as UTF-8.
+ */
+ promise_test(async (t) => {
+ const [dc1, dc2] = await createDataChannelPair(t, options);
+
+ dc1.send(unicodeString);
+ assert_equals(dc1.bufferedAmount, unicodeBuffer.byteLength,
+ 'Expect bufferedAmount to be the byte length of the unicode string');
+
+ await awaitMessage(dc2);
+ assert_equals(dc1.bufferedAmount, 0,
+ 'Expect sender bufferedAmount to be reduced after message is sent');
+ }, `${mode} bufferedAmount should increase to byte length of encoded` +
+ `unicode string sent`);
+
+ promise_test(async (t) => {
+ const [dc1, dc2] = await createDataChannelPair(t, options);
+
+ dc1.send("");
+ assert_equals(dc1.bufferedAmount, 0,
+ 'Expect bufferedAmount to stay at zero after sending empty string');
+
+ await awaitMessage(dc2);
+ assert_equals(dc1.bufferedAmount, 0, 'Expect sender bufferedAmount unchanged');
+ }, `${mode} bufferedAmount should stay at zero for empty string sent`);
+
+ /*
+ 6.2. send()
+ 3. Execute the sub step that corresponds to the type of the methods argument:
+ ArrayBuffer object
+ Let data be the data stored in the buffer described by the ArrayBuffer
+ object and increase the bufferedAmount attribute by the length of the
+ ArrayBuffer in bytes.
+ */
+ promise_test(async (t) => {
+ const [dc1, dc2] = await createDataChannelPair(t, options);
+
+ dc1.send(helloBuffer.buffer);
+ assert_equals(dc1.bufferedAmount, helloBuffer.byteLength,
+ 'Expect bufferedAmount to increase to byte length of sent buffer');
+
+ await awaitMessage(dc2);
+ assert_equals(dc1.bufferedAmount, 0,
+ 'Expect sender bufferedAmount to be reduced after message is sent');
+ }, `${mode} bufferedAmount should increase to byte length of buffer sent`);
+
+ promise_test(async (t) => {
+ const [dc1, dc2] = await createDataChannelPair(t, options);
+
+ dc1.send(emptyBuffer.buffer);
+ assert_equals(dc1.bufferedAmount, 0,
+ 'Expect bufferedAmount to stay at zero after sending empty buffer');
+
+ await awaitMessage(dc2);
+ assert_equals(dc1.bufferedAmount, 0,
+ 'Expect sender bufferedAmount unchanged');
+ }, `${mode} bufferedAmount should stay at zero for empty buffer sent`);
+
+ /*
+ 6.2. send()
+ 3. Execute the sub step that corresponds to the type of the methods argument:
+ Blob object
+ Let data be the raw data represented by the Blob object and increase
+ the bufferedAmount attribute by the size of data, in bytes.
+ */
+ promise_test(async (t) => {
+ const [dc1, dc2] = await createDataChannelPair(t, options);
+
+ dc1.send(helloBlob);
+ assert_equals(dc1.bufferedAmount, helloBlob.size,
+ 'Expect bufferedAmount to increase to size of sent blob');
+
+ await awaitMessage(dc2);
+ assert_equals(dc1.bufferedAmount, 0,
+ 'Expect sender bufferedAmount to be reduced after message is sent');
+ }, `${mode} bufferedAmount should increase to size of blob sent`);
+
+ promise_test(async (t) => {
+ const [dc1, dc2] = await createDataChannelPair(t, options);
+
+ dc1.send(emptyBlob);
+ assert_equals(dc1.bufferedAmount, 0,
+ 'Expect bufferedAmount to stay at zero after sending empty blob');
+
+ await awaitMessage(dc2);
+ assert_equals(dc1.bufferedAmount, 0,
+ 'Expect sender bufferedAmount unchanged');
+ }, `${mode} bufferedAmount should stay at zero for empty blob sent`);
+
+ // Test sending 3 messages: helloBuffer, unicodeString, helloBlob
+ promise_test(async (t) => {
+ const resolver = new Resolver();
+ let messageCount = 0;
+
+ const [dc1, dc2] = await createDataChannelPair(t, options);
+ dc2.onmessage = t.step_func(() => {
+ if (++messageCount === 3) {
+ assert_equals(dc1.bufferedAmount, 0,
+ 'Expect sender bufferedAmount to be reduced after message is sent');
+ resolver.resolve();
+ }
+ });
+
+ dc1.send(helloBuffer);
+ assert_equals(dc1.bufferedAmount, helloString.length,
+ 'Expect bufferedAmount to be the total length of all messages queued to send');
+
+ dc1.send(unicodeString);
+ assert_equals(dc1.bufferedAmount,
+ helloString.length + unicodeBuffer.byteLength,
+ 'Expect bufferedAmount to be the total length of all messages queued to send');
+
+ dc1.send(helloBlob);
+ assert_equals(dc1.bufferedAmount,
+ helloString.length*2 + unicodeBuffer.byteLength,
+ 'Expect bufferedAmount to be the total length of all messages queued to send');
+
+ await resolver;
+ }, `${mode} bufferedAmount should increase by byte length for each message sent`);
+
+ promise_test(async (t) => {
+ const [dc1] = await createDataChannelPair(t, options);
+
+ dc1.send(helloBuffer.buffer);
+ assert_equals(dc1.bufferedAmount, helloBuffer.byteLength,
+ 'Expect bufferedAmount to increase to byte length of sent buffer');
+
+ dc1.close();
+ assert_equals(dc1.bufferedAmount, helloBuffer.byteLength,
+ 'Expect bufferedAmount to not decrease immediately after closing the channel');
+ }, `${mode} bufferedAmount should not decrease immediately after initiating closure`);
+
+ promise_test(async (t) => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const [dc1] = await createDataChannelPair(t, options, pc1);
+
+ dc1.send(helloBuffer.buffer);
+ assert_equals(dc1.bufferedAmount, helloBuffer.byteLength,
+ 'Expect bufferedAmount to increase to byte length of sent buffer');
+
+ pc1.close();
+ assert_equals(dc1.bufferedAmount, helloBuffer.byteLength,
+ 'Expect bufferedAmount to not decrease after closing the peer connection');
+ }, `${mode} bufferedAmount should not decrease after closing the peer connection`);
+
+ promise_test(async t => {
+ const [channel1, channel2] = await createDataChannelPair(t, options);
+ channel1.addEventListener('bufferedamountlow', t.step_func_done(() => {
+ assert_true(channel1.bufferedAmount <= channel1.bufferedAmountLowThreshold);
+ }));
+ const eventWatcher = new EventWatcher(t, channel1, ['bufferedamountlow']);
+ channel1.send(helloString);
+ await eventWatcher.wait_for(['bufferedamountlow']);
+ }, `${mode} bufferedamountlow event fires after send() is complete`);
+
+ promise_test(async t => {
+ const [channel1, channel2] = await createDataChannelPair(t, options);
+ channel1.send(helloString);
+ assert_equals(channel1.bufferedAmount, helloString.length);
+ await awaitMessage(channel2);
+ assert_equals(channel1.bufferedAmount, 0);
+ }, `${mode} bufferedamount is data.length on send(data)`);
+
+ promise_test(async t => {
+ const [channel1, channel2] = await createDataChannelPair(t, options);
+ channel1.send(helloString);
+ assert_equals(channel1.bufferedAmount, helloString.length);
+ assert_equals(channel1.bufferedAmount, helloString.length);
+ }, `${mode} bufferedamount returns the same amount if no more data is`);
+
+ promise_test(async t => {
+ const [channel1, channel2] = await createDataChannelPair(t, options);
+ let eventFireCount = 0;
+ channel1.addEventListener('bufferedamountlow', t.step_func(() => {
+ assert_true(channel1.bufferedAmount <= channel1.bufferedAmountLowThreshold);
+ assert_equals(++eventFireCount, 1);
+ }));
+ const eventWatcher = new EventWatcher(t, channel1, ['bufferedamountlow']);
+ channel1.send(helloString);
+ assert_equals(channel1.bufferedAmount, helloString.length);
+ channel1.send(helloString);
+ assert_equals(channel1.bufferedAmount, 2 * helloString.length);
+ await eventWatcher.wait_for(['bufferedamountlow']);
+ }, `${mode} bufferedamountlow event fires only once after multiple` +
+ ` consecutive send() calls`);
+
+ promise_test(async t => {
+ const [channel1, channel2] = await createDataChannelPair(t, options);
+ const eventWatcher = new EventWatcher(t, channel1, ['bufferedamountlow']);
+ channel1.send(helloString);
+ assert_equals(channel1.bufferedAmount, helloString.length);
+ await eventWatcher.wait_for(['bufferedamountlow']);
+ assert_equals(await awaitMessage(channel2), helloString);
+ channel1.send(helloString);
+ assert_equals(channel1.bufferedAmount, helloString.length);
+ await eventWatcher.wait_for(['bufferedamountlow']);
+ assert_equals(await awaitMessage(channel2), helloString);
+ }, `${mode} bufferedamountlow event fires after each sent message`);
+}
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCDataChannel-close.html b/testing/web-platform/tests/webrtc/RTCDataChannel-close.html
new file mode 100644
index 0000000000..64534fc507
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCDataChannel-close.html
@@ -0,0 +1,180 @@
+<!doctype html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<title>RTCDataChannel.prototype.close</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="RTCPeerConnection-helper.js"></script>
+<script>
+'use strict';
+
+for (const options of [{}, {negotiated: true, id: 0}]) {
+ const mode = `${options.negotiated? "negotiated " : ""}datachannel`;
+
+ promise_test(async t => {
+ const [channel1, channel2] = await createDataChannelPair(t, options);
+ const haveClosed = new Promise(r => channel2.onclose = r);
+ let closingSeen = false;
+ channel1.onclosing = t.unreached_func();
+ channel2.onclosing = () => {
+ assert_equals(channel2.readyState, 'closing');
+ closingSeen = true;
+ };
+ channel2.addEventListener('error', t.unreached_func());
+ channel1.close();
+ await haveClosed;
+ assert_equals(channel2.readyState, 'closed');
+ assert_true(closingSeen, 'Closing event was seen');
+ }, `Close ${mode} causes onclosing and onclose to be called`);
+
+ promise_test(async t => {
+ // This is the same test as above, but using addEventListener
+ // rather than the "onclose" attribute.
+ const [channel1, channel2] = await createDataChannelPair(t, options);
+ const haveClosed = new Promise(r => channel2.addEventListener('close', r));
+ let closingSeen = false;
+ channel1.addEventListener('closing', t.unreached_func());
+ channel2.addEventListener('closing', () => {
+ assert_equals(channel2.readyState, 'closing');
+ closingSeen = true;
+ });
+ channel2.addEventListener('error', t.unreached_func());
+ channel1.close();
+ await haveClosed;
+ assert_equals(channel2.readyState, 'closed');
+ assert_true(closingSeen, 'Closing event was seen');
+ }, `Close ${mode} causes closing and close event to be called`);
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const [channel1, channel2] = await createDataChannelPair(t, options, pc1);
+ const events = [];
+ let error = null;
+ channel2.addEventListener('error', t.step_func(event => {
+ events.push('error');
+ assert_true(event instanceof RTCErrorEvent);
+ error = event.error;
+ }));
+ const haveClosed = new Promise(r => channel2.addEventListener('close', () => {
+ events.push('close');
+ r();
+ }));
+ pc1.close();
+ await haveClosed;
+ // Error should fire before close.
+ assert_array_equals(events, ['error', 'close']);
+ assert_true(error instanceof RTCError);
+ assert_equals(error.name, 'OperationError');
+ assert_equals(error.errorDetail, 'sctp-failure');
+ // Expects the sctpErrorCode is either null or 12 (User-Initiated Abort) as it is
+ // optional in the SCTP specification.
+ assert_in_array(error.sctpCauseCode, [null, 12]);
+ }, `Close peerconnection causes close event and error to be called on ${mode}`);
+
+ promise_test(async t => {
+ let pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ let [channel1, channel2] = await createDataChannelPair(t, options, pc1);
+ // The expected sequence of events when closing a DC is that
+ // channel1 goes to closing, channel2 fires onclose, and when
+ // the close is confirmed, channel1 fires onclose.
+ // After that, no more events should fire.
+ channel1.onerror = t.unreached_func();
+ let close2Handler = new Promise(resolve => {
+ channel2.onclose = event => {
+ resolve();
+ };
+ });
+ let close1Handler = new Promise(resolve => {
+ channel1.onclose = event => {
+ resolve();
+ };
+ });
+ channel1.close();
+ await close2Handler;
+ await close1Handler;
+ channel1.onclose = t.unreached_func();
+ channel2.onclose = t.unreached_func();
+ channel2.onerror = t.unreached_func();
+ pc1.close();
+ await new Promise(resolve => t.step_timeout(resolve, 10));
+ }, `Close peerconnection after ${mode} close causes no events`);
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+ pc1.createDataChannel('not-counted', options);
+ const tokenDataChannel = new Promise(resolve => {
+ pc2.ondatachannel = resolve;
+ });
+ exchangeIceCandidates(pc1, pc2);
+ await exchangeOfferAnswer(pc1, pc2);
+ if (!options.negotiated) {
+ await tokenDataChannel;
+ }
+ let closeExpectedCount = 0;
+ let errorExpectedCount = 0;
+ let resolveCountIsZero;
+ let waitForCountIsZero = new Promise(resolve => {
+ resolveCountIsZero = resolve;
+ });
+ for (let i = 1; i <= 10; i++) {
+ if ('id' in options) {
+ options.id = i;
+ }
+ pc1.createDataChannel('', options);
+ if (options.negotiated) {
+ const channel = pc2.createDataChannel('', options);
+ channel.addEventListener('error', t.step_func(event => {
+ assert_true(event instanceof RTCErrorEvent, 'error event ' + event);
+ errorExpectedCount -= 1;
+ }));
+ channel.addEventListener('close', t.step_func(event => {
+ closeExpectedCount -= 1;
+ if (closeExpectedCount == 0) {
+ resolveCountIsZero();
+ }
+ }));
+ } else {
+ await new Promise(resolve => {
+ pc2.ondatachannel = ({channel}) => {
+ channel.addEventListener('error', t.step_func(event => {
+ assert_true(event instanceof RTCErrorEvent);
+ errorExpectedCount -= 1;
+ }));
+ channel.addEventListener('close', t.step_func(event => {
+ closeExpectedCount -= 1;
+ if (closeExpectedCount == 0) {
+ resolveCountIsZero();
+ }
+ }));
+ resolve();
+ }
+ });
+ }
+ ++closeExpectedCount;
+ ++errorExpectedCount;
+ }
+ assert_equals(closeExpectedCount, 10);
+ // We have to wait until SCTP is connected before we close, otherwise
+ // there will be no signal.
+ // The state is not available under Plan B, and unreliable on negotiated
+ // channels.
+ // TODO(bugs.webrtc.org/12259): Remove dependency on "negotiated"
+ if (pc1.sctp && !options.negotiated) {
+ waitForState(pc1.sctp, 'connected');
+ } else {
+ // Under plan B, we don't have a dtls transport to wait on, so just
+ // wait a bit.
+ await new Promise(resolve => t.step_timeout(resolve, 100));
+ }
+ pc1.close();
+ await waitForCountIsZero;
+ assert_equals(closeExpectedCount, 0);
+ assert_equals(errorExpectedCount, 0);
+ }, `Close peerconnection causes close event and error on many channels, ${mode}`);
+}
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCDataChannel-iceRestart.html b/testing/web-platform/tests/webrtc/RTCDataChannel-iceRestart.html
new file mode 100644
index 0000000000..1aec50a587
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCDataChannel-iceRestart.html
@@ -0,0 +1,76 @@
+<!doctype html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<title>RTCDataChannel interactions with ICE restart</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="RTCPeerConnection-helper.js"></script>
+<script>
+'use strict';
+
+async function checkCanPassData(channel1, channel2) {
+ channel1.send('hello');
+ const message = await awaitMessage(channel2);
+ assert_equals(message, 'hello');
+}
+
+async function pingPongData(channel1, channel2, size=1) {
+ channel1.send('hello');
+ const request = await awaitMessage(channel2);
+ assert_equals(request, 'hello');
+ const response = 'x'.repeat(size);
+ channel2.send(response);
+ const responseReceived = await awaitMessage(channel1);
+ assert_equals(response, responseReceived);
+}
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const [channel1, channel2] = await createDataChannelPair(t, {}, pc1, pc2);
+ channel2.addEventListener('error', t.unreached_func());
+ channel2.addEventListener('error', t.unreached_func());
+
+ await checkCanPassData(channel1, channel2);
+ await checkCanPassData(channel2, channel1);
+
+ pc1.restartIce();
+ await exchangeOfferAnswer(pc1, pc2);
+
+ await checkCanPassData(channel1, channel2);
+ await checkCanPassData(channel2, channel1);
+ channel1.close();
+ channel2.close();
+}, `Data channel remains usable after ICE restart`);
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const [channel1, channel2] = await createDataChannelPair(t, {}, pc1, pc2);
+ channel2.addEventListener('error', t.unreached_func());
+ channel2.addEventListener('error', t.unreached_func());
+
+ await pingPongData(channel1, channel2);
+ pc1.restartIce();
+
+ await pc1.setLocalDescription();
+ await pingPongData(channel1, channel2);
+ await pc2.setRemoteDescription(pc1.localDescription);
+ await pingPongData(channel1, channel2);
+ await pc2.setLocalDescription(await pc2.createAnswer());
+ await pingPongData(channel1, channel2);
+ await pc1.setRemoteDescription(pc2.localDescription);
+ await pingPongData(channel1, channel2);
+ channel1.close();
+ channel2.close();
+}, `Data channel remains usable at each step of an ICE restart`);
+
+
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCDataChannel-id.html b/testing/web-platform/tests/webrtc/RTCDataChannel-id.html
new file mode 100644
index 0000000000..10dc5eacb9
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCDataChannel-id.html
@@ -0,0 +1,345 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCDataChannel id attribute</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src="RTCPeerConnection-helper.js"></script>
+<script>
+'use strict';
+
+// Test is based on the following revision:
+// https://rawgit.com/w3c/webrtc-pc/1cc5bfc3ff18741033d804c4a71f7891242fb5b3/webrtc.html
+
+// This is the maximum number of streams, NOT the maximum stream ID (which is 65534)
+// See: https://tools.ietf.org/html/draft-ietf-rtcweb-data-channel-13#section-6.2
+const nStreams = 65535;
+
+/*
+ 6.1.
+ 21. If the [[DataChannelId]] slot is null (due to no ID being passed into
+ createDataChannel, or [[Negotiated]] being false), and the DTLS role of the SCTP
+ transport has already been negotiated, then initialize [[DataChannelId]] to a value
+ generated by the user agent, according to [RTCWEB-DATA-PROTOCOL] [...]
+ */
+promise_test(async (t) => {
+ const pc = new RTCPeerConnection;
+ t.add_cleanup(() => pc.close());
+
+ const dc1 = pc.createDataChannel('');
+ const ids = new UniqueSet();
+
+ const offer = await pc.createOffer();
+ await pc.setLocalDescription(offer);
+ // Turn our own offer SDP into valid answer SDP by setting the DTLS role to
+ // "active".
+ const answer = {
+ type: 'answer',
+ sdp: pc.localDescription.sdp.replace('actpass', 'active')
+ };
+ await pc.setRemoteDescription(answer);
+
+ // Since the remote description had an 'active' DTLS role, we're the server
+ // and should use odd data channel IDs, according to rtcweb-data-channel.
+ assert_equals(dc1.id % 2, 1,
+ `Channel created by the DTLS server role must be odd (was ${dc1.id})`);
+ const dc2 = pc.createDataChannel('another');
+ assert_equals(dc2.id % 2, 1,
+ `Channel created by the DTLS server role must be odd (was ${dc2.id})`);
+
+ // Ensure IDs are unique
+ ids.add(dc1.id, `Channel ID ${dc1.id} should be unique`);
+ ids.add(dc2.id, `Channel ID ${dc2.id} should be unique`);
+}, 'DTLS client uses odd data channel IDs');
+
+promise_test(async (t) => {
+ const pc = new RTCPeerConnection;
+ t.add_cleanup(() => pc.close());
+
+ const dc1 = pc.createDataChannel('');
+ const ids = new UniqueSet();
+
+ const offer = await pc.createOffer();
+ await pc.setLocalDescription(offer);
+ // Turn our own offer SDP into valid answer SDP by setting the DTLS role to
+ // 'passive'.
+ const answer = {
+ type: 'answer',
+ sdp: pc.localDescription.sdp.replace('actpass', 'passive')
+ };
+ await pc.setRemoteDescription(answer);
+
+ // Since the remote description had a 'passive' DTLS role, we're the client
+ // and should use even data channel IDs, according to rtcweb-data-channel.
+ assert_equals(dc1.id % 2, 0,
+ `Channel created by the DTLS client role must be even (was ${dc1.id})`);
+ const dc2 = pc.createDataChannel('another');
+ assert_equals(dc2.id % 2, 0,
+ `Channel created by the DTLS client role must be even (was ${dc1.id})`);
+
+ // Ensure IDs are unique
+ ids.add(dc1.id, `Channel ID ${dc1.id} should be unique`);
+ ids.add(dc2.id, `Channel ID ${dc2.id} should be unique`);
+}, 'DTLS server uses even data channel IDs');
+
+/*
+ Checks that the id is ignored if "negotiated" is false.
+ See section 6.1, createDataChannel step 13.
+ */
+promise_test(async (t) => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+
+ const dc1 = pc1.createDataChannel('', {
+ negotiated: false,
+ id: 42
+ });
+ dc1.onopen = t.step_func(() => {
+ dc1.send(':(');
+ });
+
+ const dc2 = pc2.createDataChannel('', {
+ negotiated: false,
+ id: 42
+ });
+ // ID should be null prior to negotiation.
+ assert_equals(dc1.id, null);
+ assert_equals(dc2.id, null);
+
+ exchangeIceCandidates(pc1, pc2);
+ await exchangeOfferAnswer(pc1, pc2);
+ // We should now have 2 datachannels with different IDs.
+ // At least one of the datachannels should not be 42.
+ // If one has the value 42, it's an accident; if both have,
+ // they are the same datachannel, and it's a bug.
+ assert_false(dc1.id == 42 && dc2.id == 42);
+}, 'In-band negotiation with a specific ID should not work');
+
+/*
+ Check if the implementation still follows the odd/even role correctly if we annoy it with
+ negotiated channels not following that rule.
+
+ Note: This test assumes that the implementation can handle a minimum of 40 data channels.
+ */
+promise_test(async (t) => {
+ // Takes the DTLS server role
+ const pc1 = new RTCPeerConnection();
+ // Takes the DTLS client role
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+
+ exchangeIceCandidates(pc1, pc2);
+ const dcs = [];
+ const negotiatedDcs = [];
+ const ids = new UniqueSet();
+
+ // Create 10 DCEP-negotiated channels with pc1
+ // Note: These should not have any associated valid ID at this point
+ for (let i = 0; i < 10; ++i) {
+ const dc = pc1.createDataChannel('before-connection');
+ assert_equals(dc.id, null, 'Channel id must be null before DTLS role has been determined');
+ dcs.push(dc);
+ }
+
+ // Create 10 negotiated channels with pc1 violating the odd/even rule
+ for (let id = 0; id < 20; id += 2) {
+ const dc = pc1.createDataChannel(`negotiated-not-odd-${id}-before-connection`, {
+ negotiated: true,
+ id: id,
+ });
+ assert_equals(dc.id, id, 'Channel id must be set before DTLS role has been determined when negotiated is true');
+ negotiatedDcs.push([dc, id]);
+ ids.add(dc.id, `Channel ID ${dc.id} should be unique`);
+ }
+
+ await exchangeOfferAnswer(pc1, pc2, {
+ offer: (offer) => {
+ // Ensure pc1 takes the server role
+ assert_true(offer.sdp.includes('actpass') || offer.sdp.includes('passive'),
+ 'pc1 must take the DTLS server role');
+ return offer;
+ },
+ answer: (answer) => {
+ // Ensure pc2 takes the client role
+ // Note: It very likely will choose 'active' itself
+ answer.sdp = answer.sdp.replace('actpass', 'active');
+ assert_true(answer.sdp.includes('active'), 'pc2 must take the DTLS client role');
+ return answer;
+ },
+ });
+
+ for (const dc of dcs) {
+ assert_equals(dc.id % 2, 1,
+ `Channel created by the DTLS server role must be odd (was ${dc.id})`);
+ ids.add(dc.id, `Channel ID ${dc.id} should be unique`);
+ }
+
+ // Create 10 channels with pc1
+ for (let i = 0; i < 10; ++i) {
+ const dc = pc1.createDataChannel('after-connection');
+ assert_equals(dc.id % 2, 1,
+ `Channel created by the DTLS server role must be odd (was ${dc.id})`);
+ dcs.push(dc);
+ ids.add(dc.id, `Channel ID ${dc.id} should be unique`);
+ }
+
+ // Create 10 negotiated channels with pc1 violating the odd/even rule
+ for (let i = 0; i < 10; ++i) {
+ // Generate a valid even ID that has not been taken, yet.
+ let id = 20;
+ while (ids.has(id)) {
+ id += 2;
+ }
+ const dc = pc1.createDataChannel(`negotiated-not-odd-${i}-after-connection`, {
+ negotiated: true,
+ id: id,
+ });
+ negotiatedDcs.push([dc, id]);
+ ids.add(dc.id, `Channel ID ${dc.id} should be unique`);
+ }
+
+ // Since we've added new channels, let's check again that the odd/even role is not violated
+ for (const dc of dcs) {
+ assert_equals(dc.id % 2, 1,
+ `Channel created by the DTLS server role must be odd (was ${dc.id})`);
+ }
+
+ // Let's also make sure the negotiated channels have kept their ID
+ for (const [dc, id] of negotiatedDcs) {
+ assert_equals(dc.id, id, 'Negotiated channels should keep their assigned ID');
+ }
+}, 'Odd/even role should not be violated when mixing with negotiated channels');
+
+/*
+ Create 32768 (client), 32767 (server) channels to make sure all ids are exhausted AFTER
+ establishing a peer connection.
+
+ 6.1. createDataChannel
+ 21. If the [[DataChannelId]] slot is null (due to no ID being passed into
+ createDataChannel, or [[Negotiated]] being false), and the DTLS role of the SCTP
+ transport has already been negotiated, then initialize [[DataChannelId]] to a value
+ generated by the user agent, according to [RTCWEB-DATA-PROTOCOL], and skip
+ to the next step. If no available ID could be generated, or if the value of the
+ [[DataChannelId]] slot is being used by an existing RTCDataChannel, throw an
+ OperationError exception.
+ */
+/*
+ TODO: Improve test coverage for RTCSctpTransport.maxChannels.
+ TODO: Improve test coverage for exhausting channel cases.
+ */
+
+/*
+ Create 32768 (client), 32767 (server) channels to make sure all ids are exhausted BEFORE
+ establishing a peer connection.
+
+ Be aware that late channel id assignment can currently fail in many places not covered by the
+ spec, see: https://github.com/w3c/webrtc-pc/issues/1818
+
+ 4.4.1.6.
+ 2.2.6. If description negotiates the DTLS role of the SCTP transport, and there is an
+ RTCDataChannel with a null id, then generate an ID according to [RTCWEB-DATA-PROTOCOL].
+ If no available ID could be generated, then run the following steps:
+ 1. Let channel be the RTCDataChannel object for which an ID could not be generated.
+ 2. Set channel's [[ReadyState]] slot to "closed".
+ 3. Fire an event named error with an OperationError exception at channel.
+ 4. Fire a simple event named close at channel.
+ */
+/* TEST DISABLED - it takes so long, it times out.
+promise_test(async (t) => {
+ const resolver = new Resolver();
+ // Takes the DTLS server role
+ const pc1 = new RTCPeerConnection();
+ // Takes the DTLS client role
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+
+ exchangeIceCandidates(pc1, pc2);
+ const dcs = [];
+ const ids = new UniqueSet();
+ let nExpected = 0;
+ let nActualCloses = 0;
+ let nActualErrors = 0;
+
+ const maybeDone = t.step_func(() => {
+ if (nExpected === nActualCloses && nExpected === nActualErrors) {
+ resolver.resolve();
+ }
+ });
+
+ // Create 65535+2 channels (since 65535 streams is a SHOULD, we may have less than that.)
+ // Create two extra channels to possibly trigger the steps in the description.
+ //
+ // Note: Following the spec strictly would assume that this cannot fail. But in reality it will
+ // fail because the implementation knows how many streams it supports. What it doesn't
+ // know is how many streams the other peer supports (e.g. what will be negotiated).
+ for (let i = 0; i < (nStreams + 2); ++i) {
+ let dc;
+ try {
+ const pc = i % 2 === 1 ? pc1 : pc2;
+ dc = pc.createDataChannel('this is going to be fun');
+ dc.onclose = t.step_func(() => {
+ ++nActualCloses;
+ maybeDone();
+ });
+ dc.onerror = t.step_func((e) => {
+ assert_true(e instanceof RTCError, 'Expect error object to be instance of RTCError');
+ assert_equals(e.error, 'sctp-failure', "Expect error to be of type 'sctp-failure'");
+ ++nActualErrors;
+ maybeDone();
+ });
+ } catch (e) {
+ assert_equals(e.name, 'OperationError', 'Fail on creation should throw OperationError');
+ break;
+ }
+ assert_equals(dc.id, null, 'Channel id must be null before DTLS role has been determined');
+ assert_not_equals(dc.readyState, 'closed',
+ 'Channel may not be closed before connection establishment');
+ dcs.push([dc, i % 2 === 1]);
+ }
+
+ await exchangeOfferAnswer(pc1, pc2, {
+ offer: (offer) => {
+ // Ensure pc1 takes the server role
+ assert_true(offer.sdp.includes('actpass') || offer.sdp.includes('passive'),
+ 'pc1 must take the DTLS server role');
+ return offer;
+ },
+ answer: (answer) => {
+ // Ensure pc2 takes the client role
+ // Note: It very likely will choose 'active' itself
+ answer.sdp = answer.sdp.replace('actpass', 'active');
+ assert_true(answer.sdp.includes('active'), 'pc2 must take the DTLS client role');
+ return answer;
+ },
+ });
+
+ // Since the spec does not define a specific order to which channels may fail if an ID could
+ // not be generated, any of the channels may be affected by the steps of the description.
+ for (const [dc, odd] of dcs) {
+ if (dc.readyState !== 'closed') {
+ assert_equals(dc.id % 2, odd ? 1 : 0,
+ `Channels created by the DTLS ${odd ? 'server' : 'client'} role must be
+ ${odd ? 'odd' : 'even'} (was ${dc.id})`);
+ ids.add(dc.id, `Channel ID ${dc.id} should be unique`);
+ } else {
+ ++nExpected;
+ }
+ }
+
+ // Try creating one further channel on both sides. The attempt should fail since all IDs are
+ // taken. If one ID is available, the implementation probably miscounts (or I did in the test).
+ assert_throws_dom('OperationError', () =>
+ pc1.createDataChannel('this is too exhausting!'));
+ assert_throws_dom('OperationError', () =>
+ pc2.createDataChannel('this is too exhausting!'));
+
+ maybeDone();
+ await resolver;
+}, 'Channel ID exhaustion handling (before and after connection establishment)');
+
+END DISABLED TEST */
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCDataChannel-send-blob-order.html b/testing/web-platform/tests/webrtc/RTCDataChannel-send-blob-order.html
new file mode 100644
index 0000000000..3fcf116bc8
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCDataChannel-send-blob-order.html
@@ -0,0 +1,31 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCDataChannel.prototype.send for blobs</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="RTCPeerConnection-helper.js"></script>
+<script>
+
+for (const options of [{}, {negotiated: true, id: 0}]) {
+ const mode = `${options.negotiated? "Negotiated d" : "D"}atachannel`;
+
+ promise_test(async t => {
+ const data1 = new Blob(['blob']);
+ const data1Size = data1.size;
+ const data2 = new ArrayBuffer(8);
+ const data2Size = data2.byteLength;
+
+ const [channel1, channel2] = await createDataChannelPair(t, options);
+ channel2.binaryType = "arraybuffer";
+
+ channel1.send(data1);
+ channel1.send(data2);
+
+ let e = await new Promise(r => channel2.onmessage = r);
+ assert_equals(e.data.byteLength, data1Size);
+
+ e = await new Promise(r => channel2.onmessage = r);
+ assert_equals(e.data.byteLength, data2Size);
+ }, `${mode} should send data following the order of the send call`);
+}
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCDataChannel-send.html b/testing/web-platform/tests/webrtc/RTCDataChannel-send.html
new file mode 100644
index 0000000000..70cdf8657f
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCDataChannel-send.html
@@ -0,0 +1,336 @@
+<!doctype html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<title>RTCDataChannel.prototype.send</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="RTCPeerConnection-helper.js"></script>
+<script>
+'use strict';
+
+// Test is based on the following editor draft:
+// https://w3c.github.io/webrtc-pc/archives/20170605/webrtc.html
+
+// The following helper functions are called from RTCPeerConnection-helper.js:
+// createDataChannelPair
+// awaitMessage
+// blobToArrayBuffer
+// assert_equals_typed_array
+
+/*
+ 6.2. RTCDataChannel
+ interface RTCDataChannel : EventTarget {
+ ...
+ readonly attribute RTCDataChannelState readyState;
+ readonly attribute unsigned long bufferedAmount;
+ attribute EventHandler onmessage;
+ attribute DOMString binaryType;
+
+ void send(USVString data);
+ void send(Blob data);
+ void send(ArrayBuffer data);
+ void send(ArrayBufferView data);
+ };
+ */
+
+// Simple ASCII encoded string
+const helloString = 'hello';
+const emptyString = '';
+// ASCII encoded buffer representation of the string
+const helloBuffer = Uint8Array.of(0x68, 0x65, 0x6c, 0x6c, 0x6f);
+const emptyBuffer = new Uint8Array();
+const helloBlob = new Blob([helloBuffer]);
+
+// Unicode string with multiple code units
+const unicodeString = 'äļ–į•Œä― åĨ―';
+// UTF-8 encoded buffer representation of the string
+const unicodeBuffer = Uint8Array.of(
+ 0xe4, 0xb8, 0x96, 0xe7, 0x95, 0x8c,
+ 0xe4, 0xbd, 0xa0, 0xe5, 0xa5, 0xbd);
+
+/*
+ 6.2. send()
+ 2. If channel's readyState attribute is connecting, throw an InvalidStateError.
+ */
+test(t => {
+ const pc = new RTCPeerConnection();
+ const channel = pc.createDataChannel('test');
+ assert_equals(channel.readyState, 'connecting');
+ assert_throws_dom('InvalidStateError', () => channel.send(helloString));
+}, 'Calling send() when data channel is in connecting state should throw InvalidStateError');
+
+for (const options of [{}, {negotiated: true, id: 0}]) {
+ const mode = `${options.negotiated? "Negotiated d" : "D"}atachannel`;
+
+ /*
+ 6.2. send()
+ 3. Execute the sub step that corresponds to the type of the methods argument:
+
+ string object
+ Let data be the object and increase the bufferedAmount attribute
+ by the number of bytes needed to express data as UTF-8.
+
+ [WebSocket]
+ 5. Feedback from the protocol
+ When a WebSocket message has been received
+ 4. If type indicates that the data is Text, then initialize event's data
+ attribute to data.
+ */
+ promise_test(t => {
+ return createDataChannelPair(t, options)
+ .then(([channel1, channel2]) => {
+ channel1.send(helloString);
+ return awaitMessage(channel2)
+ }).then(message => {
+ assert_equals(typeof message, 'string',
+ 'Expect message to be a string');
+
+ assert_equals(message, helloString);
+ });
+ }, `${mode} should be able to send simple string and receive as string`);
+
+ promise_test(t => {
+ return createDataChannelPair(t, options)
+ .then(([channel1, channel2]) => {
+ channel1.send(unicodeString);
+ return awaitMessage(channel2)
+ }).then(message => {
+ assert_equals(typeof message, 'string',
+ 'Expect message to be a string');
+
+ assert_equals(message, unicodeString);
+ });
+ }, `${mode} should be able to send unicode string and receive as unicode string`);
+ promise_test(t => {
+ return createDataChannelPair(t, options)
+ .then(([channel1, channel2]) => {
+ channel2.binaryType = 'arraybuffer';
+ channel1.send(helloString);
+ return awaitMessage(channel2);
+ }).then(message => {
+ assert_equals(typeof message, 'string',
+ 'Expect message to be a string');
+
+ assert_equals(message, helloString);
+ });
+ }, `${mode} should ignore binaryType and always receive string message as string`);
+ promise_test(t => {
+ return createDataChannelPair(t, options)
+ .then(([channel1, channel2]) => {
+ channel1.send(emptyString);
+ // Send a non-empty string in case the implementation ignores empty messages
+ channel1.send(helloString);
+ return awaitMessage(channel2)
+ }).then(message => {
+ assert_equals(typeof message, 'string',
+ 'Expect message to be a string');
+
+ assert_equals(message, emptyString);
+ });
+ }, `${mode} should be able to send an empty string and receive an empty string`);
+
+ /*
+ 6.2. send()
+ 3. Execute the sub step that corresponds to the type of the methods argument:
+ ArrayBufferView object
+ Let data be the data stored in the section of the buffer described
+ by the ArrayBuffer object that the ArrayBufferView object references
+ and increase the bufferedAmount attribute by the length of the
+ ArrayBufferView in bytes.
+
+ [WebSocket]
+ 5. Feedback from the protocol
+ When a WebSocket message has been received
+ 4. If binaryType is set to "arraybuffer", then initialize event's data
+ attribute to a new read-only ArrayBuffer object whose contents are data.
+
+ [WebIDL]
+ 4.1. ArrayBufferView
+ typedef (Int8Array or Int16Array or Int32Array or
+ Uint8Array or Uint16Array or Uint32Array or Uint8ClampedArray or
+ Float32Array or Float64Array or DataView) ArrayBufferView;
+ */
+ promise_test(t => {
+ return createDataChannelPair(t, options)
+ .then(([channel1, channel2]) => {
+ channel2.binaryType = 'arraybuffer';
+ channel1.send(helloBuffer);
+ return awaitMessage(channel2)
+ }).then(messageBuffer => {
+ assert_true(messageBuffer instanceof ArrayBuffer,
+ 'Expect messageBuffer to be an ArrayBuffer');
+
+ assert_equals_typed_array(messageBuffer, helloBuffer.buffer);
+ });
+ }, `${mode} should be able to send Uint8Array message and receive as ArrayBuffer`);
+
+ /*
+ 6.2. send()
+ 3. Execute the sub step that corresponds to the type of the methods argument:
+ ArrayBuffer object
+ Let data be the data stored in the buffer described by the ArrayBuffer
+ object and increase the bufferedAmount attribute by the length of the
+ ArrayBuffer in bytes.
+ */
+ promise_test(t => {
+ return createDataChannelPair(t, options)
+ .then(([channel1, channel2]) => {
+ channel2.binaryType = 'arraybuffer';
+ channel1.send(helloBuffer.buffer);
+ return awaitMessage(channel2)
+ }).then(messageBuffer => {
+ assert_true(messageBuffer instanceof ArrayBuffer,
+ 'Expect messageBuffer to be an ArrayBuffer');
+
+ assert_equals_typed_array(messageBuffer, helloBuffer.buffer);
+ });
+ }, `${mode} should be able to send ArrayBuffer message and receive as ArrayBuffer`);
+
+ promise_test(t => {
+ return createDataChannelPair(t, options)
+ .then(([channel1, channel2]) => {
+ channel2.binaryType = 'arraybuffer';
+ channel1.send(emptyBuffer.buffer);
+ // Send a non-empty buffer in case the implementation ignores empty messages
+ channel1.send(helloBuffer.buffer);
+ return awaitMessage(channel2)
+ }).then(messageBuffer => {
+ assert_true(messageBuffer instanceof ArrayBuffer,
+ 'Expect messageBuffer to be an ArrayBuffer');
+
+ assert_equals_typed_array(messageBuffer, emptyBuffer.buffer);
+ });
+ }, `${mode} should be able to send an empty ArrayBuffer message and receive as ArrayBuffer`);
+
+ /*
+ 6.2. send()
+ 3. Execute the sub step that corresponds to the type of the methods argument:
+ Blob object
+ Let data be the raw data represented by the Blob object and increase
+ the bufferedAmount attribute by the size of data, in bytes.
+ */
+ promise_test(t => {
+ return createDataChannelPair(t, options)
+ .then(([channel1, channel2]) => {
+ channel2.binaryType = 'arraybuffer';
+ channel1.send(helloBlob);
+ return awaitMessage(channel2);
+ }).then(messageBuffer => {
+ assert_true(messageBuffer instanceof ArrayBuffer,
+ 'Expect messageBuffer to be an ArrayBuffer');
+
+ assert_equals_typed_array(messageBuffer, helloBuffer.buffer);
+ });
+ }, `${mode} should be able to send Blob message and receive as ArrayBuffer`);
+
+ /*
+ [WebSocket]
+ 5. Feedback from the protocol
+ When a WebSocket message has been received
+ 4. If binaryType is set to "blob", then initialize event's data attribute
+ to a new Blob object that represents data as its raw data.
+ */
+ promise_test(t => {
+ return createDataChannelPair(t, options)
+ .then(([channel1, channel2]) => {
+ channel2.binaryType = 'blob';
+ channel1.send(helloBuffer);
+ return awaitMessage(channel2);
+ })
+ .then(messageBlob => {
+ assert_true(messageBlob instanceof Blob,
+ 'Expect received messageBlob to be a Blob');
+
+ return blobToArrayBuffer(messageBlob);
+ }).then(messageBuffer => {
+ assert_true(messageBuffer instanceof ArrayBuffer,
+ 'Expect messageBuffer to be an ArrayBuffer');
+
+ assert_equals_typed_array(messageBuffer, helloBuffer.buffer);
+ });
+ }, `${mode} should be able to send ArrayBuffer message and receive as Blob`);
+
+ /*
+ 6.2. RTCDataChannel
+ binaryType
+ The binaryType attribute must, on getting, return the value to which it was
+ last set. On setting, the user agent must set the IDL attribute to the new
+ value. When a RTCDataChannel object is created, the binaryType attribute must
+ be initialized to the string "blob".
+ */
+ promise_test(t => {
+ return createDataChannelPair(t, options)
+ .then(([channel1, channel2]) => {
+ assert_equals(channel2.binaryType, 'blob',
+ 'Expect initial binaryType value to be blob');
+
+ channel1.send(helloBuffer);
+ return awaitMessage(channel2);
+ })
+ .then(messageBlob => {
+ assert_true(messageBlob instanceof Blob,
+ 'Expect received messageBlob to be a Blob');
+
+ return blobToArrayBuffer(messageBlob);
+ }).then(messageBuffer => {
+ assert_true(messageBuffer instanceof ArrayBuffer,
+ 'Expect messageBuffer to be an ArrayBuffer');
+
+ assert_equals_typed_array(messageBuffer, helloBuffer.buffer);
+ });
+ }, `${mode} binaryType should receive message as Blob by default`);
+
+ // Test sending 3 messages: helloBuffer, unicodeString, helloBlob
+ async_test(t => {
+ const receivedMessages = [];
+
+ const onMessage = t.step_func(event => {
+ const { data } = event;
+ receivedMessages.push(data);
+
+ if(receivedMessages.length === 3) {
+ assert_equals_typed_array(receivedMessages[0], helloBuffer.buffer);
+ assert_equals(receivedMessages[1], unicodeString);
+ assert_equals_typed_array(receivedMessages[2], helloBuffer.buffer);
+
+ t.done();
+ }
+ });
+
+ createDataChannelPair(t, options)
+ .then(([channel1, channel2]) => {
+ channel2.binaryType = 'arraybuffer';
+ channel2.addEventListener('message', onMessage);
+
+ channel1.send(helloBuffer);
+ channel1.send(unicodeString);
+ channel1.send(helloBlob);
+
+ }).catch(t.step_func(err =>
+ assert_unreached(`Unexpected promise rejection: ${err}`)));
+ }, `${mode} sending multiple messages with different types should succeed and be received`);
+
+ /*
+ [Deferred]
+ 6.2. RTCDataChannel
+ The send() method is being amended in w3c/webrtc-pc#1209 to throw error instead
+ of closing data channel when buffer is full
+
+ send()
+ 4. If channel's underlying data transport is not established yet, or if the
+ closing procedure has started, then abort these steps.
+ 5. Attempt to send data on channel's underlying data transport; if the data
+ cannot be sent, e.g. because it would need to be buffered but the buffer
+ is full, the user agent must abruptly close channel's underlying data
+ transport with an error.
+
+ test(t => {
+ const pc = new RTCPeerConnection();
+ const channel = pc.createDataChannel('test');
+ channel.close();
+ assert_equals(channel.readyState, 'closing');
+ channel.send(helloString);
+ }, 'Calling send() when data channel is in closing state should succeed');
+ */
+}
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCDataChannelEvent-constructor.html b/testing/web-platform/tests/webrtc/RTCDataChannelEvent-constructor.html
new file mode 100644
index 0000000000..265943ae56
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCDataChannelEvent-constructor.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>RTCDataChannelEvent constructor</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+// Test is based on the following revision:
+// https://rawgit.com/w3c/webrtc-pc/1cc5bfc3ff18741033d804c4a71f7891242fb5b3/webrtc.html
+
+test(function() {
+ assert_equals(RTCDataChannelEvent.length, 2);
+ assert_throws_js(
+ TypeError,
+ function() { new RTCDataChannelEvent('type'); }
+ );
+}, 'RTCDataChannelEvent constructor without a required argument.');
+
+test(function() {
+ assert_throws_js(
+ TypeError,
+ function() { new RTCDataChannelEvent('type', { channel: null }); }
+ );
+}, 'RTCDataChannelEvent constructor with channel passed as null.');
+
+test(function() {
+ assert_throws_js(
+ TypeError,
+ function() { new RTCDataChannelEvent('type', { channel: undefined }); }
+ );
+}, 'RTCDataChannelEvent constructor with a channel passed as undefined.');
+
+test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const dc = pc.createDataChannel('');
+ const event = new RTCDataChannelEvent('type', { channel: dc });
+ assert_true(event instanceof RTCDataChannelEvent);
+ assert_equals(event.channel, dc);
+}, 'RTCDataChannelEvent constructor with full arguments.');
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCDtlsTransport-getRemoteCertificates.html b/testing/web-platform/tests/webrtc/RTCDtlsTransport-getRemoteCertificates.html
new file mode 100644
index 0000000000..899e603cbe
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCDtlsTransport-getRemoteCertificates.html
@@ -0,0 +1,97 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>RTCDtlsTransport.prototype.getRemoteCertificates</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="RTCPeerConnection-helper.js"></script>
+<script>
+ 'use strict';
+
+ // The following helper functions are called from RTCPeerConnection-helper.js:
+ // exchangeIceCandidates
+ // exchangeOfferAnswer
+
+ /*
+ 5.5. RTCDtlsTransport Interface
+ interface RTCDtlsTransport : EventTarget {
+ readonly attribute RTCDtlsTransportState state;
+ sequence<ArrayBuffer> getRemoteCertificates();
+ attribute EventHandler onstatechange;
+ attribute EventHandler onerror;
+ ...
+ };
+
+ enum RTCDtlsTransportState {
+ "new",
+ "connecting",
+ "connected",
+ "closed",
+ "failed"
+ };
+
+ getRemoteCertificates
+ Returns the certificate chain in use by the remote side, with each certificate
+ encoded in binary Distinguished Encoding Rules (DER) [X690].
+ getRemoteCertificates() will return an empty list prior to selection of the
+ remote certificate, which will be completed by the time RTCDtlsTransportState
+ transitions to "connected".
+ */
+ async_test(t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ pc1.addTrack(trackFactories.audio());
+ exchangeIceCandidates(pc1, pc2);
+
+ exchangeOfferAnswer(pc1, pc2)
+ .then(t.step_func(() => {
+ const dtlsTransport1 = pc1.getSenders()[0].transport;
+ const dtlsTransport2 = pc2.getReceivers()[0].transport;
+
+ const testedTransports = new Set();
+
+ // Callback function that test the respective DTLS transports
+ // when they become connected.
+ const onConnected = t.step_func(dtlsTransport => {
+ const certs = dtlsTransport.getRemoteCertificates();
+
+ assert_greater_than(certs.length, 0,
+ 'Expect DTLS transport to have at least one remote certificate when connected');
+
+ for(const cert of certs) {
+ assert_true(cert instanceof ArrayBuffer,
+ 'Expect certificate elements be instance of ArrayBuffer');
+ }
+
+ testedTransports.add(dtlsTransport);
+
+ // End the test if both dtlsTransports are tested.
+ if(testedTransports.has(dtlsTransport1) && testedTransports.has(dtlsTransport2)) {
+ t.done();
+ }
+ })
+
+ for(const dtlsTransport of [dtlsTransport1, dtlsTransport2]) {
+ if(dtlsTransport.state === 'connected') {
+ onConnected(dtlsTransport);
+ } else {
+ assert_array_equals(dtlsTransport.getRemoteCertificates(), [],
+ 'Expect DTLS certificates be initially empty until become connected');
+
+ dtlsTransport.addEventListener('statechange', t.step_func(() => {
+ if(dtlsTransport.state === 'connected') {
+ onConnected(dtlsTransport);
+ }
+ }));
+
+ dtlsTransport.addEventListener('error', t.step_func(err => {
+ assert_unreached(`Unexpected error during DTLS handshake: ${err}`);
+ }));
+ }
+ }
+ }));
+ });
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCDtlsTransport-state.html b/testing/web-platform/tests/webrtc/RTCDtlsTransport-state.html
new file mode 100644
index 0000000000..ca49fcc95f
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCDtlsTransport-state.html
@@ -0,0 +1,142 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>RTCDtlsTransport</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="RTCPeerConnection-helper.js"></script>
+<script>
+'use strict';
+
+// The following helper functions are called from RTCPeerConnection-helper.js:
+// exchangeIceCandidates
+// exchangeOfferAnswer
+// trackFactories.audio()
+
+/*
+ 5.5. RTCDtlsTransport Interface
+ interface RTCDtlsTransport : EventTarget {
+ readonly attribute RTCDtlsTransportState state;
+ sequence<ArrayBuffer> getRemoteCertificates();
+ attribute EventHandler onstatechange;
+ attribute EventHandler onerror;
+ ...
+ };
+
+ enum RTCDtlsTransportState {
+ "new",
+ "connecting",
+ "connected",
+ "closed",
+ "failed"
+ };
+
+*/
+function resolveWhen(t, dtlstransport, state) {
+ return new Promise((resolve, reject) => {
+ if (dtlstransport.state == state) { resolve(); }
+ dtlstransport.addEventListener('statechange', t.step_func(e => {
+ if (dtlstransport.state == state) {
+ resolve();
+ }
+ }));
+ });
+}
+
+
+async function setupConnections(t) {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ pc1.addTrack(trackFactories.audio());
+ const channels = exchangeIceCandidates(pc1, pc2);
+ await exchangeOfferAnswer(pc1, pc2);
+ return [pc1, pc2];
+}
+
+promise_test(async t => {
+ const [pc1, pc2] = await setupConnections(t);
+ const dtlsTransport1 = pc1.getTransceivers()[0].sender.transport;
+ const dtlsTransport2 = pc2.getTransceivers()[0].sender.transport;
+ assert_true(dtlsTransport1 instanceof RTCDtlsTransport);
+ assert_true(dtlsTransport2 instanceof RTCDtlsTransport);
+ await Promise.all([resolveWhen(t, dtlsTransport1, 'connected'),
+ resolveWhen(t, dtlsTransport2, 'connected')]);
+}, 'DTLS transport goes to connected state');
+
+promise_test(async t => {
+ const [pc1, pc2] = await setupConnections(t);
+
+ const dtlsTransport1 = pc1.getTransceivers()[0].sender.transport;
+ const dtlsTransport2 = pc2.getTransceivers()[0].sender.transport;
+ await Promise.all([resolveWhen(t, dtlsTransport1, 'connected'),
+ resolveWhen(t, dtlsTransport2, 'connected')]);
+ pc1.close();
+ assert_equals(dtlsTransport1.state, 'closed');
+}, 'close() causes the local transport to close immediately');
+
+promise_test(async t => {
+ const [pc1, pc2] = await setupConnections(t);
+ const dtlsTransport1 = pc1.getTransceivers()[0].sender.transport;
+ const dtlsTransport2 = pc2.getTransceivers()[0].sender.transport;
+ await Promise.all([resolveWhen(t, dtlsTransport1, 'connected'),
+ resolveWhen(t, dtlsTransport2, 'connected')]);
+ pc1.close();
+ await resolveWhen(t, dtlsTransport2, 'closed');
+}, 'close() causes the other end\'s DTLS transport to close');
+
+promise_test(async t => {
+ const config = {bundlePolicy: "max-bundle"};
+ const pc1 = new RTCPeerConnection(config);
+ const pc2 = new RTCPeerConnection(config);
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+
+ pc1.onicecandidate = e => pc2.addIceCandidate(e.candidate);
+ pc2.onicecandidate = e => pc1.addIceCandidate(e.candidate);
+
+ pc1.addTransceiver("video");
+ pc1.addTransceiver("audio");
+ await pc1.setLocalDescription(await pc1.createOffer());
+ await pc2.setRemoteDescription(pc1.localDescription);
+ await pc2.setLocalDescription(await pc2.createAnswer());
+ await pc1.setRemoteDescription(pc2.localDescription);
+
+ const [videoTc, audioTc] = pc1.getTransceivers();
+ const [videoTp, audioTp] =
+ pc1.getTransceivers().map(tc => tc.sender.transport);
+
+ const [videoPc2Tp, audioPc2Tp] =
+ pc2.getTransceivers().map(tc => tc.sender.transport);
+
+ assert_equals(pc1.getTransceivers().length, 2, 'pc1 transceiver count');
+ assert_equals(pc2.getTransceivers().length, 2, 'pc2 transceiver count');
+ assert_equals(videoTc.sender.transport, videoTc.receiver.transport);
+ assert_equals(videoTc.sender.transport, audioTc.sender.transport);
+
+ await Promise.all([resolveWhen(t, videoTp, 'connected'),
+ resolveWhen(t, videoPc2Tp, 'connected')]);
+
+ assert_equals(audioTc.sender, pc1.getSenders()[1]);
+
+ let stoppedTransceiver = pc1.getTransceivers()[0];
+ assert_equals(stoppedTransceiver, videoTc); // sanity
+ let onended = new Promise(resolve => {
+ stoppedTransceiver.receiver.track.onended = resolve;
+ });
+ stoppedTransceiver.stop();
+ await onended;
+
+ assert_equals(
+ pc1.getReceivers().length, 1,
+ 'getReceivers does not expose a receiver of a stopped transceiver');
+ assert_equals(
+ pc1.getSenders().length, 1,
+ 'getSenders does not expose a sender of a stopped transceiver');
+ assert_equals(audioTc.sender, pc1.getSenders()[0]); // sanity
+ assert_equals(audioTc.sender.transport, audioTp); // sanity
+ assert_equals(audioTp.state, 'connected');
+}, 'stop bundled transceiver retains dtls transport state');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCError.html b/testing/web-platform/tests/webrtc/RTCError.html
new file mode 100644
index 0000000000..bcc5749bf7
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCError.html
@@ -0,0 +1,89 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCError and RTCErrorInit</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="RTCPeerConnection-helper.js"></script>
+<script>
+'use strict';
+
+test(() => {
+ const error = new RTCError({errorDetail:'data-channel-failure'}, 'message');
+ assert_equals(error.message, 'message');
+ assert_equals(error.errorDetail, 'data-channel-failure');
+}, 'RTCError constructor with errorDetail and message');
+
+test(() => {
+ const error = new RTCError({errorDetail:'data-channel-failure'});
+ assert_equals(error.message, '');
+}, 'RTCError constructor\'s message argument is optional');
+
+test(() => {
+ assert_throws_js(TypeError, () => {
+ new RTCError();
+ });
+ assert_throws_js(TypeError, () => {
+ new RTCError({}); // {errorDetail} is missing.
+ });
+}, 'RTCError constructor throws TypeError if arguments are missing');
+
+test(() => {
+ assert_throws_js(TypeError, () => {
+ new RTCError({errorDetail:'invalid-error-detail'}, 'message');
+ });
+}, 'RTCError constructor throws TypeError if the errorDetail is invalid');
+
+test(() => {
+ const error = new RTCError({errorDetail:'data-channel-failure'}, 'message');
+ assert_equals(error.name, 'OperationError');
+}, 'RTCError.name is \'OperationError\'');
+
+test(() => {
+ const error = new RTCError({errorDetail:'data-channel-failure'}, 'message');
+ assert_equals(error.code, 0);
+}, 'RTCError.code is 0');
+
+test(() => {
+ const error = new RTCError({errorDetail:'data-channel-failure'}, 'message');
+ assert_throws_js(TypeError, () => {
+ error.errorDetail = 'dtls-failure';
+ });
+}, 'RTCError.errorDetail is readonly.');
+
+test(() => {
+ // Infers what are valid RTCErrorInit objects by passing them to the RTCError
+ // constructor.
+ assert_throws_js(TypeError, () => {
+ new RTCError({}, 'message');
+ });
+ new RTCError({errorDetail:'data-channel-failure'}, 'message');
+}, 'RTCErrorInit.errorDetail is the only required attribute');
+
+// All of these are number types (long or unsigned long).
+const nullableAttributes = ['sdpLineNumber',
+ 'httpRequestStatusCode',
+ 'sctpCauseCode',
+ 'receivedAlert',
+ 'sentAlert'];
+
+nullableAttributes.forEach(attribute => {
+ test(() => {
+ const error = new RTCError({errorDetail:'data-channel-failure'}, 'message');
+ assert_equals(error[attribute], null);
+ }, 'RTCError.' + attribute + ' is null by default');
+
+ test(() => {
+ const error = new RTCError(
+ {errorDetail:'data-channel-failure', [attribute]: 0}, 'message');
+ assert_equals(error[attribute], 0);
+ }, 'RTCError.' + attribute + ' is settable by constructor');
+
+ test(() => {
+ const error = new RTCError({errorDetail:'data-channel-failure'}, 'message');
+ assert_throws_js(TypeError, () => {
+ error[attribute] = 42;
+ });
+ }, 'RTCError.' + attribute + ' is readonly');
+});
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCIceCandidate-constructor.html b/testing/web-platform/tests/webrtc/RTCIceCandidate-constructor.html
new file mode 100644
index 0000000000..66d6962079
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCIceCandidate-constructor.html
@@ -0,0 +1,234 @@
+<!doctype html>
+<title>RTCIceCandidate constructor</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script>
+ 'use strict';
+
+ const candidateString = 'candidate:1905690388 1 udp 2113937151 192.168.0.1 58041 typ host generation 0 ufrag thC8 network-cost 50';
+ const candidateString2 = 'candidate:435653019 2 tcp 1845501695 192.168.0.196 4444 typ srflx raddr www.example.com rport 22222 tcptype active';
+ const arbitraryString = '<arbitrary string[0] content>;';
+
+ test(t => {
+ // The argument for RTCIceCandidateInit is optional (w3c/webrtc-pc #1153 #1166),
+ // but the constructor throws because both sdpMid and sdpMLineIndex are null by default.
+ // Note that current browsers pass this test but may throw TypeError for
+ // different reason, i.e. they don't accept empty argument.
+ // Further tests below are used to differentiate the errors.
+ assert_throws_js(TypeError, () => new RTCIceCandidate());
+ }, 'new RTCIceCandidate()');
+
+ test(t => {
+ // All fields in RTCIceCandidateInit are optional,
+ // but the constructor throws because both sdpMid and sdpMLineIndex are null by default.
+ // Note that current browsers pass this test but may throw TypeError for
+ // different reason, i.e. they don't allow undefined candidate string.
+ // Further tests below are used to differentiate the errors.
+ assert_throws_js(TypeError, () => new RTCIceCandidate({}));
+ }, 'new RTCIceCandidate({})');
+
+ test(t => {
+ // Checks that manually filling the default values for RTCIceCandidateInit
+ // still throws because both sdpMid and sdpMLineIndex are null
+ assert_throws_js(TypeError,
+ () => new RTCIceCandidate({
+ candidate: '',
+ sdpMid: null,
+ sdpMLineIndex: null,
+ usernameFragment: undefined
+ }));
+ }, 'new RTCIceCandidate({ ... }) with manually filled default values');
+
+ test(t => {
+ // Checks that explicitly setting both sdpMid and sdpMLineIndex null should throw
+ assert_throws_js(TypeError,
+ () => new RTCIceCandidate({
+ sdpMid: null,
+ sdpMLineIndex: null
+ }));
+ }, 'new RTCIceCandidate({ sdpMid: null, sdpMLineIndex: null })');
+
+ test(t => {
+ // Throws because both sdpMid and sdpMLineIndex are null by default
+ assert_throws_js(TypeError,
+ () => new RTCIceCandidate({
+ candidate: ''
+ }));
+ }, `new RTCIceCandidate({ candidate: '' })`);
+
+ test(t => {
+ // Throws because the candidate field is not nullable
+ assert_throws_js(TypeError,
+ () => new RTCIceCandidate({
+ candidate: null
+ }));
+ }, `new RTCIceCandidate({ candidate: null })`);
+
+ test(t => {
+ // Throws because both sdpMid and sdpMLineIndex are null by default
+ assert_throws_js(TypeError,
+ () => new RTCIceCandidate({
+ candidate: candidateString
+ }));
+ }, 'new RTCIceCandidate({ ... }) with valid candidate string only');
+
+ test(t => {
+ const candidate = new RTCIceCandidate({ sdpMid: 'audio' });
+
+ assert_equals(candidate.candidate, '', 'candidate');
+ assert_equals(candidate.sdpMid, 'audio', 'sdpMid');
+ assert_equals(candidate.sdpMLineIndex, null, 'sdpMLineIndex');
+ assert_equals(candidate.usernameFragment, null, 'usernameFragment');
+ }, `new RTCIceCandidate({ sdpMid: 'audio' })`);
+
+ test(t => {
+ const candidate = new RTCIceCandidate({ sdpMLineIndex: 0 });
+
+ assert_equals(candidate.candidate, '', 'candidate');
+ assert_equals(candidate.sdpMid, null, 'sdpMid');
+ assert_equals(candidate.sdpMLineIndex, 0, 'sdpMLineIndex');
+ assert_equals(candidate.usernameFragment, null, 'usernameFragment');
+ }, 'new RTCIceCandidate({ sdpMLineIndex: 0 })');
+
+ test(t => {
+ const candidate = new RTCIceCandidate({
+ sdpMid: 'audio',
+ sdpMLineIndex: 0
+ });
+
+ assert_equals(candidate.candidate, '', 'candidate');
+ assert_equals(candidate.sdpMid, 'audio', 'sdpMid');
+ assert_equals(candidate.sdpMLineIndex, 0, 'sdpMLineIndex');
+ assert_equals(candidate.usernameFragment, null, 'usernameFragment');
+ }, `new RTCIceCandidate({ sdpMid: 'audio', sdpMLineIndex: 0 })`);
+
+ test(t => {
+ const candidate = new RTCIceCandidate({
+ candidate: '',
+ sdpMid: 'audio'
+ });
+
+ assert_equals(candidate.candidate, '', 'candidate');
+ assert_equals(candidate.sdpMid, 'audio', 'sdpMid');
+ assert_equals(candidate.sdpMLineIndex, null, 'sdpMLineIndex');
+ assert_equals(candidate.usernameFragment, null, 'usernameFragment');
+ }, `new RTCIceCandidate({ candidate: '', sdpMid: 'audio' }`);
+
+ test(t => {
+ const candidate = new RTCIceCandidate({
+ candidate: '',
+ sdpMLineIndex: 0
+ });
+
+ assert_equals(candidate.candidate, '', 'candidate');
+ assert_equals(candidate.sdpMid, null, 'sdpMid', 'sdpMid');
+ assert_equals(candidate.sdpMLineIndex, 0, 'sdpMLineIndex');
+ assert_equals(candidate.usernameFragment, null, 'usernameFragment');
+ }, `new RTCIceCandidate({ candidate: '', sdpMLineIndex: 0 }`);
+
+ test(t => {
+ const candidate = new RTCIceCandidate({
+ candidate: candidateString,
+ sdpMid: 'audio'
+ });
+
+ assert_equals(candidate.candidate, candidateString, 'candidate');
+ assert_equals(candidate.sdpMid, 'audio', 'sdpMid');
+ assert_equals(candidate.sdpMLineIndex, null, 'sdpMLineIndex');
+ assert_equals(candidate.usernameFragment, null, 'usernameFragment');
+ }, 'new RTCIceCandidate({ ... }) with valid candidate string and sdpMid');
+
+ test(t =>{
+ // candidate string is not validated in RTCIceCandidate
+ const candidate = new RTCIceCandidate({
+ candidate: arbitraryString,
+ sdpMid: 'audio'
+ });
+
+ assert_equals(candidate.candidate, arbitraryString, 'candidate');
+ assert_equals(candidate.sdpMid, 'audio', 'sdpMid');
+ assert_equals(candidate.sdpMLineIndex, null, 'sdpMLineIndex');
+ assert_equals(candidate.usernameFragment, null, 'usernameFragment');
+ }, 'new RTCIceCandidate({ ... }) with invalid candidate string and sdpMid');
+
+ test(t => {
+ const candidate = new RTCIceCandidate({
+ candidate: candidateString,
+ sdpMid: 'video',
+ sdpMLineIndex: 1,
+ usernameFragment: 'test'
+ });
+
+ assert_equals(candidate.candidate, candidateString, 'candidate');
+ assert_equals(candidate.sdpMid, 'video', 'sdpMid');
+ assert_equals(candidate.sdpMLineIndex, 1, 'sdpMLineIndex');
+ assert_equals(candidate.usernameFragment, 'test', 'usernameFragment');
+
+ // The following fields should match those in the candidate field
+ assert_equals(candidate.foundation, '1905690388', 'foundation');
+ assert_equals(candidate.component, 'rtp', 'component');
+ assert_equals(candidate.priority, 2113937151, 'priority');
+ assert_equals(candidate.address, '192.168.0.1', 'address');
+ assert_equals(candidate.protocol, 'udp', 'protocol');
+ assert_equals(candidate.port, 58041, 'port');
+ assert_equals(candidate.type, 'host', 'type');
+ assert_equals(candidate.tcpType, null, 'tcpType');
+ assert_equals(candidate.relatedAddress, null, 'relatedAddress');
+ assert_equals(candidate.relatedPort, null, 'relatedPort');
+ }, 'new RTCIceCandidate({ ... }) with nondefault values for all fields');
+
+ test(t => {
+ const candidate = new RTCIceCandidate({
+ candidate: candidateString2,
+ sdpMid: 'video',
+ sdpMLineIndex: 1,
+ usernameFragment: 'user1'
+ });
+
+ assert_equals(candidate.candidate, candidateString2, 'candidate');
+ assert_equals(candidate.sdpMid, 'video', 'sdpMid');
+ assert_equals(candidate.sdpMLineIndex, 1, 'sdpMLineIndex');
+ assert_equals(candidate.usernameFragment, 'user1', 'usernameFragment');
+
+ // The following fields should match those in the candidate field
+ assert_equals(candidate.foundation, '435653019', 'foundation');
+ assert_equals(candidate.component, 'rtcp', 'component');
+ assert_equals(candidate.priority, 1845501695, 'priority');
+ assert_equals(candidate.address, '192.168.0.196', 'address');
+ assert_equals(candidate.protocol, 'tcp', 'protocol');
+ assert_equals(candidate.port, 4444, 'port');
+ assert_equals(candidate.type, 'srflx', 'type');
+ assert_equals(candidate.tcpType, 'active', 'tcpType');
+ assert_equals(candidate.relatedAddress, 'www.example.com', 'relatedAddress');
+ assert_equals(candidate.relatedPort, 22222, 'relatedPort');
+ }, 'new RTCIceCandidate({ ... }) with nondefault values for all fields, tcp candidate');
+
+ test(t => {
+ // sdpMid is not validated in RTCIceCandidate
+ const candidate = new RTCIceCandidate({
+ sdpMid: arbitraryString
+ });
+
+ assert_equals(candidate.candidate, '', 'candidate');
+ assert_equals(candidate.sdpMid, arbitraryString, 'sdpMid');
+ assert_equals(candidate.sdpMLineIndex, null, 'sdpMLineIndex');
+ assert_equals(candidate.usernameFragment, null, 'usernameFragment');
+ }, 'new RTCIceCandidate({ ... }) with invalid sdpMid');
+
+
+ test(t => {
+ // Some arbitrary large out of bound line index that practically
+ // do not reference any m= line in SDP.
+ // However sdpMLineIndex is not validated in RTCIceCandidate
+ // and it has no knowledge of the SDP it is associated with.
+ const candidate = new RTCIceCandidate({
+ sdpMLineIndex: 65535
+ });
+
+ assert_equals(candidate.candidate, '', 'candidate');
+ assert_equals(candidate.sdpMid, null, 'sdpMid');
+ assert_equals(candidate.sdpMLineIndex, 65535, 'sdpMLineIndex');
+ assert_equals(candidate.usernameFragment, null, 'usernameFragment');
+ }, 'new RTCIceCandidate({ ... }) with invalid sdpMLineIndex');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCIceConnectionState-candidate-pair.https.html b/testing/web-platform/tests/webrtc/RTCIceConnectionState-candidate-pair.https.html
new file mode 100644
index 0000000000..3b2c253401
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCIceConnectionState-candidate-pair.https.html
@@ -0,0 +1,33 @@
+<!doctype html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<title>RTCIceConnectionState and RTCIceCandidatePair</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="RTCPeerConnection-helper.js"></script>
+<script>
+'use strict';
+
+promise_test(async t => {
+ const caller = new RTCPeerConnection();
+ t.add_cleanup(() => caller.close());
+ const callee = new RTCPeerConnection();
+ t.add_cleanup(() => callee.close());
+
+ const stream = await getNoiseStream({audio:true});
+ const [track] = stream.getTracks();
+ caller.addTrack(track, stream);
+ exchangeIceCandidates(caller, callee);
+ await exchangeOfferAnswer(caller, callee);
+ await listenToIceConnected(caller);
+
+ const report = await caller.getStats();
+ let succeededPairFound = false;
+ report.forEach(stats => {
+ if (stats.type == 'candidate-pair' && stats.state == 'succeeded')
+ succeededPairFound = true;
+ });
+ assert_true(succeededPairFound, 'A succeeded candidate-pair should exist');
+}, 'On ICE connected, getStats() contains a connected candidate-pair');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCIceTransport.html b/testing/web-platform/tests/webrtc/RTCIceTransport.html
new file mode 100644
index 0000000000..fe12c384e5
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCIceTransport.html
@@ -0,0 +1,193 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCIceTransport</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="RTCPeerConnection-helper.js"></script>
+<script>
+ 'use strict';
+
+ // Test is based on the following editor draft:
+ // https://w3c.github.io/webrtc-pc/archives/20170605/webrtc.html
+
+ // The following helper functions are called from RTCPeerConnection-helper.js:
+ // createDataChannelPair
+ // awaitMessage
+
+ /*
+ 5.6. RTCIceTransport Interface
+ interface RTCIceTransport {
+ readonly attribute RTCIceRole role;
+ readonly attribute RTCIceComponent component;
+ readonly attribute RTCIceTransportState state;
+ readonly attribute RTCIceGathererState gatheringState;
+ sequence<RTCIceCandidate> getLocalCandidates();
+ sequence<RTCIceCandidate> getRemoteCandidates();
+ RTCIceCandidatePair? getSelectedCandidatePair();
+ RTCIceParameters? getLocalParameters();
+ RTCIceParameters? getRemoteParameters();
+ ...
+ };
+
+ getLocalCandidates
+ Returns a sequence describing the local ICE candidates gathered for this
+ RTCIceTransport and sent in onicecandidate
+
+ getRemoteCandidates
+ Returns a sequence describing the remote ICE candidates received by this
+ RTCIceTransport via addIceCandidate()
+
+ getSelectedCandidatePair
+ Returns the selected candidate pair on which packets are sent, or null if
+ there is no such pair.
+
+ getLocalParameters
+ Returns the local ICE parameters received by this RTCIceTransport via
+ setLocalDescription , or null if the parameters have not yet been received.
+
+ getRemoteParameters
+ Returns the remote ICE parameters received by this RTCIceTransport via
+ setRemoteDescription or null if the parameters have not yet been received.
+ */
+ function getIceTransportFromSctp(pc) {
+ const sctpTransport = pc.sctp;
+ assert_true(sctpTransport instanceof RTCSctpTransport,
+ 'Expect pc.sctp to be instantiated from RTCSctpTransport');
+
+ const dtlsTransport = sctpTransport.transport;
+ assert_true(dtlsTransport instanceof RTCDtlsTransport,
+ 'Expect sctp.transport to be an RTCDtlsTransport');
+
+ const iceTransport = dtlsTransport.iceTransport;
+ assert_true(iceTransport instanceof RTCIceTransport,
+ 'Expect dtlsTransport.transport to be an RTCIceTransport');
+
+ return iceTransport;
+ }
+
+ function validateCandidates(candidates) {
+ assert_greater_than(candidates.length, 0,
+ 'Expect at least one ICE candidate returned from get*Candidates()');
+
+ for(const candidate of candidates) {
+ assert_true(candidate instanceof RTCIceCandidate,
+ 'Expect candidate elements to be instance of RTCIceCandidate');
+ }
+ }
+
+ function validateCandidateParameter(param) {
+ assert_not_equals(param, null,
+ 'Expect candidate parameter to be non-null after data channels are connected');
+
+ assert_equals(typeof param.usernameFragment, 'string',
+ 'Expect param.usernameFragment to be set with string value');
+ assert_equals(typeof param.password, 'string',
+ 'Expect param.password to be set with string value');
+ }
+
+ function validateConnectedIceTransport(iceTransport) {
+ const { state, gatheringState, role, component } = iceTransport;
+
+ assert_true(role === 'controlling' || role === 'controlled',
+ 'Expect RTCIceRole to be either controlling or controlled, found ' + role);
+
+ assert_true(component === 'rtp' || component === 'rtcp',
+ 'Expect RTCIceComponent to be either rtp or rtcp');
+
+ assert_true(state === 'connected' || state === 'completed',
+ 'Expect ICE transport to be in connected or completed state after data channels are connected');
+
+ assert_true(gatheringState === 'gathering' || gatheringState === 'completed',
+ 'Expect ICE transport to be in gathering or completed gatheringState after data channels are connected');
+
+ validateCandidates(iceTransport.getLocalCandidates());
+ validateCandidates(iceTransport.getRemoteCandidates());
+
+ const candidatePair = iceTransport.getSelectedCandidatePair();
+ assert_not_equals(candidatePair, null,
+ 'Expect selected candidate pair to be non-null after ICE transport is connected');
+
+ assert_true(candidatePair.local instanceof RTCIceCandidate,
+ 'Expect candidatePair.local to be instance of RTCIceCandidate');
+
+ assert_true(candidatePair.remote instanceof RTCIceCandidate,
+ 'Expect candidatePair.remote to be instance of RTCIceCandidate');
+
+ validateCandidateParameter(iceTransport.getLocalParameters());
+ validateCandidateParameter(iceTransport.getRemoteParameters());
+ }
+
+ promise_test(t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ return createDataChannelPair(t, {}, pc1, pc2)
+ .then(([channel1, channel2]) => {
+ // Send a ping message and wait for it just to make sure
+ // that the connection is fully working before testing
+ channel1.send('ping');
+ return awaitMessage(channel2);
+ })
+ .then(() => {
+ const iceTransport1 = getIceTransportFromSctp(pc1);
+ const iceTransport2 = getIceTransportFromSctp(pc2);
+
+ validateConnectedIceTransport(iceTransport1);
+ validateConnectedIceTransport(iceTransport2);
+
+ assert_equals(
+ iceTransport1.getLocalCandidates().length,
+ iceTransport2.getRemoteCandidates().length,
+ `Expect iceTransport1 to have same number of local candidate as iceTransport2's remote candidates`);
+
+ assert_equals(
+ iceTransport1.getRemoteCandidates().length,
+ iceTransport2.getLocalCandidates().length,
+ `Expect iceTransport1 to have same number of remote candidate as iceTransport2's local candidates`);
+
+ const candidatePair1 = iceTransport1.getSelectedCandidatePair();
+ const candidatePair2 = iceTransport2.getSelectedCandidatePair();
+
+ assert_equals(candidatePair1.local.candidate, candidatePair2.remote.candidate,
+ 'Expect selected local candidate of one pc is the selected remote candidate or another');
+
+ assert_equals(candidatePair1.remote.candidate, candidatePair2.local.candidate,
+ 'Expect selected local candidate of one pc is the selected remote candidate or another');
+
+ assert_equals(iceTransport1.role, 'controlling',
+ `Expect offerer's iceTransport to take the controlling role`);
+
+ assert_equals(iceTransport2.role, 'controlled',
+ `Expect answerer's iceTransport to take the controlled role`);
+ });
+ }, 'Two connected iceTransports should has matching local/remote candidates returned');
+
+ promise_test(t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+ pc1.createDataChannel('');
+
+ // setRemoteDescription(answer) without the other peer
+ // setting answer it's localDescription
+ return pc1.createOffer()
+ .then(offer =>
+ pc1.setLocalDescription(offer)
+ .then(() => pc2.setRemoteDescription(offer))
+ .then(() => pc2.createAnswer()))
+ .then(answer => pc1.setRemoteDescription(answer))
+ .then(() => {
+ const iceTransport = getIceTransportFromSctp(pc1);
+
+ assert_array_equals(iceTransport.getRemoteCandidates(), [],
+ 'Expect iceTransport to not have any remote candidate');
+
+ assert_equals(iceTransport.getSelectedCandidatePair(), null,
+ 'Expect selectedCandidatePair to be null');
+ });
+ }, 'Unconnected iceTransport should have empty remote candidates and selected pair');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-GC.https.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-GC.https.html
new file mode 100644
index 0000000000..76ae3087e4
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-GC.https.html
@@ -0,0 +1,92 @@
+<!doctype html>
+<html>
+<head>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/gc.js"></script>
+<script src="RTCPeerConnection-helper.js"></script>
+</head>
+<body>
+<script>
+'use strict';
+
+// Check that RTCPeerConnection is not collected by GC while displaying video.
+
+promise_test(async t => {
+ const canvas = document.createElement('canvas');
+ canvas.width = canvas.height = 160;
+ const ctx = canvas.getContext("2d");
+ ctx.fillStyle = "blue";
+ const drawCanvas = () => {
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
+ };
+
+ let pc1 = new RTCPeerConnection();
+ let pc2 = new RTCPeerConnection();
+
+ // Attach video to pc1.
+ const [inputTrack] = canvas.captureStream().getTracks();
+ pc1.addTrack(inputTrack);
+
+ const destVideo = document.createElement('video');
+ destVideo.autoplay = true;
+ const onVideoChange = async () => {
+ const start = performance.now();
+ const width = destVideo.videoWidth;
+ const height = destVideo.videoHeight;
+ const resizeEvent = new Promise(r => destVideo.onresize = r);
+ while (destVideo.videoWidth == width && destVideo.videoHeight == height) {
+ if (performance.now() - start > 5000) {
+ throw new Error("Timeout waiting for video size change");
+ }
+ drawCanvas();
+ await Promise.race([
+ resizeEvent,
+ new Promise(r => requestAnimationFrame(r)),
+ ]);
+ }
+ };
+
+ // Setup cleanup. We cannot keep references to pc1 or pc2 so do a best-effort with GC.
+ t.add_cleanup(async () => {
+ inputTrack.stop();
+ destVideo.srcObject = null;
+ await garbageCollect();
+ });
+
+ // Setup pc1->pc2.
+ let haveTrackEvent = new Promise(r => pc2.ontrack = r);
+ exchangeIceCandidates(pc1, pc2);
+ await pc1.setLocalDescription();
+ await pc2.setRemoteDescription(pc1.localDescription);
+ await pc2.setLocalDescription();
+ await pc1.setRemoteDescription(pc2.localDescription);
+
+ // Display pc2 received track in video element.
+ const loadedMetadata = new Promise(r => destVideo.onloadedmetadata = r);
+ destVideo.srcObject = new MediaStream([(await haveTrackEvent).track]);
+
+ // Wait for video on the other side.
+ await onVideoChange();
+ const color = getVideoSignal(destVideo);
+ assert_not_equals(color, 0);
+
+ // Remove RTCPeerConnection references and garbage collect.
+ pc1 = null;
+ pc2 = null;
+ haveTrackEvent = null;
+ await garbageCollect();
+
+ // Check that a change to video input is reflected in the output, i.e., the
+ // peer connections were not garbage collected.
+ canvas.width = canvas.height = 240;
+ ctx.fillStyle = "red";
+ await onVideoChange();
+ assert_not_equals(color, getVideoSignal(destVideo));
+ }, "GC does not collect a peer connection pipe rendering to a video element");
+</script>
+</body>
+</html>
+
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-SLD-SRD-timing.https.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-SLD-SRD-timing.https.html
new file mode 100644
index 0000000000..36bde06c96
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-SLD-SRD-timing.https.html
@@ -0,0 +1,24 @@
+<!doctype html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<title></title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script>
+'use strict';
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const signalingStates = [];
+ pc.onsignalingstatechange = ev => signalingStates.push(pc.signalingState);
+ pc.addTransceiver('audio', {direction:'recvonly'});
+ const offer = await pc.createOffer();
+ const sldPromise = pc.setLocalDescription(offer);
+ const srdPromise = pc.setRemoteDescription(offer);
+ await Promise.all([sldPromise, srdPromise]);
+ assert_array_equals(signalingStates,
+ ['have-local-offer','stable','have-remote-offer']);
+}, 'setLocalDescription and setRemoteDescription are not racy');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-add-track-no-deadlock.https.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-add-track-no-deadlock.https.html
new file mode 100644
index 0000000000..81e3b73643
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-add-track-no-deadlock.https.html
@@ -0,0 +1,31 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerConnection addTrack does not deadlock</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="RTCPeerConnection-helper.js"></script>
+<script>
+ 'use strict';
+
+ // This test sets up two peer connections using a sequence of operations
+ // that triggered a deadlock in Chrome. See https://crbug.com/736725.
+ // If a deadlock is introduced again, this test times out.
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const stream = await getNoiseStream(
+ {audio: false, video: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const videoTrack = stream.getVideoTracks()[0];
+ pc1.addTrack(videoTrack, stream);
+ const offer = await pc1.createOffer();
+ await pc1.setLocalDescription(offer);
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+ const srdPromise = pc2.setRemoteDescription(offer);
+ pc2.addTrack(videoTrack, stream);
+ // The deadlock encountered in https://crbug.com/736725 occured here.
+ await srdPromise;
+ await pc2.createAnswer();
+ }, 'RTCPeerConnection addTrack does not deadlock.');
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-addIceCandidate-connectionSetup.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-addIceCandidate-connectionSetup.html
new file mode 100644
index 0000000000..cedc2ca8f0
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-addIceCandidate-connectionSetup.html
@@ -0,0 +1,92 @@
+<!doctype html>
+<meta name="timeout" content="long">
+<title>Test RTCPeerConnection.prototype.addIceCandidate</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="RTCPeerConnection-helper.js"></script>
+<script>
+ 'use strict';
+
+// This test may be flaky, so it's in its own file.
+// The test belongs in RTCPeerConnection-addIceCandidate.
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+ const transceiver = pc1.addTransceiver('video');
+
+ exchangeIceCandidates(pc1, pc2);
+ await exchangeOffer(pc1, pc2);
+ const answer = await pc2.createAnswer();
+ // Note that sequence of the following two calls is critical
+ // for test stability.
+ await pc1.setRemoteDescription(answer);
+ await pc2.setLocalDescription(answer);
+ await waitForState(transceiver.sender.transport, 'connected');
+}, 'Candidates are added dynamically; connection should work');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+ const transceiver = pc1.addTransceiver('video');
+
+ let candidates1to2 = [];
+ let candidates2to1 = [];
+ pc1.onicecandidate = e => candidates1to2.push(e.candidate);
+ pc2.onicecandidate = e => candidates2to1.push(e.candidate);
+ const pc2GatheredCandidates = new Promise((resolve) => {
+ pc2.addEventListener('icegatheringstatechange', () => {
+ if (pc2.iceGatheringState == 'complete') {
+ resolve();
+ }
+ });
+ });
+ await exchangeOffer(pc1, pc2);
+ let answer = await pc2.createAnswer();
+ await pc1.setRemoteDescription(answer);
+ await pc2.setLocalDescription(answer);
+ await pc2GatheredCandidates;
+ // Add candidates to pc1, ensuring that it goes to "connecting" state before "connected".
+ // We do not iterate/await because repeatedly awaiting while we serially add
+ // the candidates opens the opportunity to miss the 'connecting' transition.
+ const addCandidatesDone = Promise.all(candidates2to1.map(c => pc1.addIceCandidate(c)));
+ await waitForState(transceiver.sender.transport, 'connecting');
+ await addCandidatesDone;
+ await waitForState(transceiver.sender.transport, 'connected');
+}, 'Candidates are added at PC1; connection should work');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+ const transceiver = pc1.addTransceiver('video');
+
+ let candidates1to2 = [];
+ let candidates2to1 = [];
+ pc1.onicecandidate = e => candidates1to2.push(e.candidate);
+ pc2.onicecandidate = e => candidates2to1.push(e.candidate);
+ const pc1GatheredCandidates = new Promise((resolve) => {
+ pc1.addEventListener('icegatheringstatechange', () => {
+ if (pc1.iceGatheringState == 'complete') {
+ resolve();
+ }
+ });
+ });
+ await exchangeOffer(pc1, pc2);
+ let answer = await pc2.createAnswer();
+ await pc1.setRemoteDescription(answer);
+ await pc2.setLocalDescription(answer);
+ await pc1GatheredCandidates;
+ // Add candidates to pc2
+ // We do not iterate/await because repeatedly awaiting while we serially add
+ // the candidates opens the opportunity to miss the ICE state transitions.
+ await Promise.all(candidates1to2.map(c => pc2.addIceCandidate(c)));
+ await waitForState(transceiver.sender.transport, 'connected');
+}, 'Candidates are added at PC2; connection should work');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-addIceCandidate-timing.https.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-addIceCandidate-timing.https.html
new file mode 100644
index 0000000000..9793844f56
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-addIceCandidate-timing.https.html
@@ -0,0 +1,149 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+
+'use strict';
+
+// In this test, the promises should resolve in the execution order
+// (setLocalDescription, setLocalDescription, addIceCandidate) as is ensured by
+// the Operations Chain; if an operation is pending, executing another operation
+// will queue it. This test will fail if an Operations Chain is not implemented,
+// but it gives the implementation some slack: it only ensures that
+// addIceCandidate() is not resolved first, allowing timing issues in resolving
+// promises where the test still passes even if addIceCandidate() is resolved
+// *before* the second setLocalDescription().
+//
+// This test covers Chrome issue (https://crbug.com/1019222), but does not
+// require setLocalDescription-promises to resolve immediately which is another
+// Chrome bug (https://crbug.com/1019232). The true order is covered by the next
+// test.
+// TODO(https://crbug.com/1019232): Delete this test when the next test passes
+// in Chrome.
+promise_test(async t => {
+ const caller = new RTCPeerConnection();
+ t.add_cleanup(() => caller.close());
+ const callee = new RTCPeerConnection();
+ t.add_cleanup(() => callee.close());
+ caller.addTransceiver('audio');
+
+ const candidatePromise = new Promise(resolve => {
+ caller.onicecandidate = e => resolve(e.candidate);
+ });
+ await caller.setLocalDescription(await caller.createOffer());
+ await callee.setRemoteDescription(caller.localDescription);
+ const candidate = await candidatePromise;
+
+ // Chain setLocalDescription(), setLocalDescription() and addIceCandidate()
+ // without performing await between the calls.
+ const pendingPromises = [];
+ const resolveOrder = [];
+ pendingPromises.push(callee.setLocalDescription().then(() => {
+ resolveOrder.push('setLocalDescription 1');
+ }));
+ pendingPromises.push(callee.setLocalDescription().then(() => {
+ resolveOrder.push('setLocalDescription 2');
+ }));
+ pendingPromises.push(callee.addIceCandidate(candidate).then(() => {
+ resolveOrder.push('addIceCandidate');
+ }));
+ await Promise.all(pendingPromises);
+
+ assert_equals(resolveOrder[0], 'setLocalDescription 1');
+}, 'addIceCandidate is not resolved first if 2x setLocalDescription ' +
+ 'operations are pending');
+
+promise_test(async t => {
+ const caller = new RTCPeerConnection();
+ t.add_cleanup(() => caller.close());
+ const callee = new RTCPeerConnection();
+ t.add_cleanup(() => callee.close());
+ caller.addTransceiver('audio');
+
+ const candidatePromise = new Promise(resolve => {
+ caller.onicecandidate = e => resolve(e.candidate);
+ });
+ await caller.setLocalDescription(await caller.createOffer());
+ await callee.setRemoteDescription(caller.localDescription);
+ const candidate = await candidatePromise;
+
+ // Chain setLocalDescription(), setLocalDescription() and addIceCandidate()
+ // without performing await between the calls.
+ const pendingPromises = [];
+ const resolveOrder = [];
+ pendingPromises.push(callee.setLocalDescription().then(() => {
+ resolveOrder.push('setLocalDescription 1');
+ }));
+ pendingPromises.push(callee.setLocalDescription().then(() => {
+ resolveOrder.push('setLocalDescription 2');
+ }));
+ pendingPromises.push(callee.addIceCandidate(candidate).then(() => {
+ resolveOrder.push('addIceCandidate');
+ }));
+ await Promise.all(pendingPromises);
+
+ // This test verifies that both issues described in https://crbug.com/1019222
+ // and https://crbug.com/1019232 are fixed. If this test passes in Chrome, the
+ // ICE candidate exchange issues described in
+ // https://github.com/web-platform-tests/wpt/issues/19866 should be resolved.
+ assert_array_equals(
+ resolveOrder,
+ ['setLocalDescription 1', 'setLocalDescription 2', 'addIceCandidate']);
+}, 'addIceCandidate and setLocalDescription are resolved in the correct ' +
+ 'order, as defined by the operations chain specification');
+
+promise_test(async t => {
+ const caller = new RTCPeerConnection();
+ t.add_cleanup(() => caller.close());
+ const callee = new RTCPeerConnection();
+ t.add_cleanup(() => callee.close());
+ caller.addTransceiver('audio');
+ let events = [];
+ let pendingPromises = [];
+
+ const onCandidatePromise = new Promise(resolve => {
+ caller.onicecandidate = () => {
+ events.push('candidate generated');
+ resolve();
+ }
+ });
+ pendingPromises.push(onCandidatePromise);
+ pendingPromises.push(caller.setLocalDescription().then(() => {
+ events.push('setLocalDescription');
+ }));
+ await Promise.all(pendingPromises);
+ assert_array_equals(events, ['setLocalDescription', 'candidate generated']);
+}, 'onicecandidate fires after resolving setLocalDescription in offerer');
+
+promise_test(async t => {
+ const caller = new RTCPeerConnection();
+ t.add_cleanup(() => caller.close());
+ const callee = new RTCPeerConnection();
+ t.add_cleanup(() => callee.close());
+ caller.addTransceiver('audio');
+ let events = [];
+ let pendingPromises = [];
+
+ caller.onicecandidate = (ev) => {
+ if (ev.candidate) {
+ callee.addIceCandidate(ev.candidate);
+ }
+ }
+ const offer = await caller.createOffer();
+ const onCandidatePromise = new Promise(resolve => {
+ callee.onicecandidate = () => {
+ events.push('candidate generated');
+ resolve();
+ }
+ });
+ await callee.setRemoteDescription(offer);
+ const answer = await callee.createAnswer();
+ pendingPromises.push(onCandidatePromise);
+ pendingPromises.push(callee.setLocalDescription(answer).then(() => {
+ events.push('setLocalDescription');
+ }));
+ await Promise.all(pendingPromises);
+ assert_array_equals(events, ['setLocalDescription', 'candidate generated']);
+}, 'onicecandidate fires after resolving setLocalDescription in answerer');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-addIceCandidate.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-addIceCandidate.html
new file mode 100644
index 0000000000..d8e24d608b
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-addIceCandidate.html
@@ -0,0 +1,631 @@
+<!doctype html>
+<title>Test RTCPeerConnection.prototype.addIceCandidate</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="RTCPeerConnection-helper.js"></script>
+<script>
+ 'use strict';
+
+ // SDP copied from JSEP Example 7.1
+ // It contains two media streams with different ufrags
+ // to test if candidate is added to the correct stream
+ const sdp = `v=0
+o=- 4962303333179871722 1 IN IP4 0.0.0.0
+s=-
+t=0 0
+a=ice-options:trickle
+a=group:BUNDLE a1 v1
+a=group:LS a1 v1
+m=audio 10100 UDP/TLS/RTP/SAVPF 96 0 8 97 98
+c=IN IP4 203.0.113.100
+a=mid:a1
+a=sendrecv
+a=rtpmap:96 opus/48000/2
+a=rtpmap:0 PCMU/8000
+a=rtpmap:8 PCMA/8000
+a=rtpmap:97 telephone-event/8000
+a=rtpmap:98 telephone-event/48000
+a=maxptime:120
+a=extmap:1 urn:ietf:params:rtp-hdrext:sdes:mid
+a=extmap:2 urn:ietf:params:rtp-hdrext:ssrc-audio-level
+a=msid:47017fee-b6c1-4162-929c-a25110252400 f83006c5-a0ff-4e0a-9ed9-d3e6747be7d9
+a=ice-ufrag:ETEn
+a=ice-pwd:OtSK0WpNtpUjkY4+86js7ZQl
+a=fingerprint:sha-256 19:E2:1C:3B:4B:9F:81:E6:B8:5C:F4:A5:A8:D8:73:04:BB:05:2F:70:9F:04:A9:0E:05:E9:26:33:E8:70:88:A2
+a=setup:actpass
+a=dtls-id:1
+a=rtcp:10101 IN IP4 203.0.113.100
+a=rtcp-mux
+a=rtcp-rsize
+m=video 10102 UDP/TLS/RTP/SAVPF 100 101
+c=IN IP4 203.0.113.100
+a=mid:v1
+a=sendrecv
+a=rtpmap:100 VP8/90000
+a=rtpmap:101 rtx/90000
+a=fmtp:101 apt=100
+a=extmap:1 urn:ietf:params:rtp-hdrext:sdes:mid
+a=rtcp-fb:100 ccm fir
+a=rtcp-fb:100 nack
+a=rtcp-fb:100 nack pli
+a=msid:47017fee-b6c1-4162-929c-a25110252400 f30bdb4a-5db8-49b5-bcdc-e0c9a23172e0
+a=ice-ufrag:BGKk
+a=ice-pwd:mqyWsAjvtKwTGnvhPztQ9mIf
+a=fingerprint:sha-256 19:E2:1C:3B:4B:9F:81:E6:B8:5C:F4:A5:A8:D8:73:04:BB:05:2F:70:9F:04:A9:0E:05:E9:26:33:E8:70:88:A2
+a=setup:actpass
+a=dtls-id:1
+a=rtcp:10103 IN IP4 203.0.113.100
+a=rtcp-mux
+a=rtcp-rsize
+`;
+
+ const sessionDesc = { type: 'offer', sdp };
+
+ // valid candidate attributes
+ const sdpMid1 = 'a1';
+ const sdpMLineIndex1 = 0;
+ const usernameFragment1 = 'ETEn';
+
+ const sdpMid2 = 'v1';
+ const sdpMLineIndex2 = 1;
+ const usernameFragment2 = 'BGKk';
+
+ const mediaLine1 = 'm=audio';
+ const mediaLine2 = 'm=video';
+
+ const candidateStr1 = 'candidate:1 1 udp 2113929471 203.0.113.100 10100 typ host';
+ const candidateStr2 = 'candidate:1 2 udp 2113929470 203.0.113.100 10101 typ host';
+ const invalidCandidateStr = '(Invalid) candidate \r\n string';
+
+ const candidateLine1 = `a=${candidateStr1}`;
+ const candidateLine2 = `a=${candidateStr2}`;
+ const endOfCandidateLine = 'a=end-of-candidates';
+
+ // Copied from MDN
+ function escapeRegExp(string) {
+ return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+ }
+
+ function is_candidate_line_between(sdp, beforeMediaLine, candidateLine, afterMediaLine) {
+ const line1 = escapeRegExp(beforeMediaLine);
+ const line2 = escapeRegExp(candidateLine);
+ const line3 = escapeRegExp(afterMediaLine);
+
+ const regex = new RegExp(`${line1}[^]+${line2}[^]+${line3}`);
+ return regex.test(sdp);
+ }
+
+ // Check that a candidate line is found after the first media line
+ // but before the second, i.e. it belongs to the first media stream
+ function assert_candidate_line_between(sdp, beforeMediaLine, candidateLine, afterMediaLine) {
+ assert_true(is_candidate_line_between(sdp, beforeMediaLine, candidateLine, afterMediaLine),
+ `Expect candidate line to be found between media lines ${beforeMediaLine} and ${afterMediaLine}`);
+ }
+
+ // Check that a candidate line is found after the second media line
+ // i.e. it belongs to the second media stream
+ function is_candidate_line_after(sdp, beforeMediaLine, candidateLine) {
+ const line1 = escapeRegExp(beforeMediaLine);
+ const line2 = escapeRegExp(candidateLine);
+
+ const regex = new RegExp(`${line1}[^]+${line2}`);
+
+ return regex.test(sdp);
+ }
+
+ function assert_candidate_line_after(sdp, beforeMediaLine, candidateLine) {
+ assert_true(is_candidate_line_after(sdp, beforeMediaLine, candidateLine),
+ `Expect candidate line to be found after media line ${beforeMediaLine}`);
+ }
+
+ /*
+ 4.4.2. addIceCandidate
+ 4. Return the result of enqueuing the following steps:
+ 1. If remoteDescription is null return a promise rejected with a
+ newly created InvalidStateError.
+ */
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+
+ t.add_cleanup(() => pc.close());
+
+ return promise_rejects_dom(t, 'InvalidStateError',
+ pc.addIceCandidate({
+ candidate: candidateStr1,
+ sdpMid: sdpMid1,
+ sdpMLineIndex: sdpMLineIndex1,
+ usernameFragment: usernameFragment1
+ }));
+ }, 'Add ICE candidate before setting remote description should reject with InvalidStateError');
+
+ /*
+ Success cases
+ */
+
+ // All of these should work, because all of these end up being equivalent to the
+ // same thing; an end-of-candidates signal for all levels/mids/ufrags.
+ [
+ // This is just the default. Everything else here is equivalent to this.
+ {
+ candidate: '',
+ sdpMid: null,
+ sdpMLineIndex: null,
+ usernameFragment: undefined
+ },
+ // The arg is optional, so when passing undefined we'll just get the default
+ undefined,
+ // The arg is optional, but not nullable, so we get the default again
+ null,
+ // Members in the dictionary take their default values
+ {}
+ ].forEach(init => {
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+
+ t.add_cleanup(() => pc.close());
+
+ await pc.setRemoteDescription(sessionDesc);
+ await pc.addIceCandidate(init);
+ }, `addIceCandidate(${JSON.stringify(init)}) works`);
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+
+ t.add_cleanup(() => pc.close());
+
+ await pc.setRemoteDescription(sessionDesc);
+ await pc.addIceCandidate(init);
+ assert_candidate_line_between(pc.remoteDescription.sdp,
+ mediaLine1, endOfCandidateLine, mediaLine2);
+ assert_candidate_line_after(pc.remoteDescription.sdp,
+ mediaLine2, endOfCandidateLine);
+ }, `addIceCandidate(${JSON.stringify(init)}) adds a=end-of-candidates to both m-sections`);
+ });
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ await pc.setRemoteDescription(sessionDesc);
+ await pc.setLocalDescription(await pc.createAnswer());
+ await pc.addIceCandidate({});
+ assert_candidate_line_between(pc.remoteDescription.sdp,
+ mediaLine1, endOfCandidateLine, mediaLine2);
+ assert_candidate_line_after(pc.remoteDescription.sdp,
+ mediaLine2, endOfCandidateLine);
+ }, 'addIceCandidate({}) in stable should work, and add a=end-of-candidates to both m-sections');
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+
+ t.add_cleanup(() => pc.close());
+
+ await pc.setRemoteDescription(sessionDesc);
+ await pc.addIceCandidate({
+ usernameFragment: usernameFragment1,
+ sdpMid: sdpMid1
+ });
+ assert_candidate_line_between(pc.remoteDescription.sdp,
+ mediaLine1, endOfCandidateLine, mediaLine2);
+ assert_false(is_candidate_line_after(pc.remoteDescription.sdp,
+ mediaLine2, endOfCandidateLine));
+ }, 'addIceCandidate({usernameFragment: usernameFragment1, sdpMid: sdpMid1}) should work, and add a=end-of-candidates to the first m-section');
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+
+ t.add_cleanup(() => pc.close());
+
+ await pc.setRemoteDescription(sessionDesc);
+ await pc.addIceCandidate({
+ usernameFragment: usernameFragment2,
+ sdpMLineIndex: 1
+ });
+ assert_false(is_candidate_line_between(pc.remoteDescription.sdp,
+ mediaLine1, endOfCandidateLine, mediaLine2));
+ assert_true(is_candidate_line_after(pc.remoteDescription.sdp,
+ mediaLine2, endOfCandidateLine));
+ }, 'addIceCandidate({usernameFragment: usernameFragment2, sdpMLineIndex: 1}) should work, and add a=end-of-candidates to the first m-section');
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+
+ t.add_cleanup(() => pc.close());
+
+ await pc.setRemoteDescription(sessionDesc);
+ await promise_rejects_dom(t, 'OperationError',
+ pc.addIceCandidate({usernameFragment: "no such ufrag"}));
+ }, 'addIceCandidate({usernameFragment: "no such ufrag"}) should not work');
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+
+ t.add_cleanup(() => pc.close());
+
+ await pc.setRemoteDescription(sessionDesc)
+ await pc.addIceCandidate({
+ candidate: candidateStr1,
+ sdpMid: sdpMid1,
+ sdpMLineIndex: sdpMLineIndex1,
+ usernameFragement: usernameFragment1
+ });
+ assert_candidate_line_after(pc.remoteDescription.sdp,
+ mediaLine1, candidateStr1);
+ }, 'Add ICE candidate after setting remote description should succeed');
+
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+
+ t.add_cleanup(() => pc.close());
+
+ return pc.setRemoteDescription(sessionDesc)
+ .then(() => pc.addIceCandidate(new RTCIceCandidate({
+ candidate: candidateStr1,
+ sdpMid: sdpMid1,
+ sdpMLineIndex: sdpMLineIndex1,
+ usernameFragement: usernameFragment1
+ })));
+ }, 'Add ICE candidate with RTCIceCandidate should succeed');
+
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+
+ t.add_cleanup(() => pc.close());
+ return pc.setRemoteDescription(sessionDesc)
+ .then(() => pc.addIceCandidate({
+ candidate: candidateStr1,
+ sdpMid: sdpMid1 }));
+ }, 'Add candidate with only valid sdpMid should succeed');
+
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+
+ t.add_cleanup(() => pc.close());
+
+ return pc.setRemoteDescription(sessionDesc)
+ .then(() => pc.addIceCandidate(new RTCIceCandidate({
+ candidate: candidateStr1,
+ sdpMid: sdpMid1 })));
+ }, 'Add candidate with only valid sdpMid and RTCIceCandidate should succeed');
+
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+
+ t.add_cleanup(() => pc.close());
+
+ return pc.setRemoteDescription(sessionDesc)
+ .then(() => pc.addIceCandidate({
+ candidate: candidateStr1,
+ sdpMLineIndex: sdpMLineIndex1 }));
+ }, 'Add candidate with only valid sdpMLineIndex should succeed');
+
+ /*
+ 4.4.2. addIceCandidate
+ 4.6.2. If candidate is applied successfully, the user agent MUST queue
+ a task that runs the following steps:
+ 2. If connection.pendingRemoteDescription is non-null, and represents
+ the ICE generation for which candidate was processed, add
+ candidate to connection.pendingRemoteDescription.
+ 3. If connection.currentRemoteDescription is non-null, and represents
+ the ICE generation for which candidate was processed, add
+ candidate to connection.currentRemoteDescription.
+ */
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+
+ t.add_cleanup(() => pc.close());
+
+ return pc.setRemoteDescription(sessionDesc)
+ .then(() => pc.addIceCandidate({
+ candidate: candidateStr1,
+ sdpMid: sdpMid1,
+ sdpMLineIndex: sdpMLineIndex1,
+ usernameFragement: usernameFragment1
+ }))
+ .then(() => {
+ assert_candidate_line_between(pc.remoteDescription.sdp,
+ mediaLine1, candidateLine1, mediaLine2);
+ });
+ }, 'addIceCandidate with first sdpMid and sdpMLineIndex add candidate to first media stream');
+
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+
+ t.add_cleanup(() => pc.close());
+
+ return pc.setRemoteDescription(sessionDesc)
+ .then(() => pc.addIceCandidate({
+ candidate: candidateStr2,
+ sdpMid: sdpMid2,
+ sdpMLineIndex: sdpMLineIndex2,
+ usernameFragment: usernameFragment2
+ }))
+ .then(() => {
+ assert_candidate_line_after(pc.remoteDescription.sdp,
+ mediaLine2, candidateLine2);
+ });
+ }, 'addIceCandidate with second sdpMid and sdpMLineIndex should add candidate to second media stream');
+
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+
+ t.add_cleanup(() => pc.close());
+
+ return pc.setRemoteDescription(sessionDesc)
+ .then(() => pc.addIceCandidate({
+ candidate: candidateStr1,
+ sdpMid: sdpMid1,
+ sdpMLineIndex: sdpMLineIndex1,
+ usernameFragment: null
+ }))
+ .then(() => {
+ assert_candidate_line_between(pc.remoteDescription.sdp,
+ mediaLine1, candidateLine1, mediaLine2);
+ });
+ }, 'Add candidate for first media stream with null usernameFragment should add candidate to first media stream');
+
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+
+ t.add_cleanup(() => pc.close());
+
+ return pc.setRemoteDescription(sessionDesc)
+ .then(() => pc.addIceCandidate({
+ candidate: candidateStr1,
+ sdpMid: sdpMid1,
+ sdpMLineIndex: sdpMLineIndex1,
+ usernameFragement: usernameFragment1
+ }))
+ .then(() => pc.addIceCandidate({
+ candidate: candidateStr2,
+ sdpMid: sdpMid2,
+ sdpMLineIndex: sdpMLineIndex2,
+ usernameFragment: usernameFragment2
+ }))
+ .then(() => {
+ assert_candidate_line_between(pc.remoteDescription.sdp,
+ mediaLine1, candidateLine1, mediaLine2);
+
+ assert_candidate_line_after(pc.remoteDescription.sdp,
+ mediaLine2, candidateLine2);
+ });
+ }, 'Adding multiple candidates should add candidates to their corresponding media stream');
+
+ /*
+ 4.4.2. addIceCandidate
+ 4.6. If candidate.candidate is an empty string, process candidate as an
+ end-of-candidates indication for the corresponding media description
+ and ICE candidate generation.
+ 2. If candidate is applied successfully, the user agent MUST queue
+ a task that runs the following steps:
+ 2. If connection.pendingRemoteDescription is non-null, and represents
+ the ICE generation for which candidate was processed, add
+ candidate to connection.pendingRemoteDescription.
+ 3. If connection.currentRemoteDescription is non-null, and represents
+ the ICE generation for which candidate was processed, add
+ candidate to connection.currentRemoteDescription.
+ */
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+
+ t.add_cleanup(() => pc.close());
+
+ return pc.setRemoteDescription(sessionDesc)
+ .then(() => pc.addIceCandidate({
+ candidate: candidateStr1,
+ sdpMid: sdpMid1,
+ sdpMLineIndex: sdpMLineIndex1,
+ usernameFragement: usernameFragment1
+ }))
+ .then(() => pc.addIceCandidate({
+ candidate: '',
+ sdpMid: sdpMid1,
+ sdpMLineIndex: sdpMLineIndex1,
+ usernameFragement: usernameFragment1
+ }))
+ .then(() => {
+ assert_candidate_line_between(pc.remoteDescription.sdp,
+ mediaLine1, candidateLine1, mediaLine2);
+
+ assert_candidate_line_between(pc.remoteDescription.sdp,
+ mediaLine1, endOfCandidateLine, mediaLine2);
+ });
+ }, 'Add with empty candidate string (end of candidates) should succeed');
+
+ /*
+ 4.4.2. addIceCandidate
+ 3. If both sdpMid and sdpMLineIndex are null, return a promise rejected
+ with a newly created TypeError.
+ */
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+
+ t.add_cleanup(() => pc.close());
+
+ return pc.setRemoteDescription(sessionDesc)
+ .then(() =>
+ promise_rejects_js(t, TypeError,
+ pc.addIceCandidate({
+ candidate: candidateStr1,
+ sdpMid: null,
+ sdpMLineIndex: null
+ })));
+ }, 'Add candidate with both sdpMid and sdpMLineIndex manually set to null should reject with TypeError');
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ await pc.setRemoteDescription(sessionDesc);
+ promise_rejects_js(t, TypeError,
+ pc.addIceCandidate({candidate: candidateStr1}));
+ }, 'addIceCandidate with a candidate and neither sdpMid nor sdpMLineIndex should reject with TypeError');
+
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+
+ t.add_cleanup(() => pc.close());
+
+ return pc.setRemoteDescription(sessionDesc)
+ .then(() =>
+ promise_rejects_js(t, TypeError,
+ pc.addIceCandidate({
+ candidate: candidateStr1
+ })));
+ }, 'Add candidate with only valid candidate string should reject with TypeError');
+
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+
+ t.add_cleanup(() => pc.close());
+
+ return pc.setRemoteDescription(sessionDesc)
+ .then(() =>
+ promise_rejects_js(t, TypeError,
+ pc.addIceCandidate({
+ candidate: invalidCandidateStr,
+ sdpMid: null,
+ sdpMLineIndex: null
+ })));
+ }, 'Add candidate with invalid candidate string and both sdpMid and sdpMLineIndex null should reject with TypeError');
+
+ /*
+ 4.4.2. addIceCandidate
+ 4.3. If candidate.sdpMid is not null, run the following steps:
+ 1. If candidate.sdpMid is not equal to the mid of any media
+ description in remoteDescription , reject p with a newly
+ created OperationError and abort these steps.
+ */
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+
+ t.add_cleanup(() => pc.close());
+
+ return pc.setRemoteDescription(sessionDesc)
+ .then(() =>
+ promise_rejects_dom(t, 'OperationError',
+ pc.addIceCandidate({
+ candidate: candidateStr1,
+ sdpMid: 'invalid',
+ sdpMLineIndex: sdpMLineIndex1,
+ usernameFragement: usernameFragment1
+ })));
+ }, 'Add candidate with invalid sdpMid should reject with OperationError');
+
+ /*
+ 4.4.2. addIceCandidate
+ 4.4. Else, if candidate.sdpMLineIndex is not null, run the following
+ steps:
+ 1. If candidate.sdpMLineIndex is equal to or larger than the
+ number of media descriptions in remoteDescription , reject p
+ with a newly created OperationError and abort these steps.
+ */
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+
+ t.add_cleanup(() => pc.close());
+
+ return pc.setRemoteDescription(sessionDesc)
+ .then(() =>
+ promise_rejects_dom(t, 'OperationError',
+ pc.addIceCandidate({
+ candidate: candidateStr1,
+ sdpMLineIndex: 2,
+ usernameFragement: usernameFragment1
+ })));
+ }, 'Add candidate with invalid sdpMLineIndex should reject with OperationError');
+
+ // There is an "Else" for the statement:
+ // "Else, if candidate.sdpMLineIndex is not null, ..."
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+
+ t.add_cleanup(() => pc.close());
+
+ return pc.setRemoteDescription(sessionDesc)
+ .then(() => pc.addIceCandidate({
+ candidate: candidateStr1,
+ sdpMid: sdpMid1,
+ sdpMLineIndex: 2,
+ usernameFragement: usernameFragment1
+ }));
+ }, 'Invalid sdpMLineIndex should be ignored if valid sdpMid is provided');
+
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+
+ t.add_cleanup(() => pc.close());
+
+ return pc.setRemoteDescription(sessionDesc)
+ .then(() => pc.addIceCandidate({
+ candidate: candidateStr2,
+ sdpMid: sdpMid2,
+ sdpMLineIndex: sdpMLineIndex2,
+ usernameFragment: null
+ }))
+ .then(() => {
+ assert_candidate_line_after(pc.remoteDescription.sdp,
+ mediaLine2, candidateLine2);
+ });
+ }, 'Add candidate for media stream 2 with null usernameFragment should succeed');
+
+ /*
+ 4.3.2. addIceCandidate
+ 4.5. If candidate.usernameFragment is neither undefined nor null, and is not equal
+ to any usernameFragment present in the corresponding media description of an
+ applied remote description, reject p with a newly created
+ OperationError and abort these steps.
+ */
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+
+ t.add_cleanup(() => pc.close());
+
+ return pc.setRemoteDescription(sessionDesc)
+ .then(() =>
+ promise_rejects_dom(t, 'OperationError',
+ pc.addIceCandidate({
+ candidate: candidateStr1,
+ sdpMid: sdpMid1,
+ sdpMLineIndex: sdpMLineIndex1,
+ usernameFragment: 'invalid'
+ })));
+ }, 'Add candidate with invalid usernameFragment should reject with OperationError');
+
+ /*
+ 4.4.2. addIceCandidate
+ 4.6.1. If candidate could not be successfully added the user agent MUST
+ queue a task that runs the following steps:
+ 2. Reject p with a DOMException object whose name attribute has
+ the value OperationError and abort these steps.
+ */
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+
+ t.add_cleanup(() => pc.close());
+
+ return pc.setRemoteDescription(sessionDesc)
+ .then(() =>
+ promise_rejects_dom(t, 'OperationError',
+ pc.addIceCandidate({
+ candidate: invalidCandidateStr,
+ sdpMid: sdpMid1,
+ sdpMLineIndex: sdpMLineIndex1,
+ usernameFragement: usernameFragment1
+ })));
+ }, 'Add candidate with invalid candidate string should reject with OperationError');
+
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+
+ t.add_cleanup(() => pc.close());
+
+ return pc.setRemoteDescription(sessionDesc)
+ .then(() =>
+ promise_rejects_dom(t, 'OperationError',
+ pc.addIceCandidate({
+ candidate: candidateStr2,
+ sdpMid: sdpMid2,
+ sdpMLineIndex: sdpMLineIndex2,
+ usernameFragment: usernameFragment1
+ })));
+ }, 'Add candidate with sdpMid belonging to different usernameFragment should reject with OperationError');
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-addTrack.https.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-addTrack.https.html
new file mode 100644
index 0000000000..91665822c4
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-addTrack.https.html
@@ -0,0 +1,394 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerConnection.prototype.addTrack</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src=/resources/testdriver.js></script>
+<script src=/resources/testdriver-vendor.js></script>
+<script src='../mediacapture-streams/permission-helper.js'></script>
+<script src="RTCPeerConnection-helper.js"></script>
+<script>
+ 'use strict';
+
+ // Test is based on the following editor draft:
+ // https://w3c.github.io/webrtc-pc/archives/20170605/webrtc.html
+
+ // The following helper functions are called from RTCPeerConnection-helper.js:
+ // getNoiseStream()
+
+ /*
+ 5.1. RTCPeerConnection Interface Extensions
+ partial interface RTCPeerConnection {
+ ...
+ sequence<RTCRtpSender> getSenders();
+ sequence<RTCRtpReceiver> getReceivers();
+ sequence<RTCRtpTransceiver> getTransceivers();
+ RTCRtpSender addTrack(MediaStreamTrack track,
+ MediaStream... streams);
+ RTCRtpTransceiver addTransceiver((MediaStreamTrack or DOMString) trackOrKind,
+ optional RTCRtpTransceiverInit init);
+ };
+
+ Note
+ While addTrack checks if the MediaStreamTrack given as an argument is
+ already being sent to avoid sending the same MediaStreamTrack twice,
+ the other ways do not, allowing the same MediaStreamTrack to be sent
+ several times simultaneously.
+ */
+
+ /*
+ 5.1. addTrack
+ 4. If connection's [[isClosed]] slot is true, throw an InvalidStateError.
+ */
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const stream = await getNoiseStream({ audio: true });
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const [track] = stream.getTracks();
+
+ pc.close();
+ assert_throws_dom('InvalidStateError', () => pc.addTrack(track, stream))
+ }, 'addTrack when pc is closed should throw InvalidStateError');
+
+ /*
+ 5.1. addTrack
+ 8. If sender is null, run the following steps:
+ 1. Create an RTCRtpSender with track and streams and let sender be
+ the result.
+ 2. Create an RTCRtpReceiver with track.kind as kind and let receiver
+ be the result.
+ 3. Create an RTCRtpTransceiver with sender and receiver and let
+ transceiver be the result.
+ 4. Add transceiver to connection's set of transceivers.
+ */
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const stream = await getNoiseStream({ audio: true });
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const [track] = stream.getTracks();
+
+ const sender = pc.addTrack(track);
+
+ assert_true(sender instanceof RTCRtpSender,
+ 'Expect sender to be instance of RTCRtpSender');
+
+ assert_equals(sender.track, track,
+ `Expect sender's track to be the added track`);
+
+ const transceivers = pc.getTransceivers();
+ assert_equals(transceivers.length, 1,
+ 'Expect only one transceiver with sender added');
+
+ const [transceiver] = transceivers;
+ assert_equals(transceiver.sender, sender);
+
+ assert_array_equals([sender], pc.getSenders(),
+ 'Expect only one sender with given track added');
+
+ const { receiver } = transceiver;
+ assert_equals(receiver.track.kind, 'audio');
+ assert_array_equals([transceiver.receiver], pc.getReceivers(),
+ 'Expect only one receiver associated with transceiver added');
+ }, 'addTrack with single track argument and no stream should succeed');
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const stream = await getNoiseStream({ audio: true });
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const [track] = stream.getTracks();
+
+ const sender = pc.addTrack(track, stream);
+
+ assert_true(sender instanceof RTCRtpSender,
+ 'Expect sender to be instance of RTCRtpSender');
+
+ assert_equals(sender.track, track,
+ `Expect sender's track to be the added track`);
+ }, 'addTrack with single track argument and single stream should succeed');
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const stream = await getNoiseStream({ audio: true });
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const [track] = stream.getTracks();
+
+ const stream2 = new MediaStream([track]);
+ const sender = pc.addTrack(track, stream, stream2);
+
+ assert_true(sender instanceof RTCRtpSender,
+ 'Expect sender to be instance of RTCRtpSender');
+
+ assert_equals(sender.track, track,
+ `Expect sender's track to be the added track`);
+ }, 'addTrack with single track argument and multiple streams should succeed');
+
+ /*
+ 5.1. addTrack
+ 5. Let senders be the result of executing the CollectSenders algorithm.
+ If an RTCRtpSender for track already exists in senders, throw an
+ InvalidAccessError.
+ */
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const stream = await getNoiseStream({ audio: true });
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const [track] = stream.getTracks();
+
+ pc.addTrack(track, stream);
+ assert_throws_dom('InvalidAccessError', () => pc.addTrack(track, stream));
+ }, 'Adding the same track multiple times should throw InvalidAccessError');
+
+ /*
+ 5.1. addTrack
+ 6. The steps below describe how to determine if an existing sender can
+ be reused.
+
+ If any RTCRtpSender object in senders matches all the following
+ criteria, let sender be that object, or null otherwise:
+ - The sender's track is null.
+ - The transceiver kind of the RTCRtpTransceiver, associated with
+ the sender, matches track's kind.
+ - The sender has never been used to send. More precisely, the
+ RTCRtpTransceiver associated with the sender has never had a
+ currentDirection of sendrecv or sendonly.
+ 7. If sender is not null, run the following steps to use that sender:
+ 1. Set sender.track to track.
+ 3. Enable sending direction on the RTCRtpTransceiver associated
+ with sender.
+ */
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const transceiver = pc.addTransceiver('audio', { direction: 'recvonly' });
+ assert_equals(transceiver.sender.track, null);
+ assert_equals(transceiver.direction, 'recvonly');
+
+ await setMediaPermission("granted", ["microphone"]);
+ const stream = await navigator.mediaDevices.getUserMedia({audio: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const [track] = stream.getTracks();
+ const sender = pc.addTrack(track);
+
+ assert_equals(sender, transceiver.sender);
+ assert_equals(sender.track, track);
+ assert_equals(transceiver.direction, 'sendrecv');
+ assert_array_equals([sender], pc.getSenders());
+ }, 'addTrack with existing sender with null track, same kind, and recvonly direction should reuse sender');
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const transceiver = pc.addTransceiver('audio');
+ assert_equals(transceiver.sender.track, null);
+ assert_equals(transceiver.direction, 'sendrecv');
+
+ const stream = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const [track] = stream.getTracks();
+ const sender = pc.addTrack(track);
+
+ assert_equals(sender.track, track);
+ assert_equals(sender, transceiver.sender);
+ }, 'addTrack with existing sender that has not been used to send should reuse the sender');
+
+ promise_test(async t => {
+ const caller = new RTCPeerConnection();
+ t.add_cleanup(() => caller.close());
+ const callee = new RTCPeerConnection();
+ t.add_cleanup(() => callee.close());
+
+ const stream = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const [track] = stream.getTracks();
+ const transceiver = caller.addTransceiver(track);
+ {
+ const offer = await caller.createOffer();
+ await caller.setLocalDescription(offer);
+ await callee.setRemoteDescription(offer);
+ const answer = await callee.createAnswer();
+ await callee.setLocalDescription(answer);
+ await caller.setRemoteDescription(answer);
+ }
+ assert_equals(transceiver.currentDirection, 'sendonly');
+
+ caller.removeTrack(transceiver.sender);
+ {
+ const offer = await caller.createOffer();
+ await caller.setLocalDescription(offer);
+ await callee.setRemoteDescription(offer);
+ const answer = await callee.createAnswer();
+ await callee.setLocalDescription(answer);
+ await caller.setRemoteDescription(answer);
+ }
+ assert_equals(transceiver.direction, 'recvonly');
+ assert_equals(transceiver.currentDirection, 'inactive');
+
+ // |transceiver.sender| is currently not used for sending, but it should not
+ // be reused because it has been used for sending before.
+ const sender = caller.addTrack(track);
+ assert_true(sender != null);
+ assert_not_equals(sender, transceiver.sender);
+ }, 'addTrack with existing sender that has been used to send should create new sender');
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const transceiver = pc.addTransceiver('video', { direction: 'recvonly' });
+ assert_equals(transceiver.sender.track, null);
+ assert_equals(transceiver.direction, 'recvonly');
+
+ const stream = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const [track] = stream.getTracks();
+ const sender = pc.addTrack(track);
+
+ assert_equals(sender.track, track);
+ assert_not_equals(sender, transceiver.sender);
+
+ const senders = pc.getSenders();
+ assert_equals(senders.length, 2,
+ 'Expect 2 senders added to connection');
+
+ assert_true(senders.includes(sender),
+ 'Expect senders list to include sender');
+
+ assert_true(senders.includes(transceiver.sender),
+ `Expect senders list to include first transceiver's sender`);
+ }, 'addTrack with existing sender with null track, different kind, and recvonly direction should create new sender');
+
+ /*
+ TODO
+ 5.1. addTrack
+ 3. Let streams be a list of MediaStream objects constructed from the
+ method's remaining arguments, or an empty list if the method was
+ called with a single argument.
+ 6. The steps below describe how to determine if an existing sender can
+ be reused. Doing so will cause future calls to createOffer and
+ createAnswer to mark the corresponding media description as sendrecv
+ or sendonly and add the MSID of the track added, as defined in [JSEP]
+ (section 5.2.2. and section 5.3.2.).
+
+ Non-Testable
+ 5.1. addTrack
+ 7. If sender is not null, run the following steps to use that sender:
+ 2. Set sender's [[associated MediaStreams]] to streams.
+
+ Tested in RTCPeerConnection-onnegotiationneeded.html:
+ 5.1. addTrack
+ 10. Update the negotiation-needed flag for connection.
+
+ */
+
+ promise_test(async t => {
+ const caller = new RTCPeerConnection();
+ t.add_cleanup(() => caller.close());
+ const callee = new RTCPeerConnection();
+ t.add_cleanup(() => callee.close());
+
+ const stream = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const [track] = stream.getTracks();
+ const transceiver = caller.addTransceiver(track);
+ // Note that this test doesn't process canididates.
+ {
+ const offer = await caller.createOffer();
+ await caller.setLocalDescription(offer);
+ await callee.setRemoteDescription(offer);
+ const answer = await callee.createAnswer();
+ await callee.setLocalDescription(answer);
+ await caller.setRemoteDescription(answer);
+ }
+ assert_equals(transceiver.currentDirection, 'sendonly');
+ await waitForIceGatheringState(caller, ['complete']);
+ await waitForIceGatheringState(callee, ['complete']);
+
+ const second_stream = await getNoiseStream({audio: true});
+ t.add_cleanup(() => second_stream.getTracks().forEach(track => track.stop()));
+ // There may be callee candidates in flight. It seems that waiting
+ // for a createOffer() is enough time to let them complete processing.
+ // TODO(https://crbug.com/webrtc/13095): Fix bug and remove.
+ await caller.createOffer();
+
+ const [second_track] = second_stream.getTracks();
+ caller.onicecandidate = t.unreached_func(
+ 'No caller candidates should be generated.');
+ callee.onicecandidate = t.unreached_func(
+ 'No callee candidates should be generated.');
+ caller.addTrack(second_track);
+ {
+ const offer = await caller.createOffer();
+ await caller.setLocalDescription(offer);
+ await callee.setRemoteDescription(offer);
+ const answer = await callee.createAnswer();
+ await callee.setLocalDescription(answer);
+ await caller.setRemoteDescription(answer);
+ }
+ // Check that we're bundled.
+ const [first_transceiver, second_transceiver] = caller.getTransceivers();
+ assert_equals(first_transceiver.transport, second_transceiver.transport);
+
+ }, 'Adding more tracks does not generate more candidates if bundled');
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const stream = await getNoiseStream({ audio: true });
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const [track] = stream.getTracks();
+
+ pc1.addTrack(track);
+ const offer = await pc1.createOffer();
+ // We do not await here; we want to ensure that the transceiver this creates
+ // is untouched by addTrack, and that addTrack creates _another_ transceiver
+ const srdPromise = pc2.setRemoteDescription(offer);
+
+ const sender = pc2.addTrack(track);
+
+ await srdPromise;
+
+ assert_equals(pc2.getTransceivers().length, 1, "Should have 1 transceiver");
+ assert_equals(pc2.getTransceivers()[0].sender, sender, "The transceiver should be the one added by addTrack");
+ }, 'Calling addTrack while sRD(offer) is pending should allow the new remote transceiver to be the same one that addTrack creates');
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+ pc1.addTransceiver('video');
+
+ const stream = await getNoiseStream({ audio: true });
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const [track] = stream.getTracks();
+
+ const offer = await pc1.createOffer();
+ const srdPromise = pc2.setRemoteDescription(offer);
+ assert_equals(pc2.getTransceivers().length, 0);
+ pc2.addTrack(track);
+ assert_equals(pc2.getTransceivers().length, 1);
+ const transceiver0 = pc2.getTransceivers()[0];
+ assert_equals(transceiver0.mid, null);
+ await srdPromise;
+ assert_equals(pc2.getTransceivers().length, 2);
+ const transceiver1 = pc2.getTransceivers()[1];
+ assert_equals(transceiver0.mid, null);
+ assert_not_equals(transceiver1.mid, null);
+ }, 'When addTrack is called while sRD is in progress, and both addTrack and sRD add a transceiver of different media types, the addTrack transceiver should come first, and then the sRD transceiver.');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-addTransceiver.https.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-addTransceiver.https.html
new file mode 100644
index 0000000000..3fd83a76fe
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-addTransceiver.https.html
@@ -0,0 +1,441 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerConnection.prototype.addTransceiver</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="RTCPeerConnection-helper.js"></script>
+<script>
+ 'use strict';
+
+ // Test is based on the following editor draft:
+ // https://rawgit.com/w3c/webrtc-pc/cc8d80f455b86c8041d63bceb8b457f45c72aa89/webrtc.html
+
+ /*
+ 5.1. RTCPeerConnection Interface Extensions
+
+ partial interface RTCPeerConnection {
+ sequence<RTCRtpSender> getSenders();
+ sequence<RTCRtpReceiver> getReceivers();
+ sequence<RTCRtpTransceiver> getTransceivers();
+ RTCRtpTransceiver addTransceiver((MediaStreamTrack or DOMString) trackOrKind,
+ optional RTCRtpTransceiverInit init);
+ ...
+ };
+
+ dictionary RTCRtpTransceiverInit {
+ RTCRtpTransceiverDirection direction = "sendrecv";
+ sequence<MediaStream> streams;
+ sequence<RTCRtpEncodingParameters> sendEncodings;
+ };
+
+ enum RTCRtpTransceiverDirection {
+ "sendrecv",
+ "sendonly",
+ "recvonly",
+ "inactive"
+ };
+
+ 5.2. RTCRtpSender Interface
+
+ interface RTCRtpSender {
+ readonly attribute MediaStreamTrack? track;
+ ...
+ };
+
+ 5.3. RTCRtpReceiver Interface
+
+ interface RTCRtpReceiver {
+ readonly attribute MediaStreamTrack track;
+ ...
+ };
+
+ 5.4. RTCRtpTransceiver Interface
+
+ interface RTCRtpTransceiver {
+ readonly attribute DOMString? mid;
+ [SameObject]
+ readonly attribute RTCRtpSender sender;
+ [SameObject]
+ readonly attribute RTCRtpReceiver receiver;
+ readonly attribute boolean stopped;
+ readonly attribute RTCRtpTransceiverDirection direction;
+ readonly attribute RTCRtpTransceiverDirection? currentDirection;
+ ...
+ };
+
+ Note
+ While addTrack checks if the MediaStreamTrack given as an argument is
+ already being sent to avoid sending the same MediaStreamTrack twice,
+ the other ways do not, allowing the same MediaStreamTrack to be sent
+ several times simultaneously.
+ */
+
+ /*
+ 5.1. addTransceiver
+ 3. If the first argument is a string, let it be kind and run the following steps:
+ 1. If kind is not a legal MediaStreamTrack kind, throw a TypeError.
+ */
+ test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ assert_idl_attribute(pc, 'addTransceiver');
+ assert_throws_js(TypeError, () => pc.addTransceiver('invalid'));
+ }, 'addTransceiver() with string argument as invalid kind should throw TypeError');
+
+ /*
+ 5.1. addTransceiver
+ The initial value of mid is null.
+
+ 3. If the dictionary argument is present, let direction be the value of the
+ direction member. Otherwise let direction be sendrecv.
+ 4. If the first argument is a string, let it be kind and run the following steps:
+ 2. Let track be null.
+ 8. Create an RTCRtpSender with track, streams and sendEncodings and let
+ sender be the result.
+ 9. Create an RTCRtpReceiver with kind and let receiver be the result.
+ 10. Create an RTCRtpTransceiver with sender, receiver and direction, and let
+ transceiver be the result.
+ 11. Add transceiver to connection's set of transceivers.
+
+ 5.2. RTCRtpSender Interface
+ Create an RTCRtpSender
+ 2. Set sender.track to track.
+
+ 5.3. RTCRtpReceiver Interface
+ Create an RTCRtpReceiver
+ 2. Let track be a new MediaStreamTrack object [GETUSERMEDIA]. The source of
+ track is a remote source provided by receiver.
+ 3. Initialize track.kind to kind.
+ 5. Initialize track.label to the result of concatenating the string "remote "
+ with kind.
+ 6. Initialize track.readyState to live.
+ 7. Initialize track.muted to true.
+ 8. Set receiver.track to track.
+
+ 5.4. RTCRtpTransceiver Interface
+ Create an RTCRtpTransceiver
+ 2. Set transceiver.sender to sender.
+ 3. Set transceiver.receiver to receiver.
+ 4. Let transceiver have a [[Direction]] internal slot, initialized to direction.
+ 5. Let transceiver have a [[CurrentDirection]] internal slot, initialized
+ to null.
+ 6. Set transceiver.stopped to false.
+ */
+ test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ assert_idl_attribute(pc, 'addTransceiver');
+
+ const transceiver = pc.addTransceiver('audio');
+ assert_true(transceiver instanceof RTCRtpTransceiver,
+ 'Expect transceiver to be instance of RTCRtpTransceiver');
+
+ assert_equals(transceiver.mid, null);
+ assert_equals(transceiver.stopped, false);
+ assert_equals(transceiver.direction, 'sendrecv');
+ assert_equals(transceiver.currentDirection, null);
+
+ assert_array_equals([transceiver], pc.getTransceivers(),
+ `Expect added transceiver to be the only element in connection's list of transceivers`);
+
+ const sender = transceiver.sender;
+
+ assert_true(sender instanceof RTCRtpSender,
+ 'Expect sender to be instance of RTCRtpSender');
+
+ assert_equals(sender.track, null);
+
+ assert_array_equals([sender], pc.getSenders(),
+ `Expect added sender to be the only element in connection's list of senders`);
+
+ const receiver = transceiver.receiver;
+ assert_true(receiver instanceof RTCRtpReceiver,
+ 'Expect receiver to be instance of RTCRtpReceiver');
+
+ const track = receiver.track;
+ assert_true(track instanceof MediaStreamTrack,
+ 'Expect receiver.track to be instance of MediaStreamTrack');
+
+ assert_equals(track.kind, 'audio');
+ assert_equals(track.readyState, 'live');
+ assert_equals(track.muted, true);
+
+ assert_array_equals([receiver], pc.getReceivers(),
+ `Expect added receiver to be the only element in connection's list of receivers`);
+
+ }, `addTransceiver('audio') should return an audio transceiver`);
+
+ test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ assert_idl_attribute(pc, 'addTransceiver');
+
+ const transceiver = pc.addTransceiver('video');
+ assert_true(transceiver instanceof RTCRtpTransceiver,
+ 'Expect transceiver to be instance of RTCRtpTransceiver');
+
+ assert_equals(transceiver.mid, null);
+ assert_equals(transceiver.stopped, false);
+ assert_equals(transceiver.direction, 'sendrecv');
+
+ assert_array_equals([transceiver], pc.getTransceivers(),
+ `Expect added transceiver to be the only element in connection's list of transceivers`);
+
+ const sender = transceiver.sender;
+
+ assert_true(sender instanceof RTCRtpSender,
+ 'Expect sender to be instance of RTCRtpSender');
+
+ assert_equals(sender.track, null);
+
+ assert_array_equals([sender], pc.getSenders(),
+ `Expect added sender to be the only element in connection's list of senders`);
+
+ const receiver = transceiver.receiver;
+ assert_true(receiver instanceof RTCRtpReceiver,
+ 'Expect receiver to be instance of RTCRtpReceiver');
+
+ const track = receiver.track;
+ assert_true(track instanceof MediaStreamTrack,
+ 'Expect receiver.track to be instance of MediaStreamTrack');
+
+ assert_equals(track.kind, 'video');
+ assert_equals(track.readyState, 'live');
+ assert_equals(track.muted, true);
+
+ assert_array_equals([receiver], pc.getReceivers(),
+ `Expect added receiver to be the only element in connection's list of receivers`);
+
+ }, `addTransceiver('video') should return a video transceiver`);
+
+ test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const transceiver = pc.addTransceiver('audio', { direction: 'sendonly' });
+ assert_equals(transceiver.direction, 'sendonly');
+ }, `addTransceiver() with direction sendonly should have result transceiver.direction be the same`);
+
+ test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const transceiver = pc.addTransceiver('audio', { direction: 'inactive' });
+ assert_equals(transceiver.direction, 'inactive');
+ }, `addTransceiver() with direction inactive should have result transceiver.direction be the same`);
+
+ test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ assert_idl_attribute(pc, 'addTransceiver');
+ assert_throws_js(TypeError, () =>
+ pc.addTransceiver('audio', { direction: 'invalid' }));
+ }, `addTransceiver() with invalid direction should throw TypeError`);
+
+ /*
+ 5.1. addTransceiver
+ 5. If the first argument is a MediaStreamTrack , let it be track and let
+ kind be track.kind.
+ */
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const stream = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const [track] = stream.getTracks();
+ const transceiver = pc.addTransceiver(track);
+ const { sender, receiver } = transceiver;
+
+ assert_true(sender instanceof RTCRtpSender,
+ 'Expect sender to be instance of RTCRtpSender');
+
+ assert_true(receiver instanceof RTCRtpReceiver,
+ 'Expect receiver to be instance of RTCRtpReceiver');
+
+ assert_equals(sender.track, track,
+ 'Expect sender.track should be the track that is added');
+
+ const receiverTrack = receiver.track;
+ assert_true(receiverTrack instanceof MediaStreamTrack,
+ 'Expect receiver.track to be instance of MediaStreamTrack');
+
+ assert_equals(receiverTrack.kind, 'audio',
+ `receiver.track should have the same kind as added track's kind`);
+
+ assert_equals(receiverTrack.readyState, 'live');
+ assert_equals(receiverTrack.muted, true);
+
+ assert_array_equals([transceiver], pc.getTransceivers(),
+ `Expect added transceiver to be the only element in connection's list of transceivers`);
+
+ assert_array_equals([sender], pc.getSenders(),
+ `Expect added sender to be the only element in connection's list of senders`);
+
+ assert_array_equals([receiver], pc.getReceivers(),
+ `Expect added receiver to be the only element in connection's list of receivers`);
+
+ }, 'addTransceiver(track) should have result with sender.track be given track');
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const stream = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const [track] = stream.getTracks();
+ const transceiver1 = pc.addTransceiver(track);
+ const transceiver2 = pc.addTransceiver(track);
+
+ assert_not_equals(transceiver1, transceiver2);
+
+ const sender1 = transceiver1.sender;
+ const sender2 = transceiver2.sender;
+
+ assert_not_equals(sender1, sender2);
+ assert_equals(transceiver1.sender.track, track);
+ assert_equals(transceiver2.sender.track, track);
+
+ const transceivers = pc.getTransceivers();
+ assert_equals(transceivers.length, 2);
+ assert_true(transceivers.includes(transceiver1));
+ assert_true(transceivers.includes(transceiver2));
+
+ const senders = pc.getSenders();
+ assert_equals(senders.length, 2);
+ assert_true(senders.includes(sender1));
+ assert_true(senders.includes(sender2));
+
+ }, 'addTransceiver(track) multiple times should create multiple transceivers');
+
+ /*
+ 5.1. addTransceiver
+ 6. Verify that each rid value in sendEncodings is composed only of
+ case-sensitive alphanumeric characters (a-z, A-Z, 0-9) up to a maximum
+ of 16 characters. If one of the RIDs does not meet these requirements,
+ throw a TypeError.
+ */
+ test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ assert_idl_attribute(pc, 'addTransceiver');
+
+ assert_throws_js(TypeError, () =>
+ pc.addTransceiver('video', {
+ sendEncodings: [{
+ rid: '@Invalid!'
+ }]
+ }));
+ }, 'addTransceiver() with rid containing invalid non-alphanumeric characters should throw TypeError');
+
+ test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ assert_idl_attribute(pc, 'addTransceiver');
+
+ assert_throws_js(TypeError, () =>
+ pc.addTransceiver('audio', {
+ sendEncodings: [{
+ rid: 'a'.repeat(17)
+ }]
+ }));
+ }, 'addTransceiver() with rid longer than 16 characters should throw TypeError');
+
+ test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ pc.addTransceiver('audio', {
+ sendEncodings: [{
+ rid: 'foo'
+ }]
+ });
+ }, `addTransceiver() with valid rid value should succeed`);
+
+ test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ pc.addTransceiver('video', {
+ sendEncodings: [{
+ dtx: 'enabled',
+ active: false,
+ ptime: 5,
+ maxBitrate: 8,
+ maxFramerate: 25,
+ rid: 'foo'
+ }]
+ });
+ }, `addTransceiver() with valid sendEncodings should succeed`);
+
+ /*
+ TODO
+ 5.1. addTransceiver
+ - Adding a transceiver will cause future calls to createOffer to add a media
+ description for the corresponding transceiver, as defined in [JSEP]
+ (section 5.2.2.).
+
+ - Setting a new RTCSessionDescription may change mid to a non-null value,
+ as defined in [JSEP] (section 5.5. and section 5.6.).
+
+ 1. If the dictionary argument is present, and it has a streams member, let
+ streams be that list of MediaStream objects.
+
+ 5.2. RTCRtpSender Interface
+ Create an RTCRtpSender
+ 3. Let sender have an [[associated MediaStreams]] internal slot, representing
+ a list of MediaStream objects that the MediaStreamTrack object of this
+ sender is associated with.
+
+ 4. Set sender's [[associated MediaStreams]] slot to streams.
+
+ 5. Let sender have a [[send encodings]] internal slot, representing a list
+ of RTCRtpEncodingParameters dictionaries.
+
+ 6. If sendEncodings is given as input to this algorithm, and is non-empty,
+ set the [[send encodings]] slot to sendEncodings. Otherwise, set it to a
+ list containing a single RTCRtpEncodingParameters with active set to true.
+
+ 5.3. RTCRtpReceiver Interface
+ Create an RTCRtpReceiver
+ 4. If an id string, id, was given as input to this algorithm, initialize
+ track.id to id. (Otherwise the value generated when track was created
+ will be used.)
+
+ Tested in RTCPeerConnection-onnegotiationneeded.html
+ 5.1. addTransceiver
+ 12. Update the negotiation-needed flag for connection.
+
+ Out of Scope
+ 5.1. addTransceiver
+ 8. If sendEncodings is set, then subsequent calls to createOffer will be
+ configured to send multiple RTP encodings as defined in [JSEP]
+ (section 5.2.2. and section 5.2.1.).
+
+ When setRemoteDescription is called with a corresponding remote
+ description that is able to receive multiple RTP encodings as defined
+ in [JSEP] (section 3.7.), the RTCRtpSender may send multiple RTP
+ encodings and the parameters retrieved via the transceiver's
+ sender.getParameters() will reflect the encodings negotiated.
+
+ 9. This specification does not define how to configure createOffer to
+ receive multiple RTP encodings. However when setRemoteDescription is
+ called with a corresponding remote description that is able to send
+ multiple RTP encodings as defined in [JSEP], the RTCRtpReceiver may
+ receive multiple RTP encodings and the parameters retrieved via the
+ transceiver's receiver.getParameters() will reflect the encodings
+ negotiated.
+
+ Coverage Report
+ Tested Not-Tested Non-Testable Total
+ addTransceiver 14 1 3 18
+ Create Sender 3 4 0 7
+ Create Receiver 8 1 0 9
+ Create Transceiver 7 0 0 7
+
+ Total 32 6 3 41
+ */
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-canTrickleIceCandidates.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-canTrickleIceCandidates.html
new file mode 100644
index 0000000000..09ad67751a
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-canTrickleIceCandidates.html
@@ -0,0 +1,62 @@
+<!doctype html>
+<html>
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+ <title>RTCPeerConnection canTrickleIceCandidates tests</title>
+</head>
+<body>
+ <!-- These files are in place when executing on W3C. -->
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script type="text/javascript">
+ // tests support for RTCPeerConnection.canTrickleIceCandidates:
+ // http://w3c.github.io/webrtc-pc/#dom-rtcpeerconnection-cantrickleicecandidates
+ const sdp = 'v=0\r\n' +
+ 'o=- 166855176514521964 2 IN IP4 127.0.0.1\r\n' +
+ 's=-\r\n' +
+ 't=0 0\r\n' +
+ 'a=ice-options:trickle\r\n' +
+ 'm=audio 9 UDP/TLS/RTP/SAVPF 111\r\n' +
+ 'c=IN IP4 0.0.0.0\r\n' +
+ 'a=rtcp:9 IN IP4 0.0.0.0\r\n' +
+ 'a=ice-ufrag:someufrag\r\n' +
+ 'a=ice-pwd:somelongpwdwithenoughrandomness\r\n' +
+ 'a=fingerprint:sha-256 8C:71:B3:8D:A5:38:FD:8F:A4:2E:A2:65:6C:86:52:BC:E0:6E:94:F2:9F:7C:4D:B5:DF:AF:AA:6F:44:90:8D:F4\r\n' +
+ 'a=setup:actpass\r\n' +
+ 'a=rtcp-mux\r\n' +
+ 'a=mid:mid1\r\n' +
+ 'a=sendonly\r\n' +
+ 'a=msid:stream1 track1\r\n' +
+ 'a=ssrc:1001 cname:some\r\n' +
+ 'a=rtpmap:111 opus/48000/2\r\n';
+
+ test(function() {
+ var pc = new RTCPeerConnection();
+ assert_equals(pc.canTrickleIceCandidates, null, 'canTrickleIceCandidates property is null');
+ }, 'canTrickleIceCandidates property is null prior to setRemoteDescription');
+
+ promise_test(function(t) {
+ var pc = new RTCPeerConnection();
+
+ t.add_cleanup(() => pc.close());
+
+ return pc.setRemoteDescription(new RTCSessionDescription({type: 'offer', sdp: sdp}))
+ .then(function() {
+ assert_true(pc.canTrickleIceCandidates, 'canTrickleIceCandidates property is true after setRemoteDescription');
+ })
+ }, 'canTrickleIceCandidates property is true after setRemoteDescription with a=ice-options:trickle');
+
+ promise_test(function(t) {
+ var pc = new RTCPeerConnection();
+
+ t.add_cleanup(() => pc.close());
+
+ return pc.setRemoteDescription(new RTCSessionDescription({type: 'offer', sdp: sdp.replace('a=ice-options:trickle\r\n', '')}))
+ .then(function() {
+ assert_false(pc.canTrickleIceCandidates, 'canTrickleIceCandidates property is false after setRemoteDescription');
+ })
+ }, 'canTrickleIceCandidates property is false after setRemoteDescription without a=ice-options:trickle');
+</script>
+
+</body>
+</html>
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-candidate-in-sdp.https.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-candidate-in-sdp.https.html
new file mode 100644
index 0000000000..6c97afe94a
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-candidate-in-sdp.https.html
@@ -0,0 +1,26 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+'use strict';
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ let resolveIceCandidatePromise = null;
+ const iceCandidatePromise = new Promise(r => resolveIceCandidatePromise = r);
+ pc.onicecandidate = e => {
+ resolveIceCandidatePromise(pc.localDescription.sdp);
+ pc.onicecandidate = null;
+ }
+ pc.addTransceiver("audio");
+ await pc.setLocalDescription(await pc.createOffer());
+ assert_false(pc.localDescription.sdp.includes("a=candidate:"),
+ "localDescription is missing candidate before onicecandidate");
+ // The localDescription at the time of the onicecandidate event.
+ const localDescriptionSdp = await iceCandidatePromise;
+ assert_true(localDescriptionSdp.includes("a=candidate:"),
+ "localDescription contains candidate after onicecandidate");
+}, 'localDescription contains candidates');
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-capture-video.https.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-capture-video.https.html
new file mode 100644
index 0000000000..b6c0222dc2
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-capture-video.https.html
@@ -0,0 +1,72 @@
+<!doctype html>
+<html>
+<head>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src="RTCPeerConnection-helper.js"></script>
+</head>
+<body>
+<script>
+ 'use strict';
+
+// This test checks that <video> capture works via PeerConnection.
+
+promise_test(async t => {
+ const sourceVideo = document.createElement('video');
+ sourceVideo.src = "/media/test-v-128k-320x240-24fps-8kfr.webm";
+ sourceVideo.loop = true;
+
+ const onCanPlay = new Promise(r => sourceVideo.oncanplay = r);
+ await onCanPlay;
+
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ // Attach video to pc1.
+ const stream = sourceVideo.captureStream();
+ const tracks = stream.getTracks();
+ pc1.addTrack(tracks[0]);
+
+ const destVideo = document.createElement('video');
+ destVideo.autoplay = true;
+
+ // Setup pc1->pc2.
+ const haveTrackEvent1 = new Promise(r => pc2.ontrack = r);
+ exchangeIceCandidates(pc1, pc2);
+ await pc1.setLocalDescription();
+ await pc2.setRemoteDescription(pc1.localDescription);
+ await pc2.setLocalDescription();
+ await pc1.setRemoteDescription(pc2.localDescription);
+
+ // Display pc2 received track in video element.
+ const onLoadedMetadata = new Promise(r => destVideo.onloadedmetadata = r);
+ destVideo.srcObject = new MediaStream([(await haveTrackEvent1).track]);
+
+ // Start playback and wait for video on the other side.
+ sourceVideo.play();
+ await onLoadedMetadata;
+
+ // Wait until the video has non-zero resolution and some non-black pixels.
+ await new Promise(p => {
+ function checkColor() {
+ if (destVideo.videoWidth > 0 && getVideoSignal(destVideo) > 0.0)
+ p();
+ else
+ t.step_timeout(checkColor, 0);
+ }
+ checkColor();
+ });
+
+ // Uses Helper.js GetVideoSignal to query |destVideo| pixel value at a certain position.
+ const pixelValue = getVideoSignal(destVideo);
+
+ // Anything non-black means that capture works.
+ assert_not_equals(pixelValue, 0);
+ }, "Capturing a video element and sending it via PeerConnection");
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-connectionState.https.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-connectionState.https.html
new file mode 100644
index 0000000000..d7716a1d4d
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-connectionState.https.html
@@ -0,0 +1,291 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerConnection.prototype.connectionState</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="RTCPeerConnection-helper.js"></script>
+<script>
+ 'use strict';
+ // Test is based on the following editor draft:
+ // https://w3c.github.io/webrtc-pc/archives/20170605/webrtc.htm
+
+ // The following helper functions are called from RTCPeerConnection-helper.js:
+ // exchangeIceCandidates
+ // exchangeOfferAnswer
+
+ /*
+ 4.3.2. Interface Definition
+ interface RTCPeerConnection : EventTarget {
+ ...
+ readonly attribute RTCPeerConnectionState connectionState;
+ attribute EventHandler onconnectionstatechange;
+ };
+
+ 4.4.3. RTCPeerConnectionState Enum
+ enum RTCPeerConnectionState {
+ "new",
+ "connecting",
+ "connected",
+ "disconnected",
+ "failed",
+ "closed"
+ };
+
+ 5.5. RTCDtlsTransport Interface
+ interface RTCDtlsTransport {
+ readonly attribute RTCIceTransport iceTransport;
+ readonly attribute RTCDtlsTransportState state;
+ ...
+ };
+
+ enum RTCDtlsTransportState {
+ "new",
+ "connecting",
+ "connected",
+ "closed",
+ "failed"
+ };
+
+ 5.6. RTCIceTransport Interface
+ interface RTCIceTransport {
+ readonly attribute RTCIceTransportState state;
+ ...
+ };
+
+ enum RTCIceTransportState {
+ "new",
+ "checking",
+ "connected",
+ "completed",
+ "failed",
+ "disconnected",
+ "closed"
+ };
+ */
+
+ /*
+ 4.4.3. RTCPeerConnectionState Enum
+ new
+ Any of the RTCIceTransports or RTCDtlsTransports are in the new
+ state and none of the transports are in the connecting, checking,
+ failed or disconnected state, or all transports are in the closed state.
+ */
+ test(t => {
+ const pc = new RTCPeerConnection();
+ assert_equals(pc.connectionState, 'new');
+ }, 'Initial connectionState should be new');
+
+ test(t => {
+ const pc = new RTCPeerConnection();
+ pc.close();
+ assert_equals(pc.connectionState, 'closed');
+ }, 'Closing the connection should set connectionState to closed');
+
+ /*
+ 4.4.3. RTCPeerConnectionState Enum
+ connected
+ All RTCIceTransports and RTCDtlsTransports are in the connected,
+ completed or closed state and at least of them is in the connected
+ or completed state.
+
+ 5.5. RTCDtlsTransportState
+ connected
+ DTLS has completed negotiation of a secure connection.
+
+ 5.6. RTCIceTransportState
+ connected
+ The RTCIceTransport has found a usable connection, but is still
+ checking other candidate pairs to see if there is a better connection.
+ It may also still be gathering and/or waiting for additional remote
+ candidates. If consent checks [RFC7675] fail on the connection in use,
+ and there are no other successful candidate pairs available, then the
+ state transitions to "checking" (if there are candidate pairs remaining
+ to be checked) or "disconnected" (if there are no candidate pairs to
+ check, but the peer is still gathering and/or waiting for additional
+ remote candidates).
+
+ completed
+ The RTCIceTransport has finished gathering, received an indication that
+ there are no more remote candidates, finished checking all candidate
+ pairs and found a connection. If consent checks [RFC7675] subsequently
+ fail on all successful candidate pairs, the state transitions to "failed".
+ */
+
+ async_test(t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ let had_connecting = false;
+
+ const onConnectionStateChange = t.step_func(() => {
+ const {connectionState} = pc1;
+ if (connectionState === 'connecting') {
+ had_connecting = true;
+ } else if (connectionState === 'connected') {
+ assert_true(had_connecting, "state should pass connecting before reaching connected");
+ t.done();
+ }
+ });
+
+ pc1.createDataChannel('test');
+
+ pc1.addEventListener('connectionstatechange', onConnectionStateChange);
+
+ exchangeIceCandidates(pc1, pc2);
+ exchangeOfferAnswer(pc1, pc2);
+ }, 'connection with one data channel should eventually have connected connection state');
+
+ async_test(t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const onConnectionStateChange = t.step_func(() => {
+ const {connectionState} = pc1;
+ if (connectionState === 'connected') {
+ const sctpTransport = pc1.sctp;
+
+ const dtlsTransport = sctpTransport.transport;
+ assert_equals(dtlsTransport.state, 'connected',
+ 'Expect DTLS transport to be in connected state');
+
+ const iceTransport = dtlsTransport.iceTransport
+ assert_true(iceTransport.state === 'connected' ||
+ iceTransport.state === 'completed',
+ 'Expect ICE transport to be in connected or completed state');
+
+ t.done();
+ }
+ });
+
+ pc1.createDataChannel('test');
+
+ pc1.addEventListener('connectionstatechange', onConnectionStateChange);
+
+ exchangeIceCandidates(pc1, pc2);
+ exchangeOfferAnswer(pc1, pc2);
+ }, 'connection with one data channel should eventually have transports in connected state');
+
+ /*
+ TODO
+ 4.4.3. RTCPeerConnectionState Enum
+ connecting
+ Any of the RTCIceTransports or RTCDtlsTransports are in the
+ connecting or checking state and none of them is in the failed state.
+
+ disconnected
+ Any of the RTCIceTransports or RTCDtlsTransports are in the disconnected
+ state and none of them are in the failed or connecting or checking state.
+
+ failed
+ Any of the RTCIceTransports or RTCDtlsTransports are in a failed state.
+
+ closed
+ The RTCPeerConnection object's [[isClosed]] slot is true.
+
+ 5.5. RTCDtlsTransportState
+ new
+ DTLS has not started negotiating yet.
+
+ connecting
+ DTLS is in the process of negotiating a secure connection.
+
+ closed
+ The transport has been closed.
+
+ failed
+ The transport has failed as the result of an error (such as a failure
+ to validate the remote fingerprint).
+
+ 5.6. RTCIceTransportState
+ new
+ The RTCIceTransport is gathering candidates and/or waiting for
+ remote candidates to be supplied, and has not yet started checking.
+
+ checking
+ The RTCIceTransport has received at least one remote candidate and
+ is checking candidate pairs and has either not yet found a connection
+ or consent checks [RFC7675] have failed on all previously successful
+ candidate pairs. In addition to checking, it may also still be gathering.
+
+ failed
+ The RTCIceTransport has finished gathering, received an indication that
+ there are no more remote candidates, finished checking all candidate pairs,
+ and all pairs have either failed connectivity checks or have lost consent.
+
+ disconnected
+ The ICE Agent has determined that connectivity is currently lost for this
+ RTCIceTransport . This is more aggressive than failed, and may trigger
+ intermittently (and resolve itself without action) on a flaky network.
+ The way this state is determined is implementation dependent.
+
+ Examples include:
+ Losing the network interface for the connection in use.
+ Repeatedly failing to receive a response to STUN requests.
+
+ Alternatively, the RTCIceTransport has finished checking all existing
+ candidates pairs and failed to find a connection (or consent checks
+ [RFC7675] once successful, have now failed), but it is still gathering
+ and/or waiting for additional remote candidates.
+
+ closed
+ The RTCIceTransport has shut down and is no longer responding to STUN requests.
+ */
+ promise_test(async t => {
+ const caller = new RTCPeerConnection();
+ t.add_cleanup(() => caller.close());
+ const callee = new RTCPeerConnection();
+ t.add_cleanup(() => callee.close());
+ const stream = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const [track] = stream.getTracks();
+ caller.addTrack(track, stream);
+
+ await exchangeOfferAnswer(caller, callee);
+
+ assert_equals(caller.iceConnectionState, 'new');
+ assert_equals(callee.iceConnectionState, 'new');
+ }, 'connectionState remains new when not adding remote ice candidates');
+
+ promise_test(async t => {
+
+ const caller = new RTCPeerConnection();
+ t.add_cleanup(() => caller.close());
+ const callee = new RTCPeerConnection();
+ t.add_cleanup(() => callee.close());
+ const stream = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const [track] = stream.getTracks();
+ caller.addTrack(track, stream);
+
+ const states = [];
+ caller.addEventListener('connectionstatechange', () => states.push(caller.connectionState));
+ exchangeIceCandidates(caller, callee);
+ await exchangeOfferAnswer(caller, callee);
+
+ await listenToConnected(caller);
+
+ assert_array_equals(states, ['connecting', 'connected']);
+ }, 'connectionState transitions to connected via connecting');
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ const stream = await getNoiseStream({ audio: true });
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+
+ stream.getTracks().forEach(track => pc1.addTrack(track, stream));
+ exchangeIceCandidates(pc1, pc2);
+ exchangeOfferAnswer(pc1, pc2);
+ await listenToIceConnected(pc2);
+
+ pc2.onconnectionstatechange = t.unreached_func();
+ pc2.close();
+ assert_equals(pc2.connectionState, 'closed');
+ await new Promise(r => t.step_timeout(r, 100));
+ }, 'Closing a PeerConnection should not fire connectionstatechange event');
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-constructor.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-constructor.html
new file mode 100644
index 0000000000..1708b2705f
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-constructor.html
@@ -0,0 +1,76 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerConnection constructor</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script>
+test(function() {
+ assert_equals(RTCPeerConnection.length, 0);
+}, 'RTCPeerConnection.length');
+
+// These are used for string and number dictionary members to see if they are
+// being accessed at all.
+const toStringThrows = { toString: function() { throw new Error; } };
+const toNumberThrows = Symbol();
+
+// Test the first argument of the constructor. The key is the argument itself,
+// and the value is the first argument for assert_throws_js, or false if no
+// exception should be thrown.
+const testArgs = {
+ // No argument or equivalent.
+ '': false,
+ 'null': false,
+ 'undefined': false,
+ '{}': false,
+
+ // certificates
+ '{ certificates: null }': TypeError,
+ '{ certificates: undefined }': false,
+ '{ certificates: [] }': false,
+ '{ certificates: [null] }': TypeError,
+ '{ certificates: [undefined] }': TypeError,
+
+ // iceCandidatePoolSize
+ '{ iceCandidatePoolSize: toNumberThrows }': TypeError,
+}
+
+for (const arg in testArgs) {
+ const expr = 'new RTCPeerConnection(' + arg + ')';
+ test(function() {
+ const throws = testArgs[arg];
+ if (throws) {
+ assert_throws_js(throws, function() {
+ eval(expr);
+ });
+ } else {
+ eval(expr);
+ }
+ }, expr);
+}
+
+// The initial values of attributes of RTCPeerConnection.
+const initialState = {
+ 'localDescription': null,
+ 'currentLocalDescription': null,
+ 'pendingLocalDescription': null,
+ 'remoteDescription': null,
+ 'currentRemoteDescription': null,
+ 'pendingRemoteDescription': null,
+ 'signalingState': 'stable',
+ 'iceGatheringState': 'new',
+ 'iceConnectionState': 'new',
+ 'connectionState': 'new',
+ 'canTrickleIceCandidates': null,
+ // TODO: defaultIceServers
+};
+
+for (const attr in initialState) {
+ test(function() {
+ // Use one RTCPeerConnection instance for all initial value tests.
+ if (!window.pc) {
+ window.pc = new RTCPeerConnection;
+ }
+ assert_equals(window.pc[attr], initialState[attr]);
+ }, attr + ' initial value');
+}
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-createAnswer.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-createAnswer.html
new file mode 100644
index 0000000000..1970db0737
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-createAnswer.html
@@ -0,0 +1,41 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerConnection.prototype.createAnswer</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="RTCPeerConnection-helper.js"></script>
+<script>
+'use strict';
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ await promise_rejects_dom(t, 'InvalidStateError', pc.createAnswer());
+}, 'createAnswer() with null remoteDescription should reject with InvalidStateError');
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const offer = await generateVideoReceiveOnlyOffer(pc);
+ await pc.setRemoteDescription(offer);
+ const answer = await pc.createAnswer();
+ assert_equals(typeof answer, 'object',
+ 'Expect answer to be plain object dictionary RTCSessionDescriptionInit');
+ assert_false(answer instanceof RTCSessionDescription,
+ 'Expect answer to not be instance of RTCSessionDescription');
+}, 'createAnswer() after setting remote description should succeed');
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ // generateDataChannelOffer() is defined in RTCPeerConnection-helper.js.
+ const offer = await generateDataChannelOffer(pc);
+ await pc.setRemoteDescription(offer);
+ pc.close();
+ await promise_rejects_dom(t, 'InvalidStateError', pc.createAnswer());
+}, 'createAnswer() when connection is closed should reject with InvalidStateError');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-createDataChannel.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-createDataChannel.html
new file mode 100644
index 0000000000..7ad8bf7d46
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-createDataChannel.html
@@ -0,0 +1,758 @@
+<!doctype html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<title>RTCPeerConnection.prototype.createDataChannel</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src="RTCPeerConnection-helper.js"></script>
+<script>
+'use strict';
+
+const stopTracks = (...streams) => {
+ streams.forEach(stream => stream.getTracks().forEach(track => track.stop()));
+};
+
+// Test is based on the following revision:
+// https://rawgit.com/w3c/webrtc-pc/1cc5bfc3ff18741033d804c4a71f7891242fb5b3/webrtc.html
+
+/*
+ 6.1. RTCPeerConnection Interface Extensions
+
+ partial interface RTCPeerConnection {
+ [...]
+ RTCDataChannel createDataChannel(USVString label,
+ optional RTCDataChannelInit dataChannelDict);
+ [...]
+ };
+
+ 6.2. RTCDataChannel
+
+ interface RTCDataChannel : EventTarget {
+ readonly attribute USVString label;
+ readonly attribute boolean ordered;
+ readonly attribute unsigned short? maxPacketLifeTime;
+ readonly attribute unsigned short? maxRetransmits;
+ readonly attribute USVString protocol;
+ readonly attribute boolean negotiated;
+ readonly attribute unsigned short? id;
+ readonly attribute RTCDataChannelState readyState;
+ readonly attribute unsigned long bufferedAmount;
+ attribute unsigned long bufferedAmountLowThreshold;
+ [...]
+ attribute DOMString binaryType;
+ [...]
+ };
+
+ dictionary RTCDataChannelInit {
+ boolean ordered = true;
+ unsigned short maxPacketLifeTime;
+ unsigned short maxRetransmits;
+ USVString protocol = "";
+ boolean negotiated = false;
+ [EnforceRange]
+ unsigned short id;
+ };
+ */
+
+test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ assert_equals(pc.createDataChannel.length, 1);
+ assert_throws_js(TypeError, () => pc.createDataChannel());
+}, 'createDataChannel with no argument should throw TypeError');
+
+/*
+ 6.2. createDataChannel
+ 2. If connection's [[isClosed]] slot is true, throw an InvalidStateError.
+ */
+test(t => {
+ const pc = new RTCPeerConnection();
+ pc.close();
+ assert_equals(pc.signalingState, 'closed', 'signaling state');
+ assert_throws_dom('InvalidStateError', () => pc.createDataChannel(''));
+}, 'createDataChannel with closed connection should throw InvalidStateError');
+
+/*
+ 6.1. createDataChannel
+ 4. Let channel have a [[DataChannelLabel]] internal slot initialized to the value of the
+ first argument.
+ 6. Let options be the second argument.
+ 7. Let channel have an [[MaxPacketLifeTime]] internal slot initialized to
+ option's maxPacketLifeTime member, if present, otherwise null.
+ 8. Let channel have a [[ReadyState]] internal slot initialized to "connecting".
+ 9. Let channel have a [[BufferedAmount]] internal slot initialized to 0.
+ 10. Let channel have an [[MaxRetransmits]] internal slot initialized to
+ option's maxRetransmits member, if present, otherwise null.
+ 11. Let channel have an [[Ordered]] internal slot initialized to option's
+ ordered member.
+ 12. Let channel have a [[DataChannelProtocol]] internal slot initialized to option's
+ protocol member.
+ 14. Let channel have a [[Negotiated]] internal slot initialized to option's negotiated
+ member.
+ 15. Let channel have an [[DataChannelId]] internal slot initialized to option's id
+ member, if it is present and [[Negotiated]] is true, otherwise null.
+ 21. If the [[DataChannelId]] slot is null (due to no ID being passed into
+ createDataChannel, or [[Negotiated]] being false), and the DTLS role of the SCTP
+ transport has already been negotiated, then initialize [[DataChannelId]] to a value
+ generated by the user agent, according to [RTCWEB-DATA-PROTOCOL], and skip
+ to the next step. If no available ID could be generated, or if the value of the
+ [[DataChannelId]] slot is being used by an existing RTCDataChannel, throw an
+ OperationError exception.
+
+ Note
+ If the [[DataChannelId]] slot is null after this step, it will be populated once
+ the DTLS role is determined during the process of setting an RTCSessionDescription.
+ 22. If channel is the first RTCDataChannel created on connection, update the
+ negotiation-needed flag for connection.
+
+
+ 6.2. RTCDataChannel
+
+ A RTCDataChannel, created with createDataChannel or dispatched via a
+ RTCDataChannelEvent, MUST initially be in the connecting state
+
+ bufferedAmountLowThreshold
+ [...] The bufferedAmountLowThreshold is initially zero on each new RTCDataChannel,
+ but the application may change its value at any time.
+
+ binaryType
+ [...] When a RTCDataChannel object is created, the binaryType attribute MUST
+ be initialized to the string "blob".
+ */
+test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const dc = pc.createDataChannel('');
+
+ assert_true(dc instanceof RTCDataChannel, 'is RTCDataChannel');
+ assert_equals(dc.label, '');
+ assert_equals(dc.ordered, true);
+ assert_equals(dc.maxPacketLifeTime, null);
+ assert_equals(dc.maxRetransmits, null);
+ assert_equals(dc.protocol, '');
+ assert_equals(dc.negotiated, false);
+ // Since no offer/answer exchange has occurred yet, the DTLS role is unknown
+ // and so the ID should be null.
+ assert_equals(dc.id, null);
+ assert_equals(dc.readyState, 'connecting');
+ assert_equals(dc.bufferedAmount, 0);
+ assert_equals(dc.bufferedAmountLowThreshold, 0);
+ assert_equals(dc.binaryType, 'blob');
+}, 'createDataChannel attribute default values');
+
+test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const dc = pc.createDataChannel('test', {
+ ordered: false,
+ maxRetransmits: 1,
+ // Note: maxPacketLifeTime is not set in this test.
+ protocol: 'custom',
+ negotiated: true,
+ id: 3
+ });
+
+ assert_true(dc instanceof RTCDataChannel, 'is RTCDataChannel');
+ assert_equals(dc.label, 'test');
+ assert_equals(dc.ordered, false);
+ assert_equals(dc.maxPacketLifeTime, null);
+ assert_equals(dc.maxRetransmits, 1);
+ assert_equals(dc.protocol, 'custom');
+ assert_equals(dc.negotiated, true);
+ assert_equals(dc.id, 3);
+ assert_equals(dc.readyState, 'connecting');
+ assert_equals(dc.bufferedAmount, 0);
+ assert_equals(dc.bufferedAmountLowThreshold, 0);
+ assert_equals(dc.binaryType, 'blob');
+
+ const dc2 = pc.createDataChannel('test2', {
+ ordered: false,
+ maxPacketLifeTime: 42
+ });
+ assert_equals(dc2.label, 'test2');
+ assert_equals(dc2.maxPacketLifeTime, 42);
+ assert_equals(dc2.maxRetransmits, null);
+}, 'createDataChannel with provided parameters should initialize attributes to provided values');
+
+/*
+ 6.2. createDataChannel
+ 4. Let channel have a [[DataChannelLabel]] internal slot initialized to the value of the
+ first argument.
+
+ [ECMA262] 7.1.12. ToString(argument)
+ undefined -> "undefined"
+ null -> "null"
+
+ [WebIDL] 3.10.15. Convert a DOMString to a sequence of Unicode scalar values
+ */
+const labels = [
+ ['"foo"', 'foo', 'foo'],
+ ['null', null, 'null'],
+ ['undefined', undefined, 'undefined'],
+ ['lone surrogate', '\uD800', '\uFFFD'],
+];
+for (const [description, label, expected] of labels) {
+ test(t => {
+ const pc = new RTCPeerConnection;
+ t.add_cleanup(() => pc.close());
+
+ const dc = pc.createDataChannel(label);
+ assert_equals(dc.label, expected);
+ }, `createDataChannel with label ${description} should succeed`);
+}
+
+/*
+ 6.2. RTCDataChannel
+ createDataChannel
+ 11. Let channel have an [[Ordered]] internal slot initialized to option's
+ ordered member.
+ */
+test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const dc = pc.createDataChannel('', { ordered: false });
+ assert_equals(dc.ordered, false);
+}, 'createDataChannel with ordered false should succeed');
+
+// true as the default value of a boolean is confusing because null is converted
+// to false while undefined is converted to true.
+test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const dc1 = pc.createDataChannel('', { ordered: null });
+ assert_equals(dc1.ordered, false);
+ const dc2 = pc.createDataChannel('', { ordered: undefined });
+ assert_equals(dc2.ordered, true);
+}, 'createDataChannel with ordered null/undefined should succeed');
+
+/*
+ 6.2. RTCDataChannel
+ createDataChannel
+ 7. Let channel have an [[MaxPacketLifeTime]] internal slot initialized to
+ option's maxPacketLifeTime member, if present, otherwise null.
+ */
+test(t => {
+ const pc = new RTCPeerConnection;
+ t.add_cleanup(() => pc.close());
+
+ const dc = pc.createDataChannel('', { maxPacketLifeTime: 0 });
+ assert_equals(dc.maxPacketLifeTime, 0);
+}, 'createDataChannel with maxPacketLifeTime 0 should succeed');
+
+/*
+ 6.2. RTCDataChannel
+ createDataChannel
+ 10. Let channel have an [[MaxRetransmits]] internal slot initialized to
+ option's maxRetransmits member, if present, otherwise null.
+ */
+test(t => {
+ const pc = new RTCPeerConnection;
+ t.add_cleanup(() => pc.close());
+
+ const dc = pc.createDataChannel('', { maxRetransmits: 0 });
+ assert_equals(dc.maxRetransmits, 0);
+}, 'createDataChannel with maxRetransmits 0 should succeed');
+
+/*
+ 6.2. createDataChannel
+ 18. If both [[MaxPacketLifeTime]] and [[MaxRetransmits]] attributes are set (not null),
+ throw a TypeError.
+ */
+test(t => {
+ const pc = new RTCPeerConnection;
+ t.add_cleanup(() => pc.close());
+
+ pc.createDataChannel('', {
+ maxPacketLifeTime: undefined,
+ maxRetransmits: undefined
+ });
+}, 'createDataChannel with both maxPacketLifeTime and maxRetransmits undefined should succeed');
+
+test(t => {
+ const pc = new RTCPeerConnection;
+ t.add_cleanup(() => pc.close());
+
+ assert_throws_js(TypeError, () => pc.createDataChannel('', {
+ maxPacketLifeTime: 0,
+ maxRetransmits: 0
+ }));
+ assert_throws_js(TypeError, () => pc.createDataChannel('', {
+ maxPacketLifeTime: 42,
+ maxRetransmits: 42
+ }));
+}, 'createDataChannel with both maxPacketLifeTime and maxRetransmits should throw TypeError');
+
+/*
+ 6.2. RTCDataChannel
+ createDataChannel
+ 12. Let channel have a [[DataChannelProtocol]] internal slot initialized to option's
+ protocol member.
+ */
+const protocols = [
+ ['"foo"', 'foo', 'foo'],
+ ['null', null, 'null'],
+ ['undefined', undefined, ''],
+ ['lone surrogate', '\uD800', '\uFFFD'],
+];
+for (const [description, protocol, expected] of protocols) {
+ test(t => {
+ const pc = new RTCPeerConnection;
+ t.add_cleanup(() => pc.close());
+
+ const dc = pc.createDataChannel('', { protocol });
+ assert_equals(dc.protocol, expected);
+ }, `createDataChannel with protocol ${description} should succeed`);
+}
+
+/*
+ 6.2. RTCDataChannel
+ createDataChannel
+ 20. If [[DataChannelId]] is equal to 65535, which is greater than the maximum allowed
+ ID of 65534 but still qualifies as an unsigned short, throw a TypeError.
+ */
+for (const id of [0, 1, 65534, 65535]) {
+ test((t) => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const dc = pc.createDataChannel('', { id });
+ assert_equals(dc.id, null);
+ }, `createDataChannel with id ${id} and negotiated not set should succeed, but not set the channel's id`);
+}
+
+for (const id of [0, 1, 65534]) {
+ test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const dc = pc.createDataChannel('', { 'negotiated': true, 'id': id });
+ assert_equals(dc.id, id);
+ }, `createDataChannel with id ${id} and negotiated true should succeed, and set the channel's id`);
+}
+
+for (const id of [-1, 65536]) {
+ test((t) => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ assert_throws_js(TypeError, () => pc.createDataChannel('', { id }));
+ }, `createDataChannel with id ${id} and negotiated not set should throw TypeError`);
+}
+
+for (const id of [-1, 65535, 65536]) {
+ test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ assert_throws_js(TypeError, () => pc.createDataChannel('',
+ { 'negotiated': true, 'id': id }));
+ }, `createDataChannel with id ${id} should throw TypeError`);
+}
+
+/*
+ 6.2. createDataChannel
+ 5. If [[DataChannelLabel]] is longer than 65535 bytes, throw a TypeError.
+ */
+test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ assert_throws_js(TypeError, () =>
+ pc.createDataChannel('l'.repeat(65536)));
+
+ assert_throws_js(TypeError, () =>
+ pc.createDataChannel('l'.repeat(65536), {
+ negotiated: true,
+ id: 42
+ }));
+}, 'createDataChannel with too long label should throw TypeError');
+
+test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ assert_throws_js(TypeError, () =>
+ pc.createDataChannel('\u00b5'.repeat(32768)));
+
+ assert_throws_js(TypeError, () =>
+ pc.createDataChannel('\u00b5'.repeat(32768), {
+ negotiated: true,
+ id: 42
+ }));
+}, 'createDataChannel with too long label (2 byte unicode) should throw TypeError');
+
+/*
+ 6.2. label
+ [...] Scripts are allowed to create multiple RTCDataChannel objects with the same label.
+ [...]
+ */
+test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const label = 'test';
+
+ pc.createDataChannel(label);
+ pc.createDataChannel(label);
+}, 'createDataChannel with same label used twice should not throw');
+
+/*
+ 6.2. createDataChannel
+ 13. If [[DataChannelProtocol]] is longer than 65535 bytes long, throw a TypeError.
+ */
+
+test(t => {
+ const pc = new RTCPeerConnection;
+ t.add_cleanup(() => pc.close());
+ const channel = pc.createDataChannel('', { negotiated: true, id: 42 });
+ assert_equals(channel.negotiated, true);
+}, 'createDataChannel with negotiated true and id should succeed');
+
+test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ assert_throws_js(TypeError, () =>
+ pc.createDataChannel('', {
+ protocol: 'p'.repeat(65536)
+ }));
+
+ assert_throws_js(TypeError, () =>
+ pc.createDataChannel('', {
+ protocol: 'p'.repeat(65536),
+ negotiated: true,
+ id: 42
+ }));
+}, 'createDataChannel with too long protocol should throw TypeError');
+
+test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ assert_throws_js(TypeError, () =>
+ pc.createDataChannel('', {
+ protocol: '\u00b6'.repeat(32768)
+ }));
+
+ assert_throws_js(TypeError, () =>
+ pc.createDataChannel('', {
+ protocol: '\u00b6'.repeat(32768),
+ negotiated: true,
+ id: 42
+ }));
+}, 'createDataChannel with too long protocol (2 byte unicode) should throw TypeError');
+
+test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const label = 'l'.repeat(65535);
+ const protocol = 'p'.repeat(65535);
+
+ const dc = pc.createDataChannel(label, {
+ protocol: protocol
+ });
+
+ assert_equals(dc.label, label);
+ assert_equals(dc.protocol, protocol);
+}, 'createDataChannel with maximum length label and protocol should succeed');
+
+/*
+ 6.2 createDataChannel
+ 15. Let channel have an [[DataChannelId]] internal slot initialized to option's id member,
+ if it is present and [[Negotiated]] is true, otherwise null.
+
+ NOTE
+ This means the id member will be ignored if the data channel is negotiated in-band; this
+ is intentional. Data channels negotiated in-band should have IDs selected based on the
+ DTLS role, as specified in [RTCWEB-DATA-PROTOCOL].
+ */
+test(t => {
+ const pc = new RTCPeerConnection;
+ t.add_cleanup(() => pc.close());
+
+ const dc = pc.createDataChannel('', {
+ negotiated: false,
+ });
+ assert_equals(dc.negotiated, false, 'Expect dc.negotiated to be false');
+}, 'createDataChannel with negotiated false should succeed');
+
+test(t => {
+ const pc = new RTCPeerConnection;
+ t.add_cleanup(() => pc.close());
+
+ const dc = pc.createDataChannel('', {
+ negotiated: false,
+ id: 42
+ });
+ assert_equals(dc.negotiated, false, 'Expect dc.negotiated to be false');
+ assert_equals(dc.id, null, 'Expect dc.id to be ignored (null)');
+}, 'createDataChannel with negotiated false and id 42 should ignore the id');
+
+/*
+ 6.2. createDataChannel
+ 16. If [[Negotiated]] is true and [[DataChannelId]] is null, throw a TypeError.
+ */
+test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ assert_throws_js(TypeError, () =>
+ pc.createDataChannel('test', {
+ negotiated: true
+ }));
+}, 'createDataChannel with negotiated true and id not defined should throw TypeError');
+
+/*
+ 4.4.1.6. Set the RTCSessionSessionDescription
+ 2.2.6. If description is of type "answer" or "pranswer", then run the
+ following steps:
+ 3. If description negotiates the DTLS role of the SCTP transport, and there is an
+ RTCDataChannel with a null id, then generate an ID according to
+ [RTCWEB-DATA-PROTOCOL]. [...]
+
+ 6.1. createDataChannel
+ 21. If the [[DataChannelId]] slot is null (due to no ID being passed into
+ createDataChannel, or [[Negotiated]] being false), and the DTLS role of the SCTP
+ transport has already been negotiated, then initialize [[DataChannelId]] to a value
+ generated by the user agent, according to [RTCWEB-DATA-PROTOCOL], and skip
+ to the next step. If no available ID could be generated, or if the value of the
+ [[DataChannelId]] slot is being used by an existing RTCDataChannel, throw an
+ OperationError exception.
+
+ Note
+ If the [[DataChannelId]] slot is null after this step, it will be populated once
+ the DTLS role is determined during the process of setting an RTCSessionDescription.
+ */
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+
+ const negotiatedDc = pc1.createDataChannel('negotiated-channel', {
+ negotiated: true,
+ id: 42,
+ });
+ assert_equals(negotiatedDc.id, 42, 'Expect negotiatedDc.id to be 42');
+
+ const dc1 = pc1.createDataChannel('channel');
+ assert_equals(dc1.id, null, 'Expect initial id to be null');
+
+ const offer = await pc1.createOffer();
+ await Promise.all([pc1.setLocalDescription(offer), pc2.setRemoteDescription(offer)]);
+ const answer = await pc2.createAnswer();
+ await pc1.setRemoteDescription(answer);
+
+ assert_not_equals(dc1.id, null,
+ 'Expect dc1.id to be assigned after remote description has been set');
+
+ assert_greater_than_equal(dc1.id, 0,
+ 'Expect dc1.id to be set to valid unsigned short');
+
+ assert_less_than(dc1.id, 65535,
+ 'Expect dc1.id to be set to valid unsigned short');
+
+ const dc2 = pc1.createDataChannel('channel');
+
+ assert_not_equals(dc2.id, null,
+ 'Expect dc2.id to be assigned after remote description has been set');
+
+ assert_greater_than_equal(dc2.id, 0,
+ 'Expect dc2.id to be set to valid unsigned short');
+
+ assert_less_than(dc2.id, 65535,
+ 'Expect dc2.id to be set to valid unsigned short');
+
+ assert_not_equals(dc2, dc1,
+ 'Expect channels created from same label to be different');
+
+ assert_equals(dc2.label, dc1.label,
+ 'Expect different channels can have the same label but different id');
+
+ assert_not_equals(dc2.id, dc1.id,
+ 'Expect different channels can have the same label but different id');
+
+ assert_equals(negotiatedDc.id, 42,
+ 'Expect negotiatedDc.id to be 42 after remote description has been set');
+}, 'Channels created (after setRemoteDescription) should have id assigned');
+
+test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const dc1 = pc.createDataChannel('channel-1', {
+ negotiated: true,
+ id: 42,
+ });
+ assert_equals(dc1.id, 42,
+ 'Expect dc1.id to be 42');
+
+ const dc2 = pc.createDataChannel('channel-2', {
+ negotiated: true,
+ id: 43,
+ });
+ assert_equals(dc2.id, 43,
+ 'Expect dc2.id to be 43');
+
+ assert_throws_dom('OperationError', () =>
+ pc.createDataChannel('channel-3', {
+ negotiated: true,
+ id: 42,
+ }));
+
+}, 'Reusing a data channel id that is in use should throw OperationError');
+
+// We've seen implementations behaving differently before and after the connection has been
+// established.
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+
+ const dc1 = pc1.createDataChannel('channel-1', {
+ negotiated: true,
+ id: 42,
+ });
+ assert_equals(dc1.id, 42, 'Expect dc1.id to be 42');
+
+ const dc2 = pc1.createDataChannel('channel-2', {
+ negotiated: true,
+ id: 43,
+ });
+ assert_equals(dc2.id, 43, 'Expect dc2.id to be 43');
+
+ const offer = await pc1.createOffer();
+ await Promise.all([pc1.setLocalDescription(offer), pc2.setRemoteDescription(offer)]);
+ const answer = await pc2.createAnswer();
+ await pc1.setRemoteDescription(answer);
+
+ assert_equals(dc1.id, 42, 'Expect dc1.id to be 42');
+
+ assert_equals(dc2.id, 43, 'Expect dc2.id to be 43');
+
+ assert_throws_dom('OperationError', () =>
+ pc1.createDataChannel('channel-3', {
+ negotiated: true,
+ id: 42,
+ }));
+}, 'Reusing a data channel id that is in use (after setRemoteDescription) should throw ' +
+ 'OperationError');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+
+ const dc1 = pc1.createDataChannel('channel-1');
+
+ const offer = await pc1.createOffer();
+ await Promise.all([pc1.setLocalDescription(offer), pc2.setRemoteDescription(offer)]);
+ const answer = await pc2.createAnswer();
+ await pc1.setRemoteDescription(answer);
+
+ assert_not_equals(dc1.id, null,
+ 'Expect dc1.id to be assigned after remote description has been set');
+
+ assert_throws_dom('OperationError', () =>
+ pc1.createDataChannel('channel-2', {
+ negotiated: true,
+ id: dc1.id,
+ }));
+}, 'Reusing a data channel id that is in use (after setRemoteDescription, negotiated via DCEP) ' +
+ 'should throw OperationError');
+
+
+for (const options of [{}, {negotiated: true, id: 0}]) {
+ const mode = `${options.negotiated? "negotiated " : ""}datachannel`;
+
+ // Based on https://bugzilla.mozilla.org/show_bug.cgi?id=1441723
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+
+ await createDataChannelPair(t, options, pc1);
+
+ const dc = pc1.createDataChannel('');
+ assert_equals(dc.readyState, 'connecting', 'Channel should be in the connecting state');
+ }, `New ${mode} should be in the connecting state after creation ` +
+ `(after connection establishment)`);
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const stream = await getNoiseStream({audio: true, video: true});
+ t.add_cleanup(() => stopTracks(stream));
+ const audio = stream.getAudioTracks()[0];
+ const video = stream.getVideoTracks()[0];
+ pc1.addTrack(audio, stream);
+ pc1.addTrack(video, stream);
+ await createDataChannelPair(t, options, pc1);
+ }, `addTrack, then creating ${mode}, should negotiate properly`);
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection({bundlePolicy: "max-bundle"});
+ t.add_cleanup(() => pc1.close());
+ const stream = await getNoiseStream({audio: true, video: true});
+ t.add_cleanup(() => stopTracks(stream));
+ const audio = stream.getAudioTracks()[0];
+ const video = stream.getVideoTracks()[0];
+ pc1.addTrack(audio, stream);
+ pc1.addTrack(video, stream);
+ await createDataChannelPair(t, options, pc1);
+ }, `addTrack, then creating ${mode}, should negotiate properly when max-bundle is used`);
+
+/*
+This test is disabled until https://github.com/w3c/webrtc-pc/issues/2562
+has been resolved; it presupposes that stopping the first transceiver
+breaks the transport.
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection({bundlePolicy: "max-bundle"});
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+ const stream = await getNoiseStream({audio: true, video: true});
+ t.add_cleanup(() => stopTracks(stream));
+ const audio = stream.getAudioTracks()[0];
+ const video = stream.getVideoTracks()[0];
+ pc1.addTrack(audio, stream);
+ pc1.addTrack(video, stream);
+ const [dc1, dc2] = await createDataChannelPair(t, options, pc1, pc2);
+
+ pc2.getTransceivers()[0].stop();
+ const dc1Closed = new Promise(r => dc1.onclose = r);
+ await exchangeOfferAnswer(pc1, pc2);
+ await dc1Closed;
+ }, `Stopping the bundle-tag when there is a ${mode} in the bundle ` +
+ `should kill the DataChannel`);
+*/
+}
+
+/*
+ Untestable
+ 6.1. createDataChannel
+ 19. If a setting, either [[MaxPacketLifeTime]] or [[MaxRetransmits]], has been set to
+ indicate unreliable mode, and that value exceeds the maximum value supported
+ by the user agent, the value MUST be set to the user agents maximum value.
+
+ 23. Return channel and continue the following steps in parallel.
+ 24. Create channel's associated underlying data transport and configure
+ it according to the relevant properties of channel.
+
+ Tested in RTCPeerConnection-onnegotiationneeded.html
+ 22. If channel is the first RTCDataChannel created on connection, update the
+ negotiation-needed flag for connection.
+
+ Tested in RTCDataChannel-id.html
+ - Odd/even rules for '.id'
+
+ Tested in RTCDataChannel-dcep.html
+ - Transmission of '.label' and further options
+*/
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-createOffer.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-createOffer.html
new file mode 100644
index 0000000000..704fa3c646
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-createOffer.html
@@ -0,0 +1,134 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerConnection.prototype.createOffer</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="RTCPeerConnection-helper.js"></script>
+<script>
+ 'use strict';
+
+ // Test is based on the following editor draft:
+ // https://w3c.github.io/webrtc-pc/archives/20170515/webrtc.html
+
+ // The following helper functions are called from RTCPeerConnection-helper.js:
+ // countAudioLine()
+ // countVideoLine()
+ // assert_session_desc_similar()
+
+ /*
+ * 4.3.2. createOffer()
+ */
+
+ /*
+ * Final steps to create an offer
+ * 4. Let offer be a newly created RTCSessionDescriptionInit dictionary
+ * with its type member initialized to the string "offer" and its sdp member
+ * initialized to sdpString.
+ */
+ promise_test(t => {
+ const pc = new RTCPeerConnection()
+
+ t.add_cleanup(() => pc.close());
+
+ return pc.createOffer()
+ .then(offer => {
+ assert_equals(typeof offer, 'object',
+ 'Expect offer to be plain object dictionary RTCSessionDescriptionInit');
+
+ assert_false(offer instanceof RTCSessionDescription,
+ 'Expect offer to not be instance of RTCSessionDescription')
+ });
+ }, 'createOffer() with no argument from newly created RTCPeerConnection should succeed');
+
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const states = [];
+ pc.addEventListener('signalingstatechange', () => states.push(pc.signalingState));
+
+ return generateVideoReceiveOnlyOffer(pc)
+ .then(offer =>
+ pc.setLocalDescription(offer)
+ .then(() => {
+ assert_equals(pc.signalingState, 'have-local-offer');
+ assert_session_desc_similar(pc.localDescription, offer);
+ assert_session_desc_similar(pc.pendingLocalDescription, offer);
+ assert_equals(pc.currentLocalDescription, null);
+
+ assert_array_equals(states, ['have-local-offer']);
+ }));
+ }, 'createOffer() and then setLocalDescription() should succeed');
+
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ pc.close();
+
+ return promise_rejects_dom(t, 'InvalidStateError',
+ pc.createOffer());
+ }, 'createOffer() after connection is closed should reject with InvalidStateError');
+
+ /*
+ * Final steps to create an offer
+ * 2. If connection was modified in such a way that additional inspection of the
+ * system state is necessary, then in parallel begin the steps to create an
+ * offer again, given p, and abort these steps.
+ *
+ * This test might hit step 2 of final steps to create an offer. But the media stream
+ * is likely added already by the time steps to create an offer is executed, because
+ * that is enqueued as an operation.
+ * Either way it verifies that the media stream is included in the offer even though
+ * the stream is added after synchronous call to createOffer.
+ */
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const promise = pc.createOffer();
+
+ pc.addTransceiver('audio');
+ return promise.then(offer => {
+ assert_equals(countAudioLine(offer.sdp), 1,
+ 'Expect m=audio line to be found in offer SDP')
+ });
+ }, 'When media stream is added when createOffer() is running in parallel, the result offer should contain the new media stream');
+
+ /*
+ If connection's signaling state is neither "stable" nor "have-local-offer", return a promise rejected with a newly created InvalidStateError.
+ */
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const states = [];
+ pc.addEventListener('signalingstatechange', () => states.push(pc.signalingState));
+
+ return generateVideoReceiveOnlyOffer(pc)
+ .then(offer =>
+ pc.setRemoteDescription(offer)
+ .then(() => {
+ assert_equals(pc.signalingState, 'have-remote-offer');
+ return promise_rejects_dom(t, 'InvalidStateError',
+ pc.createOffer());
+ })
+ )
+ }, 'createOffer() should fail when signaling state is not stable or have-local-offer');
+ /*
+ * TODO
+ * 4.3.2 createOffer
+ * 3. If connection is configured with an identity provider, and an identity
+ * assertion has not yet been generated using said identity provider, then
+ * begin the identity assertion request process if it has not already begun.
+ * Steps to create an offer
+ * 1. If the need for an identity assertion was identified when createOffer was
+ * invoked, wait for the identity assertion request process to complete.
+ *
+ * Non-Testable
+ * 4.3.2 createOffer
+ * Steps to create an offer
+ * 4. Inspect the system state to determine the currently available resources as
+ * necessary for generating the offer, as described in [JSEP] (section 4.1.6.).
+ * 5. If this inspection failed for any reason, reject p with a newly created
+ * OperationError and abort these steps.
+ */
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-description-attributes-timing.https.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-description-attributes-timing.https.html
new file mode 100644
index 0000000000..2d2565c3e1
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-description-attributes-timing.https.html
@@ -0,0 +1,81 @@
+<!doctype html>
+<meta charset=utf-8>
+<title></title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script>
+'use strict';
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const offer = await pc.createOffer();
+
+ assert_equals(pc.pendingLocalDescription, null,
+ 'pendingLocalDescription is null before setLocalDescription');
+ const promise = pc.setLocalDescription(offer);
+ assert_equals(pc.pendingLocalDescription, null,
+ 'pendingLocalDescription is still null while promise pending');
+ await promise;
+ assert_not_equals(pc.pendingLocalDescription, null,
+ 'pendingLocalDescription is not null after await');
+}, "pendingLocalDescription is surfaced at the right time");
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const offer = await pc.createOffer();
+
+ assert_equals(pc.pendingRemoteDescription, null,
+ 'pendingRemoteDescription is null before setRemoteDescription');
+ const promise = pc.setRemoteDescription(offer);
+ assert_equals(pc.pendingRemoteDescription, null,
+ 'pendingRemoteDescription is still null while promise pending');
+ await promise;
+ assert_not_equals(pc.pendingRemoteDescription, null,
+ 'pendingRemoteDescription is not null after await');
+}, "pendingRemoteDescription is surfaced at the right time");
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const offer = await pc1.createOffer();
+ await pc1.setLocalDescription(offer);
+ await pc2.setRemoteDescription(offer);
+ const answer = await pc2.createAnswer();
+
+ assert_equals(pc2.currentLocalDescription, null,
+ 'currentLocalDescription is null before setLocalDescription');
+ const promise = pc2.setLocalDescription(answer);
+ assert_equals(pc2.currentLocalDescription, null,
+ 'currentLocalDescription is still null while promise pending');
+ await promise;
+ assert_not_equals(pc2.currentLocalDescription, null,
+ 'currentLocalDescription is not null after await');
+}, "currentLocalDescription is surfaced at the right time");
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const offer = await pc1.createOffer();
+ await pc1.setLocalDescription(offer);
+ await pc2.setRemoteDescription(offer);
+ const answer = await pc2.createAnswer();
+
+ assert_equals(pc1.currentRemoteDescription, null,
+ 'currentRemoteDescription is null before setRemoteDescription');
+ const promise = pc1.setRemoteDescription(answer);
+ assert_equals(pc1.currentRemoteDescription, null,
+ 'currentRemoteDescription is still null while promise pending');
+ await promise;
+ assert_not_equals(pc1.currentRemoteDescription, null,
+ 'currentRemoteDescription is not null after await');
+}, "currentRemoteDescription is surfaced at the right time");
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-explicit-rollback-iceGatheringState.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-explicit-rollback-iceGatheringState.html
new file mode 100644
index 0000000000..e39b985bef
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-explicit-rollback-iceGatheringState.html
@@ -0,0 +1,53 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerConnection.prototype.iceGatheringState</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="RTCPeerConnection-helper.js"></script>
+<script>
+'use strict';
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+ pc1.addTransceiver('audio', { direction: 'recvonly' });
+ await initialOfferAnswerWithIceGatheringStateTransitions(
+ pc1, pc2);
+ await pc1.setLocalDescription(await pc1.createOffer({iceRestart: true}));
+ await iceGatheringStateTransitions(pc1, 'gathering', 'complete');
+ expectNoMoreGatheringStateChanges(t, pc1);
+ await pc1.setLocalDescription({type: 'rollback'});
+ await new Promise(r => t.step_timeout(r, 1000));
+}, 'rolling back an ICE restart when gathering is complete should not result in iceGatheringState changes');
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ pc.addTransceiver('audio', { direction: 'recvonly' });
+ await pc.setLocalDescription(
+ await pc.createOffer());
+ await iceGatheringStateTransitions(pc, 'gathering', 'complete');
+ await pc.setLocalDescription({type: 'rollback'});
+ await iceGatheringStateTransitions(pc, 'new');
+}, 'setLocalDescription(rollback) of original offer should cause iceGatheringState to reach "new" when starting in "complete"');
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ pc.addTransceiver('audio', { direction: 'recvonly' });
+ await pc.setLocalDescription(
+ await pc.createOffer());
+ await iceGatheringStateTransitions(pc, 'gathering');
+ await pc.setLocalDescription({type: 'rollback'});
+ // We might go directly to 'new', or we might go to 'complete' first,
+ // depending on timing. Allow either.
+ const results = await Promise.allSettled([
+ iceGatheringStateTransitions(pc, 'new'),
+ iceGatheringStateTransitions(pc, 'complete', 'new')]);
+ assert_true(results.some(result => result.status == 'fulfilled'),
+ 'ICE gathering state should go back to "new", possibly through "complete"');
+}, 'setLocalDescription(rollback) of original offer should cause iceGatheringState to reach "new" when starting in "gathering"');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-generateCertificate.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-generateCertificate.html
new file mode 100644
index 0000000000..4cda97e9b7
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-generateCertificate.html
@@ -0,0 +1,138 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>Test RTCPeerConnection.generateCertificate</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+ 'use strict';
+
+ // Test is based on the following editor draft:
+ // https://w3c.github.io/webrtc-pc/archives/20170515/webrtc.html
+
+ /*
+ * 4.10. Certificate Management
+ * partial interface RTCPeerConnection {
+ * static Promise<RTCCertificate> generateCertificate(
+ * AlgorithmIdentifier keygenAlgorithm);
+ * };
+ *
+ * 4.10.2. RTCCertificate Interface
+ * interface RTCCertificate {
+ * readonly attribute DOMTimeStamp expires;
+ * ...
+ * };
+ *
+ * [WebCrypto]
+ * 11. Algorithm Dictionary
+ * typedef (object or DOMString) AlgorithmIdentifier;
+ */
+
+ /*
+ * 4.10. The following values must be supported by a user agent:
+ * { name: "RSASSA-PKCS1-v1_5", modulusLength: 2048,
+ * publicExponent: new Uint8Array([1, 0, 1]), hash: "SHA-256" },
+ * and { name: "ECDSA", namedCurve: "P-256" }.
+ */
+ promise_test(t =>
+ RTCPeerConnection.generateCertificate({
+ name: 'RSASSA-PKCS1-v1_5',
+ modulusLength: 2048,
+ publicExponent: new Uint8Array([1, 0, 1]),
+ hash: 'SHA-256'
+ }).then(cert => {
+ assert_true(cert instanceof RTCCertificate,
+ 'Expect cert to be instance of RTCCertificate');
+
+ assert_greater_than(cert.expires, Date.now(),
+ 'Expect generated certificate to expire reasonably long after current time');
+ }),
+ 'generateCertificate() with compulsary RSASSA-PKCS1-v1_5 parameters should succeed');
+
+ promise_test(t =>
+ RTCPeerConnection.generateCertificate({
+ name: 'ECDSA',
+ namedCurve: 'P-256'
+ }).then(cert => {
+ assert_true(cert instanceof RTCCertificate,
+ 'Expect cert to be instance of RTCCertificate');
+
+ assert_greater_than(cert.expires, Date.now(),
+ 'Expect generated certificate to expire reasonably long after current time');
+ }),
+ 'generateCertificate() with compulsary ECDSA parameters should succeed');
+
+ /*
+ * 4.10. A user agent must reject a call to generateCertificate() with a
+ * DOMException of type NotSupportedError if the keygenAlgorithm
+ * parameter identifies an algorithm that the user agent cannot or
+ * will not use to generate a certificate for RTCPeerConnection.
+ */
+ promise_test(t =>
+ promise_rejects_dom(t, 'NotSupportedError',
+ RTCPeerConnection.generateCertificate('invalid-algo')),
+ 'generateCertificate() with invalid string algorithm should reject with NotSupportedError');
+
+ promise_test(t =>
+ promise_rejects_dom(t, 'NotSupportedError',
+ RTCPeerConnection.generateCertificate({
+ name: 'invalid-algo'
+ })),
+ 'generateCertificate() with invalid algorithm dict should reject with NotSupportedError');
+
+ /*
+ * 4.10.1. Dictionary RTCCertificateExpiration
+ * dictionary RTCCertificateExpiration {
+ * [EnforceRange]
+ * DOMTimeStamp expires;
+ * };
+ *
+ * If this parameter is present it indicates the maximum time that
+ * the RTCCertificate is valid for relative to the current time.
+ *
+ * When generateCertificate is called with an object argument,
+ * the user agent attempts to convert the object into a
+ * RTCCertificateExpiration. If this is unsuccessful, immediately
+ * return a promise that is rejected with a newly created TypeError
+ * and abort processing.
+ */
+
+ promise_test(t => {
+ const start = Date.now();
+ return RTCPeerConnection.generateCertificate({
+ name: 'ECDSA',
+ namedCurve: 'P-256',
+ expires: 2000
+ }).then(cert => {
+ assert_approx_equals(cert.expires, start+2000, 1000);
+ })
+ }, 'generateCertificate() with valid expires parameter should succeed');
+
+ promise_test(t => {
+ return RTCPeerConnection.generateCertificate({
+ name: 'ECDSA',
+ namedCurve: 'P-256',
+ expires: 0
+ }).then(cert => {
+ assert_less_than_equal(cert.expires, Date.now());
+ })
+ }, 'generateCertificate() with 0 expires parameter should generate expired cert');
+
+ promise_test(t => {
+ return promise_rejects_js(t, TypeError,
+ RTCPeerConnection.generateCertificate({
+ name: 'ECDSA',
+ namedCurve: 'P-256',
+ expires: -1
+ }))
+ }, 'generateCertificate() with invalid range for expires should reject with TypeError');
+
+ promise_test(t => {
+ return promise_rejects_js(t, TypeError,
+ RTCPeerConnection.generateCertificate({
+ name: 'ECDSA',
+ namedCurve: 'P-256',
+ expires: 'invalid'
+ }))
+ }, 'generateCertificate() with invalid type for expires should reject with TypeError');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-getStats.https.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-getStats.https.html
new file mode 100644
index 0000000000..4889bcf4dd
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-getStats.https.html
@@ -0,0 +1,422 @@
+<!doctype html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<title>RTCPeerConnection.prototype.getStats</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="RTCPeerConnection-helper.js"></script>
+<script src="dictionary-helper.js"></script>
+<script src="RTCStats-helper.js"></script>
+<script>
+ 'use strict';
+
+ // Test is based on the following editor draft:
+ // webrtc-pc 20171130
+ // webrtc-stats 20171122
+
+ // The following helper function is called from RTCPeerConnection-helper.js
+ // getTrackFromUserMedia
+
+ // The following helper function is called from RTCStats-helper.js
+ // validateStatsReport
+ // assert_stats_report_has_stats
+
+ // The following helper function is called from RTCPeerConnection-helper.js
+ // exchangeIceCandidates
+ // exchangeOfferAnswer
+
+ /*
+ 8.2. getStats
+ 1. Let selectorArg be the method's first argument.
+ 2. Let connection be the RTCPeerConnection object on which the method was invoked.
+ 3. If selectorArg is null, let selector be null.
+ 4. If selectorArg is a MediaStreamTrack let selector be an RTCRtpSender
+ or RTCRtpReceiver on connection which track member matches selectorArg.
+ If no such sender or receiver exists, or if more than one sender or
+ receiver fit this criteria, return a promise rejected with a newly
+ created InvalidAccessError.
+ 5. Let p be a new promise.
+ 6. Run the following steps in parallel:
+ 1. Gather the stats indicated by selector according to the stats selection algorithm.
+ 2. Resolve p with the resulting RTCStatsReport object, containing the gathered stats.
+ */
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ return pc.getStats();
+ }, 'getStats() with no argument should succeed');
+
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ return pc.getStats(null);
+ }, 'getStats(null) should succeed');
+
+ /*
+ 8.2. getStats
+ 4. If selectorArg is a MediaStreamTrack let selector be an RTCRtpSender
+ or RTCRtpReceiver on connection which track member matches selectorArg.
+ If no such sender or receiver exists, or if more than one sender or
+ receiver fit this criteria, return a promise rejected with a newly
+ created InvalidAccessError.
+ */
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ return getTrackFromUserMedia('audio')
+ .then(([track, mediaStream]) => {
+ return promise_rejects_dom(t, 'InvalidAccessError', pc.getStats(track));
+ });
+ }, 'getStats() with track not added to connection should reject with InvalidAccessError');
+
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ return getTrackFromUserMedia('audio')
+ .then(([track, mediaStream]) => {
+ pc.addTrack(track, mediaStream);
+ return pc.getStats(track);
+ });
+ }, 'getStats() with track added via addTrack should succeed');
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const stream = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const [track] = stream.getTracks();
+ pc.addTransceiver(track);
+
+ return pc.getStats(track);
+ }, 'getStats() with track added via addTransceiver should succeed');
+
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const transceiver1 = pc.addTransceiver('audio');
+
+ // Create another transceiver that resends what
+ // is being received, kind of like echo
+ const transceiver2 = pc.addTransceiver(transceiver1.receiver.track);
+ assert_equals(transceiver1.receiver.track, transceiver2.sender.track);
+
+ return promise_rejects_dom(t, 'InvalidAccessError', pc.getStats(transceiver1.receiver.track));
+ }, 'getStats() with track associated with both sender and receiver should reject with InvalidAccessError');
+
+ /*
+ 8.5. The stats selection algorithm
+ 2. If selector is null, gather stats for the whole connection, add them to result,
+ return result, and abort these steps.
+ */
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ return pc.getStats()
+ .then(statsReport => {
+ validateStatsReport(statsReport);
+ assert_stats_report_has_stats(statsReport, ['peer-connection']);
+ });
+ }, 'getStats() with no argument should return stats report containing peer-connection stats on an empty PC');
+
+ promise_test(async t => {
+ const pc = createPeerConnectionWithCleanup(t);
+ const pc2 = createPeerConnectionWithCleanup(t);
+ const [sendtrack, mediaStream] = await getTrackFromUserMedia('audio');
+ pc.addTrack(sendtrack, mediaStream);
+ exchangeIceCandidates(pc, pc2);
+ await Promise.all([
+ exchangeOfferAnswer(pc, pc2),
+ new Promise(r => pc2.ontrack = e => e.track.onunmute = r)
+ ]);
+ const statsReport = await pc.getStats();
+ getRequiredStats(statsReport, 'peer-connection');
+ getRequiredStats(statsReport, 'outbound-rtp');
+ }, 'getStats() track with stream returns peer-connection and outbound-rtp stats');
+
+ promise_test(async t => {
+ const pc = createPeerConnectionWithCleanup(t);
+ const pc2 = createPeerConnectionWithCleanup(t);
+ const [sendtrack, mediaStream] = await getTrackFromUserMedia('audio');
+ pc.addTrack(sendtrack);
+ exchangeIceCandidates(pc, pc2);
+ await Promise.all([
+ exchangeOfferAnswer(pc, pc2),
+ new Promise(r => pc2.ontrack = e => e.track.onunmute = r)
+ ]);
+ const statsReport = await pc.getStats();
+ getRequiredStats(statsReport, 'peer-connection');
+ getRequiredStats(statsReport, 'outbound-rtp');
+ }, 'getStats() track without stream returns peer-connection and outbound-rtp stats');
+
+ promise_test(async t => {
+ const pc = createPeerConnectionWithCleanup(t);
+ const pc2 = createPeerConnectionWithCleanup(t);
+ const [sendtrack, mediaStream] = await getTrackFromUserMedia('audio');
+ pc.addTrack(sendtrack, mediaStream);
+ exchangeIceCandidates(pc, pc2);
+ await Promise.all([
+ exchangeOfferAnswer(pc, pc2),
+ new Promise(r => pc2.ontrack = e => e.track.onunmute = r)
+ ]);
+ const statsReport = await pc.getStats();
+ assert_stats_report_has_stats(statsReport, ['outbound-rtp']);
+ }, 'getStats() audio outbound-rtp contains all mandatory stats');
+
+ promise_test(async t => {
+ const pc = createPeerConnectionWithCleanup(t);
+ const pc2 = createPeerConnectionWithCleanup(t);
+ const [sendtrack, mediaStream] = await getTrackFromUserMedia('video');
+ pc.addTrack(sendtrack, mediaStream);
+ exchangeIceCandidates(pc, pc2);
+ await Promise.all([
+ exchangeOfferAnswer(pc, pc2),
+ new Promise(r => pc2.ontrack = e => e.track.onunmute = r)
+ ]);
+ const statsReport = await pc.getStats();
+ assert_stats_report_has_stats(statsReport, ['outbound-rtp']);
+ }, 'getStats() video outbound-rtp contains all mandatory stats');
+
+ promise_test(async t => {
+ const pc = createPeerConnectionWithCleanup(t);
+ const pc2 = createPeerConnectionWithCleanup(t);
+ const [audioTrack, audioStream] = await getTrackFromUserMedia('audio');
+ pc.addTrack(audioTrack, audioStream);
+ const [videoTrack, videoStream] = await getTrackFromUserMedia('video');
+ pc.addTrack(videoTrack, videoStream);
+ exchangeIceCandidates(pc, pc2);
+ await Promise.all([
+ exchangeOfferAnswer(pc, pc2),
+ new Promise(r => pc2.ontrack = e => e.track.onunmute = r)
+ ]);
+ const statsReport = await pc.getStats();
+ validateStatsReport(statsReport);
+ }, 'getStats() audio and video validate all mandatory stats');
+
+ /*
+ 8.5. The stats selection algorithm
+ 3. If selector is an RTCRtpSender, gather stats for and add the following objects
+ to result:
+ - All RTCOutboundRtpStreamStats objects corresponding to selector.
+ - All stats objects referenced directly or indirectly by the RTCOutboundRtpStreamStats
+ objects added.
+ */
+ promise_test(async t => {
+ const pc = createPeerConnectionWithCleanup(t);
+ const pc2 = createPeerConnectionWithCleanup(t);
+
+ let [sendtrack, mediaStream] = await getTrackFromUserMedia('audio');
+ pc.addTrack(sendtrack, mediaStream);
+ exchangeIceCandidates(pc, pc2);
+ await Promise.all([
+ exchangeOfferAnswer(pc, pc2),
+ new Promise(r => pc2.ontrack = e => e.track.onunmute = r)
+ ]);
+ const stats = await pc.getStats(sendtrack);
+ getRequiredStats(stats, 'outbound-rtp');
+ }, `getStats() on track associated with RTCRtpSender should return stats report containing outbound-rtp stats`);
+
+ /*
+ 8.5. The stats selection algorithm
+ 4. If selector is an RTCRtpReceiver, gather stats for and add the following objects
+ to result:
+ - All RTCInboundRtpStreamStats objects corresponding to selector.
+ - All stats objects referenced directly or indirectly by the RTCInboundRtpStreamStats
+ added.
+ */
+ promise_test(async t => {
+ const pc = createPeerConnectionWithCleanup(t);
+ const pc2 = createPeerConnectionWithCleanup(t);
+
+ let [track, mediaStream] = await getTrackFromUserMedia('audio');
+ pc.addTrack(track, mediaStream);
+ exchangeIceCandidates(pc, pc2);
+ await exchangeOfferAnswer(pc, pc2);
+ // Wait for unmute if the track is not already unmuted.
+ // According to spec, it should be muted when being created, but this
+ // is not what this test is testing, so allow it to be unmuted.
+ if (pc2.getReceivers()[0].track.muted) {
+ await new Promise(resolve => {
+ pc2.getReceivers()[0].track.addEventListener('unmute', resolve);
+ });
+ }
+ const stats = await pc2.getStats(pc2.getReceivers()[0].track);
+ getRequiredStats(stats, 'inbound-rtp');
+ }, `getStats() on track associated with RTCRtpReceiver should return stats report containing inbound-rtp stats`);
+
+ promise_test(async t => {
+ const pc = createPeerConnectionWithCleanup(t);
+ const pc2 = createPeerConnectionWithCleanup(t);
+
+ let [track, mediaStream] = await getTrackFromUserMedia('audio');
+ pc.addTrack(track, mediaStream);
+ exchangeIceCandidates(pc, pc2);
+ await exchangeOfferAnswer(pc, pc2);
+ // Wait for unmute if the track is not already unmuted.
+ // According to spec, it should be muted when being created, but this
+ // is not what this test is testing, so allow it to be unmuted.
+ if (pc2.getReceivers()[0].track.muted) {
+ await new Promise(resolve => {
+ pc2.getReceivers()[0].track.addEventListener('unmute', resolve);
+ });
+ }
+ const stats = await pc2.getStats(pc2.getReceivers()[0].track);
+ getRequiredStats(stats, 'inbound-rtp');
+ }, `getStats() inbound-rtp contains all mandatory stats`);
+
+ /*
+ 8.6 Mandatory To Implement Stats
+ An implementation MUST support generating statistics of the following types
+ when the corresponding objects exist on a PeerConnection, with the attributes
+ that are listed when they are valid for that object.
+ */
+
+ const mandatoryStats = [
+ "codec",
+ "inbound-rtp",
+ "outbound-rtp",
+ "remote-inbound-rtp",
+ "remote-outbound-rtp",
+ "media-source",
+ "peer-connection",
+ "data-channel",
+ "sender",
+ "receiver",
+ "transport",
+ "candidate-pair",
+ "local-candidate",
+ "remote-candidate",
+ "certificate"
+ ];
+
+ async_test(t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const dataChannel = pc1.createDataChannel('test-channel');
+
+ getNoiseStream({
+ audio: true,
+ video: true
+ })
+ .then(t.step_func(mediaStream => {
+ const tracks = mediaStream.getTracks();
+ const [audioTrack] = mediaStream.getAudioTracks();
+ const [videoTrack] = mediaStream.getVideoTracks();
+
+ for (const track of mediaStream.getTracks()) {
+ t.add_cleanup(() => track.stop());
+ pc1.addTrack(track, mediaStream);
+ }
+
+ const testStatsReport = (pc, statsReport) => {
+ validateStatsReport(statsReport);
+ assert_stats_report_has_stats(statsReport, mandatoryStats);
+
+ const dataChannelStats = findStatsFromReport(statsReport,
+ stats => {
+ return stats.type === 'data-channel' &&
+ stats.dataChannelIdentifier === dataChannel.id;
+ },
+ 'Expect data channel stats to be found');
+
+ assert_equals(dataChannelStats.label, 'test-channel');
+
+ /* TODO track stats are obsolete - replace with sender/receiver? */
+ const audioTrackStats = findStatsFromReport(statsReport,
+ stats => {
+ return stats.type === 'track' &&
+ stats.trackIdentifier === audioTrack.id;
+ },
+ 'Expect audio track stats to be found');
+
+ assert_equals(audioTrackStats.kind, 'audio');
+
+ const videoTrackStats = findStatsFromReport(statsReport,
+ stats => {
+ return stats.type === 'track' &&
+ stats.trackIdentifier === videoTrack.id;
+ },
+ 'Expect video track stats to be found');
+
+ assert_equals(videoTrackStats.kind, 'video');
+ }
+
+ const onConnected = t.step_func(() => {
+ // Wait a while for the peer connections to collect stats
+ t.step_timeout(() => {
+ Promise.all([
+ /* TODO: for both pc1 and pc2 to expose all mandatory stats, they need to both send/receive tracks and data channels */
+ pc1.getStats()
+ .then(statsReport => testStatsReport(pc1, statsReport)),
+
+ pc2.getStats()
+ .then(statsReport => testStatsReport(pc2, statsReport))
+ ])
+ .then(t.step_func_done())
+ .catch(t.step_func(err => {
+ assert_unreached(`test failed with error: ${err}`);
+ }));
+ }, 200)
+ })
+
+ let onTrackCount = 0
+ let onDataChannelCalled = false
+
+ pc2.addEventListener('track', t.step_func(() => {
+ onTrackCount++;
+ if (onTrackCount === 2 && onDataChannelCalled) {
+ onConnected();
+ }
+ }));
+
+ pc2.addEventListener('datachannel', t.step_func(() => {
+ onDataChannelCalled = true;
+ if (onTrackCount === 2) {
+ onConnected();
+ }
+ }));
+
+
+ exchangeIceCandidates(pc1, pc2);
+ exchangeOfferAnswer(pc1, pc2);
+ }))
+ .catch(t.step_func(err => {
+ assert_unreached(`test failed with error: ${err}`);
+ }));
+
+ }, `getStats() with connected peer connections having tracks and data channel should return all mandatory to implement stats`);
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const [track, mediaStream] = await getTrackFromUserMedia('audio');
+ pc.addTransceiver(track);
+ pc.addTransceiver(track);
+ await promise_rejects_dom(t, 'InvalidAccessError', pc.getStats(track));
+ }, `getStats(track) should not work if multiple senders have the same track`);
+
+ promise_test(async t => {
+ const kMinimumTimeElapsedBetweenGetStatsCallsMs = 500;
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const t0 = Math.floor(performance.now());
+ const t0Stats = getRequiredStats(await pc.getStats(), 'peer-connection');
+ await new Promise(
+ r => t.step_timeout(r, kMinimumTimeElapsedBetweenGetStatsCallsMs));
+ const t1Stats = getRequiredStats(await pc.getStats(), 'peer-connection');
+ const t1 = Math.ceil(performance.now());
+ const maximumTimeElapsedBetweenGetStatsCallsMs = t1 - t0;
+ const deltaTimestampMs = t1Stats.timestamp - t0Stats.timestamp;
+ // The delta must be at least the time we waited between calls.
+ assert_greater_than_equal(deltaTimestampMs,
+ kMinimumTimeElapsedBetweenGetStatsCallsMs);
+ // The delta must be at most the time elapsed before the first getStats()
+ // call and after the second getStats() call.
+ assert_less_than_equal(deltaTimestampMs,
+ maximumTimeElapsedBetweenGetStatsCallsMs);
+ }, `RTCStats.timestamp increases with time passing`);
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-getTransceivers.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-getTransceivers.html
new file mode 100644
index 0000000000..381b42b0cf
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-getTransceivers.html
@@ -0,0 +1,39 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerConnection.prototype.getTransceivers</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+ 'use strict';
+
+ // Test is based on the following editor draft:
+ // https://w3c.github.io/webrtc-pc/archives/20170605/webrtc.html
+
+ /*
+ * 5.1. RTCPeerConnection Interface Extensions
+ * partial interface RTCPeerConnection {
+ * sequence<RTCRtpSender> getSenders();
+ * sequence<RTCRtpReceiver> getReceivers();
+ * sequence<RTCRtpTransceiver> getTransceivers();
+ * ...
+ * };
+ */
+
+ test(t => {
+ const pc = new RTCPeerConnection();
+
+ assert_idl_attribute(pc, 'getSenders');
+ const senders = pc.getSenders();
+ assert_array_equals([], senders, 'Expect senders to be empty array');
+
+ assert_idl_attribute(pc, 'getReceivers');
+ const receivers = pc.getReceivers();
+ assert_array_equals([], receivers, 'Expect receivers to be empty array');
+
+ assert_idl_attribute(pc, 'getTransceivers');
+ const transceivers = pc.getTransceivers();
+ assert_array_equals([], transceivers, 'Expect transceivers to be empty array');
+
+ }, 'Initial peer connection should have list of zero senders, receivers and transceivers');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-helper-test.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-helper-test.html
new file mode 100644
index 0000000000..42f6652ac4
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-helper-test.html
@@ -0,0 +1,21 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerConnection-helper tests</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="RTCPeerConnection-helper.js"></script>
+<script>
+'use strict';
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+ const transceiver = pc1.addTransceiver('video');
+
+ exchangeIceCandidates(pc1, pc2);
+ await exchangeOfferAnswer(pc1, pc2);
+ await waitForState(transceiver.sender.transport, 'connected');
+}, 'Setting up a connection using helpers and defaults should work');
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-helper.js b/testing/web-platform/tests/webrtc/RTCPeerConnection-helper.js
new file mode 100644
index 0000000000..eefe10579b
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-helper.js
@@ -0,0 +1,722 @@
+'use strict'
+
+/*
+ * Helper Methods for testing the following methods in RTCPeerConnection:
+ * createOffer
+ * createAnswer
+ * setLocalDescription
+ * setRemoteDescription
+ *
+ * This file offers the following features:
+ * SDP similarity comparison
+ * Generating offer/answer using anonymous peer connection
+ * Test signalingstatechange event
+ * Test promise that never resolve
+ */
+
+const audioLineRegex = /\r\nm=audio.+\r\n/g;
+const videoLineRegex = /\r\nm=video.+\r\n/g;
+const applicationLineRegex = /\r\nm=application.+\r\n/g;
+
+function countLine(sdp, regex) {
+ const matches = sdp.match(regex);
+ if(matches === null) {
+ return 0;
+ } else {
+ return matches.length;
+ }
+}
+
+function countAudioLine(sdp) {
+ return countLine(sdp, audioLineRegex);
+}
+
+function countVideoLine(sdp) {
+ return countLine(sdp, videoLineRegex);
+}
+
+function countApplicationLine(sdp) {
+ return countLine(sdp, applicationLineRegex);
+}
+
+function similarMediaDescriptions(sdp1, sdp2) {
+ if(sdp1 === sdp2) {
+ return true;
+ } else if(
+ countAudioLine(sdp1) !== countAudioLine(sdp2) ||
+ countVideoLine(sdp1) !== countVideoLine(sdp2) ||
+ countApplicationLine(sdp1) !== countApplicationLine(sdp2))
+ {
+ return false;
+ } else {
+ return true;
+ }
+}
+
+// Assert that given object is either an
+// RTCSessionDescription or RTCSessionDescriptionInit
+function assert_is_session_description(sessionDesc) {
+ if(sessionDesc instanceof RTCSessionDescription) {
+ return;
+ }
+
+ assert_not_equals(sessionDesc, undefined,
+ 'Expect session description to be defined');
+
+ assert_true(typeof(sessionDesc) === 'object',
+ 'Expect sessionDescription to be either a RTCSessionDescription or an object');
+
+ assert_true(typeof(sessionDesc.type) === 'string',
+ 'Expect sessionDescription.type to be a string');
+
+ assert_true(typeof(sessionDesc.sdp) === 'string',
+ 'Expect sessionDescription.sdp to be a string');
+}
+
+
+// We can't do string comparison to the SDP content,
+// because RTCPeerConnection may return SDP that is
+// slightly modified or reordered from what is given
+// to it due to ICE candidate events or serialization.
+// Instead, we create SDP with different number of media
+// lines, and if the SDP strings are not the same, we
+// simply count the media description lines and if they
+// are the same, we assume it is the same.
+function isSimilarSessionDescription(sessionDesc1, sessionDesc2) {
+ assert_is_session_description(sessionDesc1);
+ assert_is_session_description(sessionDesc2);
+
+ if(sessionDesc1.type !== sessionDesc2.type) {
+ return false;
+ } else {
+ return similarMediaDescriptions(sessionDesc1.sdp, sessionDesc2.sdp);
+ }
+}
+
+function assert_session_desc_similar(sessionDesc1, sessionDesc2) {
+ assert_true(isSimilarSessionDescription(sessionDesc1, sessionDesc2),
+ 'Expect both session descriptions to have the same count of media lines');
+}
+
+function assert_session_desc_not_similar(sessionDesc1, sessionDesc2) {
+ assert_false(isSimilarSessionDescription(sessionDesc1, sessionDesc2),
+ 'Expect both session descriptions to have different count of media lines');
+}
+
+async function generateDataChannelOffer(pc) {
+ pc.createDataChannel('test');
+ const offer = await pc.createOffer();
+ assert_equals(countApplicationLine(offer.sdp), 1, 'Expect m=application line to be present in generated SDP');
+ return offer;
+}
+
+async function generateAudioReceiveOnlyOffer(pc)
+{
+ try {
+ pc.addTransceiver('audio', { direction: 'recvonly' });
+ return pc.createOffer();
+ } catch(e) {
+ return pc.createOffer({ offerToReceiveAudio: true });
+ }
+}
+
+async function generateVideoReceiveOnlyOffer(pc)
+{
+ try {
+ pc.addTransceiver('video', { direction: 'recvonly' });
+ return pc.createOffer();
+ } catch(e) {
+ return pc.createOffer({ offerToReceiveVideo: true });
+ }
+}
+
+// Helper function to generate answer based on given offer using a freshly
+// created RTCPeerConnection object
+async function generateAnswer(offer) {
+ const pc = new RTCPeerConnection();
+ await pc.setRemoteDescription(offer);
+ const answer = await pc.createAnswer();
+ pc.close();
+ return answer;
+}
+
+// Helper function to generate offer using a freshly
+// created RTCPeerConnection object
+async function generateOffer() {
+ const pc = new RTCPeerConnection();
+ const offer = await pc.createOffer();
+ pc.close();
+ return offer;
+}
+
+// Run a test function that return a promise that should
+// never be resolved. For lack of better options,
+// we wait for a time out and pass the test if the
+// promise doesn't resolve within that time.
+function test_never_resolve(testFunc, testName) {
+ async_test(t => {
+ testFunc(t)
+ .then(
+ t.step_func(result => {
+ assert_unreached(`Pending promise should never be resolved. Instead it is fulfilled with: ${result}`);
+ }),
+ t.step_func(err => {
+ assert_unreached(`Pending promise should never be resolved. Instead it is rejected with: ${err}`);
+ }));
+
+ t.step_timeout(t.step_func_done(), 100)
+ }, testName);
+}
+
+// Helper function to exchange ice candidates between
+// two local peer connections
+function exchangeIceCandidates(pc1, pc2) {
+ // private function
+ function doExchange(localPc, remotePc) {
+ localPc.addEventListener('icecandidate', event => {
+ const { candidate } = event;
+
+ // Guard against already closed peerconnection to
+ // avoid unrelated exceptions.
+ if (remotePc.signalingState !== 'closed') {
+ remotePc.addIceCandidate(candidate);
+ }
+ });
+ }
+
+ doExchange(pc1, pc2);
+ doExchange(pc2, pc1);
+}
+
+// Returns a promise that resolves when a |name| event is fired.
+function waitUntilEvent(obj, name) {
+ return new Promise(r => obj.addEventListener(name, r, {once: true}));
+}
+
+// Returns a promise that resolves when the |transport.state| is |state|
+// This should work for RTCSctpTransport, RTCDtlsTransport and RTCIceTransport.
+async function waitForState(transport, state) {
+ while (transport.state != state) {
+ await waitUntilEvent(transport, 'statechange');
+ }
+}
+
+// Returns a promise that resolves when |pc.iceConnectionState| is 'connected'
+// or 'completed'.
+async function listenToIceConnected(pc) {
+ await waitForIceStateChange(pc, ['connected', 'completed']);
+}
+
+// Returns a promise that resolves when |pc.iceConnectionState| is in one of the
+// wanted states.
+async function waitForIceStateChange(pc, wantedStates) {
+ while (!wantedStates.includes(pc.iceConnectionState)) {
+ await waitUntilEvent(pc, 'iceconnectionstatechange');
+ }
+}
+
+// Returns a promise that resolves when |pc.connectionState| is 'connected'.
+async function listenToConnected(pc) {
+ while (pc.connectionState != 'connected') {
+ await waitUntilEvent(pc, 'connectionstatechange');
+ }
+}
+
+// Returns a promise that resolves when |pc.connectionState| is in one of the
+// wanted states.
+async function waitForConnectionStateChange(pc, wantedStates) {
+ while (!wantedStates.includes(pc.connectionState)) {
+ await waitUntilEvent(pc, 'connectionstatechange');
+ }
+}
+
+async function waitForIceGatheringState(pc, wantedStates) {
+ while (!wantedStates.includes(pc.iceGatheringState)) {
+ await waitUntilEvent(pc, 'icegatheringstatechange');
+ }
+}
+
+// Resolves when RTP packets have been received.
+async function listenForSSRCs(t, receiver) {
+ while (true) {
+ const ssrcs = receiver.getSynchronizationSources();
+ if (Array.isArray(ssrcs) && ssrcs.length > 0) {
+ return ssrcs;
+ }
+ await new Promise(r => t.step_timeout(r, 0));
+ }
+}
+
+// Helper function to create a pair of connected data channels.
+// On success the promise resolves to an array with two data channels.
+// It does the heavy lifting of performing signaling handshake,
+// ICE candidate exchange, and waiting for data channel at two
+// end points to open. Can do both negotiated and non-negotiated setup.
+async function createDataChannelPair(t, options,
+ pc1 = createPeerConnectionWithCleanup(t),
+ pc2 = createPeerConnectionWithCleanup(t)) {
+ let pair = [], bothOpen;
+ try {
+ if (options.negotiated) {
+ pair = [pc1, pc2].map(pc => pc.createDataChannel('', options));
+ bothOpen = Promise.all(pair.map(dc => new Promise((r, e) => {
+ dc.onopen = r;
+ dc.onerror = ({error}) => e(error);
+ })));
+ } else {
+ pair = [pc1.createDataChannel('', options)];
+ bothOpen = Promise.all([
+ new Promise((r, e) => {
+ pair[0].onopen = r;
+ pair[0].onerror = ({error}) => e(error);
+ }),
+ new Promise((r, e) => pc2.ondatachannel = ({channel}) => {
+ pair[1] = channel;
+ channel.onopen = r;
+ channel.onerror = ({error}) => e(error);
+ })
+ ]);
+ }
+ exchangeIceCandidates(pc1, pc2);
+ await exchangeOfferAnswer(pc1, pc2);
+ await bothOpen;
+ return pair;
+ } finally {
+ for (const dc of pair) {
+ dc.onopen = dc.onerror = null;
+ }
+ }
+}
+
+// Wait for RTP and RTCP stats to arrive
+async function waitForRtpAndRtcpStats(pc) {
+ // If remote stats are never reported, return after 5 seconds.
+ const startTime = performance.now();
+ while (true) {
+ const report = await pc.getStats();
+ const stats = [...report.values()].filter(({type}) => type.endsWith("bound-rtp"));
+ // Each RTP and RTCP stat has a reference
+ // to the matching stat in the other direction
+ if (stats.length && stats.every(({localId, remoteId}) => localId || remoteId)) {
+ break;
+ }
+ if (performance.now() > startTime + 5000) {
+ break;
+ }
+ }
+}
+
+// Wait for a single message event and return
+// a promise that resolve when the event fires
+function awaitMessage(channel) {
+ const once = true;
+ return new Promise((resolve, reject) => {
+ channel.addEventListener('message', ({data}) => resolve(data), {once});
+ channel.addEventListener('error', reject, {once});
+ });
+}
+
+// Helper to convert a blob to array buffer so that
+// we can read the content
+async function blobToArrayBuffer(blob) {
+ const reader = new FileReader();
+ reader.readAsArrayBuffer(blob);
+ return new Promise((resolve, reject) => {
+ reader.addEventListener('load', () => resolve(reader.result), {once: true});
+ reader.addEventListener('error', () => reject(reader.error), {once: true});
+ });
+}
+
+// Assert that two TypedArray or ArrayBuffer objects have the same byte values
+function assert_equals_typed_array(array1, array2) {
+ const [view1, view2] = [array1, array2].map((array) => {
+ if (array instanceof ArrayBuffer) {
+ return new DataView(array);
+ } else {
+ assert_true(array.buffer instanceof ArrayBuffer,
+ 'Expect buffer to be instance of ArrayBuffer');
+ return new DataView(array.buffer, array.byteOffset, array.byteLength);
+ }
+ });
+
+ assert_equals(view1.byteLength, view2.byteLength,
+ 'Expect both arrays to be of the same byte length');
+
+ const byteLength = view1.byteLength;
+
+ for (let i = 0; i < byteLength; ++i) {
+ assert_equals(view1.getUint8(i), view2.getUint8(i),
+ `Expect byte at buffer position ${i} to be equal`);
+ }
+}
+
+// These media tracks will be continually updated with deterministic "noise" in
+// order to ensure UAs do not cease transmission in response to apparent
+// silence.
+//
+// > Many codecs and systems are capable of detecting "silence" and changing
+// > their behavior in this case by doing things such as not transmitting any
+// > media.
+//
+// Source: https://w3c.github.io/webrtc-pc/#offer-answer-options
+const trackFactories = {
+ // Share a single context between tests to avoid exceeding resource limits
+ // without requiring explicit destruction.
+ audioContext: null,
+
+ /**
+ * Given a set of requested media types, determine if the user agent is
+ * capable of procedurally generating a suitable media stream.
+ *
+ * @param {object} requested
+ * @param {boolean} [requested.audio] - flag indicating whether the desired
+ * stream should include an audio track
+ * @param {boolean} [requested.video] - flag indicating whether the desired
+ * stream should include a video track
+ *
+ * @returns {boolean}
+ */
+ canCreate(requested) {
+ const supported = {
+ audio: !!window.AudioContext && !!window.MediaStreamAudioDestinationNode,
+ video: !!HTMLCanvasElement.prototype.captureStream
+ };
+
+ return (!requested.audio || supported.audio) &&
+ (!requested.video || supported.video);
+ },
+
+ audio() {
+ const ctx = trackFactories.audioContext = trackFactories.audioContext ||
+ new AudioContext();
+ const oscillator = ctx.createOscillator();
+ const dst = oscillator.connect(ctx.createMediaStreamDestination());
+ oscillator.start();
+ return dst.stream.getAudioTracks()[0];
+ },
+
+ video({width = 640, height = 480, signal} = {}) {
+ const canvas = Object.assign(
+ document.createElement("canvas"), {width, height}
+ );
+ const ctx = canvas.getContext('2d');
+ const stream = canvas.captureStream();
+
+ let count = 0;
+ const interval = setInterval(() => {
+ ctx.fillStyle = `rgb(${count%255}, ${count*count%255}, ${count%255})`;
+ count += 1;
+ ctx.fillRect(0, 0, width, height);
+ // Add some bouncing boxes in contrast color to add a little more noise.
+ const contrast = count + 128;
+ ctx.fillStyle = `rgb(${contrast%255}, ${contrast*contrast%255}, ${contrast%255})`;
+ const xpos = count % (width - 20);
+ const ypos = count % (height - 20);
+ ctx.fillRect(xpos, ypos, xpos + 20, ypos + 20);
+ const xpos2 = (count + width / 2) % (width - 20);
+ const ypos2 = (count + height / 2) % (height - 20);
+ ctx.fillRect(xpos2, ypos2, xpos2 + 20, ypos2 + 20);
+ // If signal is set (0-255), add a constant-color box of that luminance to
+ // the video frame at coordinates 20 to 60 in both X and Y direction.
+ // (big enough to avoid color bleed from surrounding video in some codecs,
+ // for more stable tests).
+ if (signal != undefined) {
+ ctx.fillStyle = `rgb(${signal}, ${signal}, ${signal})`;
+ ctx.fillRect(20, 20, 40, 40);
+ }
+ }, 100);
+
+ if (document.body) {
+ document.body.appendChild(canvas);
+ } else {
+ document.addEventListener('DOMContentLoaded', () => {
+ document.body.appendChild(canvas);
+ }, {once: true});
+ }
+
+ // Implement track.stop() for performance in some tests on some platforms
+ const track = stream.getVideoTracks()[0];
+ const nativeStop = track.stop;
+ track.stop = function stop() {
+ clearInterval(interval);
+ nativeStop.apply(this);
+ if (document.body && canvas.parentElement == document.body) {
+ document.body.removeChild(canvas);
+ }
+ };
+ return track;
+ }
+};
+
+// Get the signal from a video element inserted by createNoiseStream
+function getVideoSignal(v) {
+ if (v.videoWidth < 60 || v.videoHeight < 60) {
+ throw new Error('getVideoSignal: video too small for test');
+ }
+ const canvas = document.createElement("canvas");
+ canvas.width = canvas.height = 60;
+ const context = canvas.getContext('2d');
+ context.drawImage(v, 0, 0);
+ // Extract pixel value at position 40, 40
+ const pixel = context.getImageData(40, 40, 1, 1);
+ // Use luma reconstruction to get back original value according to
+ // ITU-R rec BT.709
+ return (pixel.data[0] * 0.21 + pixel.data[1] * 0.72 + pixel.data[2] * 0.07);
+}
+
+async function detectSignal(t, v, value) {
+ while (true) {
+ const signal = getVideoSignal(v).toFixed();
+ // allow off-by-two pixel error (observed in some implementations)
+ if (value - 2 <= signal && signal <= value + 2) {
+ return;
+ }
+ // We would like to wait for each new frame instead here,
+ // but there seems to be no such callback.
+ await new Promise(r => t.step_timeout(r, 100));
+ }
+}
+
+// Generate a MediaStream bearing the specified tracks.
+//
+// @param {object} [caps]
+// @param {boolean} [caps.audio] - flag indicating whether the generated stream
+// should include an audio track
+// @param {boolean} [caps.video] - flag indicating whether the generated stream
+// should include a video track, or parameters for video
+async function getNoiseStream(caps = {}) {
+ if (!trackFactories.canCreate(caps)) {
+ return navigator.mediaDevices.getUserMedia(caps);
+ }
+ const tracks = [];
+
+ if (caps.audio) {
+ tracks.push(trackFactories.audio());
+ }
+
+ if (caps.video) {
+ tracks.push(trackFactories.video(caps.video));
+ }
+
+ return new MediaStream(tracks);
+}
+
+// Obtain a MediaStreamTrack of kind using procedurally-generated streams (and
+// falling back to `getUserMedia` when the user agent cannot generate the
+// requested streams).
+// Return Promise of pair of track and associated mediaStream.
+// Assumes that there is at least one available device
+// to generate the track.
+function getTrackFromUserMedia(kind) {
+ return getNoiseStream({ [kind]: true })
+ .then(mediaStream => {
+ const [track] = mediaStream.getTracks();
+ return [track, mediaStream];
+ });
+}
+
+// Obtain |count| MediaStreamTracks of type |kind| and MediaStreams. The tracks
+// do not belong to any stream and the streams are empty. Returns a Promise
+// resolved with a pair of arrays [tracks, streams].
+// Assumes there is at least one available device to generate the tracks and
+// streams and that the getUserMedia() calls resolve.
+function getUserMediaTracksAndStreams(count, type = 'audio') {
+ let otherTracksPromise;
+ if (count > 1)
+ otherTracksPromise = getUserMediaTracksAndStreams(count - 1, type);
+ else
+ otherTracksPromise = Promise.resolve([[], []]);
+ return otherTracksPromise.then(([tracks, streams]) => {
+ return getTrackFromUserMedia(type)
+ .then(([track, stream]) => {
+ // Remove the default stream-track relationship.
+ stream.removeTrack(track);
+ tracks.push(track);
+ streams.push(stream);
+ return [tracks, streams];
+ });
+ });
+}
+
+// Performs an offer exchange caller -> callee.
+async function exchangeOffer(caller, callee) {
+ await caller.setLocalDescription(await caller.createOffer());
+ await callee.setRemoteDescription(caller.localDescription);
+}
+// Performs an answer exchange caller -> callee.
+async function exchangeAnswer(caller, callee) {
+ // Note that caller's remote description must be set first; if not,
+ // there's a chance that candidates from callee arrive at caller before
+ // it has a remote description to apply them to.
+ const answer = await callee.createAnswer();
+ await caller.setRemoteDescription(answer);
+ await callee.setLocalDescription(answer);
+}
+async function exchangeOfferAnswer(caller, callee) {
+ await exchangeOffer(caller, callee);
+ await exchangeAnswer(caller, callee);
+}
+
+// The returned promise is resolved with caller's ontrack event.
+async function exchangeAnswerAndListenToOntrack(t, caller, callee) {
+ const ontrackPromise = addEventListenerPromise(t, caller, 'track');
+ await exchangeAnswer(caller, callee);
+ return ontrackPromise;
+}
+// The returned promise is resolved with callee's ontrack event.
+async function exchangeOfferAndListenToOntrack(t, caller, callee) {
+ const ontrackPromise = addEventListenerPromise(t, callee, 'track');
+ await exchangeOffer(caller, callee);
+ return ontrackPromise;
+}
+
+// The resolver extends a |promise| that can be resolved or rejected using |resolve|
+// or |reject|.
+class Resolver extends Promise {
+ constructor(executor) {
+ let resolve, reject;
+ super((resolve_, reject_) => {
+ resolve = resolve_;
+ reject = reject_;
+ if (executor) {
+ return executor(resolve_, reject_);
+ }
+ });
+
+ this._done = false;
+ this._resolve = resolve;
+ this._reject = reject;
+ }
+
+ /**
+ * Return whether the promise is done (resolved or rejected).
+ */
+ get done() {
+ return this._done;
+ }
+
+ /**
+ * Resolve the promise.
+ */
+ resolve(...args) {
+ this._done = true;
+ return this._resolve(...args);
+ }
+
+ /**
+ * Reject the promise.
+ */
+ reject(...args) {
+ this._done = true;
+ return this._reject(...args);
+ }
+}
+
+function addEventListenerPromise(t, obj, type, listener) {
+ if (!listener) {
+ return waitUntilEvent(obj, type);
+ }
+ return new Promise(r => obj.addEventListener(type,
+ t.step_func(e => r(listener(e))),
+ {once: true}));
+}
+
+function createPeerConnectionWithCleanup(t) {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ return pc;
+}
+
+async function createTrackAndStreamWithCleanup(t, kind = 'audio') {
+ let constraints = {};
+ constraints[kind] = true;
+ const stream = await getNoiseStream(constraints);
+ const [track] = stream.getTracks();
+ t.add_cleanup(() => track.stop());
+ return [track, stream];
+}
+
+function findTransceiverForSender(pc, sender) {
+ const transceivers = pc.getTransceivers();
+ for (let i = 0; i < transceivers.length; ++i) {
+ if (transceivers[i].sender == sender)
+ return transceivers[i];
+ }
+ return null;
+}
+
+function preferCodec(transceiver, mimeType, sdpFmtpLine) {
+ const {codecs} = RTCRtpSender.getCapabilities(transceiver.receiver.track.kind);
+ // sdpFmtpLine is optional, pick the first partial match if not given.
+ const selectedCodecIndex = codecs.findIndex(c => {
+ return c.mimeType === mimeType && (c.sdpFmtpLine === sdpFmtpLine || !sdpFmtpLine);
+ });
+ const selectedCodec = codecs[selectedCodecIndex];
+ codecs.slice(selectedCodecIndex, 1);
+ codecs.unshift(selectedCodec);
+ return transceiver.setCodecPreferences(codecs);
+}
+
+// Contains a set of values and will yell at you if you try to add a value twice.
+class UniqueSet extends Set {
+ constructor(items) {
+ super();
+ if (items !== undefined) {
+ for (const item of items) {
+ this.add(item);
+ }
+ }
+ }
+
+ add(value, message) {
+ if (message === undefined) {
+ message = `Value '${value}' needs to be unique but it is already in the set`;
+ }
+ assert_true(!this.has(value), message);
+ super.add(value);
+ }
+}
+
+const iceGatheringStateTransitions = async (pc, ...states) => {
+ for (const state of states) {
+ await new Promise((resolve, reject) => {
+ pc.addEventListener('icegatheringstatechange', () => {
+ if (pc.iceGatheringState == state) {
+ resolve();
+ } else {
+ reject(`Unexpected gathering state: ${pc.iceGatheringState}, was expecting ${state}`);
+ }
+ }, {once: true});
+ });
+ }
+};
+
+const initialOfferAnswerWithIceGatheringStateTransitions =
+ async (pc1, pc2, offerOptions) => {
+ await pc1.setLocalDescription(
+ await pc1.createOffer(offerOptions));
+ const pc1Transitions =
+ iceGatheringStateTransitions(pc1, 'gathering', 'complete');
+ await pc2.setRemoteDescription(pc1.localDescription);
+ await pc2.setLocalDescription(await pc2.createAnswer());
+ const pc2Transitions =
+ iceGatheringStateTransitions(pc2, 'gathering', 'complete');
+ await pc1.setRemoteDescription(pc2.localDescription);
+ await pc1Transitions;
+ await pc2Transitions;
+ };
+
+const expectNoMoreGatheringStateChanges = async (t, pc) => {
+ pc.onicegatheringstatechange =
+ t.step_func(() => {
+ assert_unreached(
+ 'Should not get an icegatheringstatechange right now!');
+ });
+};
+
+async function queueAWebrtcTask() {
+ const pc = new RTCPeerConnection();
+ pc.addTransceiver('audio');
+ await new Promise(r => pc.onnegotiationneeded = r);
+}
+
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-iceConnectionState-disconnected.https.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-iceConnectionState-disconnected.https.html
new file mode 100644
index 0000000000..af55a0c003
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-iceConnectionState-disconnected.https.html
@@ -0,0 +1,30 @@
+<!doctype html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<title>RTCPeerConnection.prototype.iceConnectionState - disconnection</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="RTCPeerConnection-helper.js"></script>
+<script>
+ 'use strict';
+ promise_test(async t => {
+ const caller = new RTCPeerConnection();
+ t.add_cleanup(() => caller.close());
+ const callee = new RTCPeerConnection();
+ t.add_cleanup(() => callee.close());
+
+ const stream = await getNoiseStream({audio:true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const [track] = stream.getTracks();
+ caller.addTrack(track, stream);
+ exchangeIceCandidates(caller, callee);
+ await exchangeOfferAnswer(caller, callee);
+
+ await listenToIceConnected(caller);
+
+ callee.close();
+ await waitForIceStateChange(caller, ['disconnected', 'failed']);
+ // TODO: this should eventually transition to failed but that takes
+ // somewhat long (15-30s) so is not testable.
+ }, 'ICE goes to disconnected if the other side goes away');
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-iceConnectionState.https.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-iceConnectionState.https.html
new file mode 100644
index 0000000000..5083be6cdf
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-iceConnectionState.https.html
@@ -0,0 +1,396 @@
+<!doctype html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<title>RTCPeerConnection.prototype.iceConnectionState</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="RTCPeerConnection-helper.js"></script>
+<script>
+ 'use strict';
+
+ // Test is based on the following editor draft:
+ // https://w3c.github.io/webrtc-pc/archives/20170605/webrtc.html
+
+ /*
+ 4.3.2. Interface Definition
+ interface RTCPeerConnection : EventTarget {
+ ...
+ readonly attribute RTCIceConnectionState iceConnectionState;
+ attribute EventHandler oniceconnectionstatechange;
+ };
+
+ 4.4.4 RTCIceConnectionState Enum
+ enum RTCIceConnectionState {
+ "new",
+ "checking",
+ "connected",
+ "completed",
+ "failed",
+ "disconnected",
+ "closed"
+ };
+
+ 5.6. RTCIceTransport Interface
+ interface RTCIceTransport {
+ readonly attribute RTCIceTransportState state;
+ attribute EventHandler onstatechange;
+
+ ...
+ };
+
+ enum RTCIceTransportState {
+ "new",
+ "checking",
+ "connected",
+ "completed",
+ "failed",
+ "disconnected",
+ "closed"
+ };
+ */
+
+ /*
+ 4.4.4 RTCIceConnectionState Enum
+ new
+ Any of the RTCIceTransports are in the new state and none of them
+ are in the checking, failed or disconnected state, or all
+ RTCIceTransport s are in the closed state.
+ */
+ test(t => {
+ const pc = new RTCPeerConnection();
+ assert_equals(pc.iceConnectionState, 'new');
+ }, 'Initial iceConnectionState should be new');
+
+ test(t => {
+ const pc = new RTCPeerConnection();
+ pc.close();
+ assert_equals(pc.iceConnectionState, 'closed');
+ }, 'Closing the connection should set iceConnectionState to closed');
+
+ /*
+ 4.4.4 RTCIceConnectionState Enum
+ checking
+ Any of the RTCIceTransport s are in the checking state and none of
+ them are in the failed or disconnected state.
+
+ connected
+ All RTCIceTransport s are in the connected, completed or closed state
+ and at least one of them is in the connected state.
+
+ completed
+ All RTCIceTransport s are in the completed or closed state and at least
+ one of them is in the completed state.
+
+ checking
+ The RTCIceTransport has received at least one remote candidate and
+ is checking candidate pairs and has either not yet found a connection
+ or consent checks [RFC7675] have failed on all previously successful
+ candidate pairs. In addition to checking, it may also still be gathering.
+
+ 5.6. enum RTCIceTransportState
+ connected
+ The RTCIceTransport has found a usable connection, but is still
+ checking other candidate pairs to see if there is a better connection.
+ It may also still be gathering and/or waiting for additional remote
+ candidates. If consent checks [RFC7675] fail on the connection in use,
+ and there are no other successful candidate pairs available, then the
+ state transitions to "checking" (if there are candidate pairs remaining
+ to be checked) or "disconnected" (if there are no candidate pairs to
+ check, but the peer is still gathering and/or waiting for additional
+ remote candidates).
+
+ completed
+ The RTCIceTransport has finished gathering, received an indication that
+ there are no more remote candidates, finished checking all candidate
+ pairs and found a connection. If consent checks [RFC7675] subsequently
+ fail on all successful candidate pairs, the state transitions to "failed".
+ */
+ async_test(t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ let had_checking = false;
+
+ const onIceConnectionStateChange = t.step_func(() => {
+ const {iceConnectionState} = pc1;
+ if (iceConnectionState === 'checking') {
+ had_checking = true;
+ } else if (iceConnectionState === 'connected' ||
+ iceConnectionState === 'completed') {
+ assert_true(had_checking, 'state should pass checking before' +
+ ' reaching connected or completed');
+ t.done();
+ } else if (iceConnectionState === 'failed') {
+ assert_unreached("ICE should not fail");
+ }
+ });
+
+ pc1.createDataChannel('test');
+
+ pc1.addEventListener('iceconnectionstatechange', onIceConnectionStateChange);
+
+ exchangeIceCandidates(pc1, pc2);
+ exchangeOfferAnswer(pc1, pc2);
+ }, 'connection with one data channel should eventually have connected or ' +
+ 'completed connection state');
+
+async_test(t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+
+ t.add_cleanup(() => pc2.close());
+
+ const onIceConnectionStateChange = t.step_func(() => {
+ const { iceConnectionState } = pc1;
+
+ if(iceConnectionState === 'checking') {
+ const iceTransport = pc1.sctp.transport.iceTransport;
+
+ assert_equals(iceTransport.state, 'checking',
+ 'Expect ICE transport to be in checking state when' +
+ ' iceConnectionState is checking');
+
+ } else if(iceConnectionState === 'connected') {
+ const iceTransport = pc1.sctp.transport.iceTransport;
+
+ assert_equals(iceTransport.state, 'connected',
+ 'Expect ICE transport to be in connected state when' +
+ ' iceConnectionState is connected');
+ t.done();
+ } else if(iceConnectionState === 'completed') {
+ const iceTransport = pc1.sctp.transport.iceTransport;
+
+ assert_equals(iceTransport.state, 'completed',
+ 'Expect ICE transport to be in connected state when' +
+ ' iceConnectionState is completed');
+ t.done();
+ } else if (iceConnectionState === 'failed') {
+ assert_unreached("ICE should not fail");
+ }
+ });
+
+ pc1.createDataChannel('test');
+
+ assert_equals(pc1.oniceconnectionstatechange, null,
+ 'Expect connection to have iceconnectionstatechange event');
+
+ pc1.addEventListener('iceconnectionstatechange', onIceConnectionStateChange);
+
+ exchangeIceCandidates(pc1, pc2);
+ exchangeOfferAnswer(pc1, pc2);
+ }, 'connection with one data channel should eventually ' +
+ 'have connected connection state');
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const stream = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ stream.getTracks().forEach(track => pc1.addTrack(track, stream));
+
+ exchangeIceCandidates(pc1, pc2);
+ exchangeOfferAnswer(pc1, pc2);
+ await listenToIceConnected(pc1);
+ }, 'connection with audio track should eventually ' +
+ 'have connected connection state');
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const stream = await getNoiseStream({audio: true, video:true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ stream.getTracks().forEach(track => pc1.addTrack(track, stream));
+
+ exchangeIceCandidates(pc1, pc2);
+ exchangeOfferAnswer(pc1, pc2);
+ await listenToIceConnected(pc1);
+ }, 'connection with audio and video tracks should eventually ' +
+ 'have connected connection state');
+
+ promise_test(async t => {
+ const caller = new RTCPeerConnection();
+ t.add_cleanup(() => caller.close());
+ const callee = new RTCPeerConnection();
+ t.add_cleanup(() => callee.close());
+
+ caller.addTransceiver('audio', {direction:'recvonly'});
+ const stream = await getNoiseStream({audio:true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const [track] = stream.getTracks();
+ callee.addTrack(track, stream);
+ exchangeIceCandidates(caller, callee);
+ await exchangeOfferAnswer(caller, callee);
+
+ assert_equals(caller.getTransceivers().length, 1);
+ const [transceiver] = caller.getTransceivers();
+ assert_equals(transceiver.currentDirection, 'recvonly');
+
+ await listenToIceConnected(caller);
+ }, 'ICE can connect in a recvonly usecase');
+
+ /*
+ TODO
+ 4.4.4 RTCIceConnectionState Enum
+ failed
+ Any of the RTCIceTransport s are in the failed state.
+
+ disconnected
+ Any of the RTCIceTransport s are in the disconnected state and none of
+ them are in the failed state.
+
+ closed
+ The RTCPeerConnection object's [[ isClosed]] slot is true.
+
+ 5.6. enum RTCIceTransportState
+ new
+ The RTCIceTransport is gathering candidates and/or waiting for
+ remote candidates to be supplied, and has not yet started checking.
+
+ failed
+ The RTCIceTransport has finished gathering, received an indication that
+ there are no more remote candidates, finished checking all candidate pairs,
+ and all pairs have either failed connectivity checks or have lost consent.
+
+ disconnected
+ The ICE Agent has determined that connectivity is currently lost for this
+ RTCIceTransport . This is more aggressive than failed, and may trigger
+ intermittently (and resolve itself without action) on a flaky network.
+ The way this state is determined is implementation dependent.
+
+ Examples include:
+ Losing the network interface for the connection in use.
+ Repeatedly failing to receive a response to STUN requests.
+
+ Alternatively, the RTCIceTransport has finished checking all existing
+ candidates pairs and failed to find a connection (or consent checks
+ [RFC7675] once successful, have now failed), but it is still gathering
+ and/or waiting for additional remote candidates.
+
+ closed
+ The RTCIceTransport has shut down and is no longer responding to STUN requests.
+ */
+
+for (let bundle_policy of ['balanced', 'max-bundle', 'max-compat']) {
+
+
+ promise_test(async t => {
+ const caller = new RTCPeerConnection({bundlePolicy: bundle_policy});
+ t.add_cleanup(() => caller.close());
+ const stream = await getNoiseStream(
+ {audio: true, video:true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const [track1, track2] = stream.getTracks();
+ const sender1 = caller.addTrack(track1);
+ const sender2 = caller.addTrack(track2);
+ caller.createDataChannel('datachannel');
+ const callee = new RTCPeerConnection();
+ t.add_cleanup(() => callee.close());
+ exchangeIceCandidates(caller, callee);
+ const offer = await caller.createOffer();
+ await caller.setLocalDescription(offer);
+ const [caller_transceiver1, caller_transceiver2] = caller.getTransceivers();
+ assert_equals(sender1.transport, caller_transceiver1.sender.transport);
+ await callee.setRemoteDescription(offer);
+ const [callee_transceiver1, callee_transceiver2] = callee.getTransceivers();
+ const answer = await callee.createAnswer();
+ await callee.setLocalDescription(answer);
+ await caller.setRemoteDescription(answer);
+ // At this point, we should have a single ICE transport, and it
+ // should eventually get to the "connected" state.
+ await waitForState(caller_transceiver1.receiver.transport.iceTransport,
+ 'connected');
+ // The PeerConnection's iceConnectionState should therefore be 'connected'
+ assert_equals(caller.iceConnectionState, 'connected',
+ 'PC.iceConnectionState:');
+ }, 'iceConnectionState changes at the right time, with bundle policy ' +
+ bundle_policy);
+}
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+ pc1.onicecandidate = e => pc2.addIceCandidate(e.candidate);
+ pc1.candidateBuffer = [];
+ pc2.onicecandidate = e => {
+ // Don't add candidate if candidate buffer is already used
+ if (pc1.candidateBuffer) {
+ pc1.candidateBuffer.push(e.candidate)
+ }
+ };
+ pc1.iceStates = [pc1.iceConnectionState];
+ pc2.iceStates = [pc2.iceConnectionState];
+ pc1.oniceconnectionstatechange = () => {
+ pc1.iceStates.push(pc1.iceConnectionState);
+ };
+ pc2.oniceconnectionstatechange = () => {
+ pc2.iceStates.push(pc2.iceConnectionState);
+ };
+
+ const localStream = await getNoiseStream({audio: true, video: true});
+ const localStream2 = await getNoiseStream({audio: true, video: true});
+ const remoteStream = await getNoiseStream({audio: true, video: true});
+ for (const stream of [localStream, localStream2, remoteStream]) {
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ }
+ localStream.getTracks().forEach(t => pc1.addTrack(t, localStream));
+ localStream2.getTracks().forEach(t => pc1.addTrack(t, localStream2));
+ remoteStream.getTracks().forEach(t => pc2.addTrack(t, remoteStream));
+ const offer = await pc1.createOffer();
+ await pc2.setRemoteDescription(offer);
+ await pc1.setLocalDescription(offer);
+ const answer = await pc2.createAnswer();
+ await pc2.setLocalDescription(answer);
+ await pc1.setRemoteDescription(answer);
+ pc1.candidateBuffer.forEach(c => pc1.addIceCandidate(c));
+ delete pc1.candidateBuffer;
+ await listenToIceConnected(pc1);
+ await listenToIceConnected(pc2);
+ // While we're waiting for pc2, pc1 may or may not have transitioned
+ // to "completed" state, so allow for both cases.
+ if (pc1.iceStates.length == 3) {
+ assert_array_equals(pc1.iceStates, ['new', 'checking', 'connected']);
+ } else {
+ assert_array_equals(pc1.iceStates, ['new', 'checking', 'connected',
+ 'completed']);
+ }
+ assert_array_equals(pc2.iceStates, ['new', 'checking', 'connected']);
+}, 'Responder ICE connection state behaves as expected');
+
+/*
+ Test case for step 11 of PeerConnection.close().
+ ...
+ 11. Set connection's ICE connection state to "closed". This does not invoke
+ the "update the ICE connection state" procedure, and does not fire any
+ event.
+ ...
+*/
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ const stream = await getNoiseStream({ audio: true });
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+
+ stream.getTracks().forEach(track => pc1.addTrack(track, stream));
+ exchangeIceCandidates(pc1, pc2);
+ exchangeOfferAnswer(pc1, pc2);
+ await listenToIceConnected(pc2);
+
+ pc2.oniceconnectionstatechange = t.unreached_func();
+ pc2.close();
+ assert_equals(pc2.iceConnectionState, 'closed');
+ await new Promise(r => t.step_timeout(r, 100));
+}, 'Closing a PeerConnection should not fire iceconnectionstatechange event');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-iceGatheringState.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-iceGatheringState.html
new file mode 100644
index 0000000000..6afaf0fbfb
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-iceGatheringState.html
@@ -0,0 +1,244 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerConnection.prototype.iceGatheringState</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="RTCPeerConnection-helper.js"></script>
+<script>
+ 'use strict';
+
+ // Test is based on the following editor draft:
+ // https://w3c.github.io/webrtc-pc/archives/20170605/webrtc.html
+
+ // The following helper functions are called from RTCPeerConnection-helper.js:
+ // exchangeAnswer
+ // exchangeIceCandidates
+ // generateAudioReceiveOnlyOffer
+
+ /*
+ 4.3.2. Interface Definition
+ interface RTCPeerConnection : EventTarget {
+ ...
+ readonly attribute RTCIceGatheringState iceGatheringState;
+ attribute EventHandler onicegatheringstatechange;
+ };
+
+ 4.4.2. RTCIceGatheringState Enum
+ enum RTCIceGatheringState {
+ "new",
+ "gathering",
+ "complete"
+ };
+
+ 5.6. RTCIceTransport Interface
+ interface RTCIceTransport {
+ readonly attribute RTCIceGathererState gatheringState;
+ ...
+ };
+
+ enum RTCIceGathererState {
+ "new",
+ "gathering",
+ "complete"
+ };
+ */
+
+ /*
+ 4.4.2. RTCIceGatheringState Enum
+ new
+ Any of the RTCIceTransport s are in the new gathering state and
+ none of the transports are in the gathering state, or there are
+ no transports.
+ */
+ test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ assert_equals(pc.iceGatheringState, 'new');
+ }, 'Initial iceGatheringState should be new');
+
+ async_test(t => {
+ const pc = new RTCPeerConnection();
+
+ t.add_cleanup(() => pc.close());
+
+ let reachedGathering = false;
+ const onIceGatheringStateChange = t.step_func(() => {
+ const { iceGatheringState } = pc;
+
+ if(iceGatheringState === 'gathering') {
+ reachedGathering = true;
+ } else if(iceGatheringState === 'complete') {
+ assert_true(reachedGathering, 'iceGatheringState should reach gathering before complete');
+ t.done();
+ }
+ });
+
+ assert_equals(pc.onicegatheringstatechange, null,
+ 'Expect connection to have icegatheringstatechange event');
+ assert_equals(pc.iceGatheringState, 'new');
+
+ pc.addEventListener('icegatheringstatechange', onIceGatheringStateChange);
+
+ generateAudioReceiveOnlyOffer(pc)
+ .then(offer => pc.setLocalDescription(offer))
+ .then(err => t.step_func(err =>
+ assert_unreached(`Unhandled rejection ${err.name}: ${err.message}`)));
+ }, 'iceGatheringState should eventually become complete after setLocalDescription');
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+ pc1.addTransceiver('audio', { direction: 'recvonly' });
+ await initialOfferAnswerWithIceGatheringStateTransitions(
+ pc1, pc2);
+
+ expectNoMoreGatheringStateChanges(t, pc1);
+ expectNoMoreGatheringStateChanges(t, pc2);
+
+ await pc1.setLocalDescription(await pc1.createOffer());
+ await pc2.setLocalDescription(await pc2.createOffer());
+
+ await new Promise(r => t.step_timeout(r, 500));
+ }, 'setLocalDescription(reoffer) with no new transports should not cause iceGatheringState to change');
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+
+ expectNoMoreGatheringStateChanges(t, pc1);
+
+ await pc1.setLocalDescription(await pc1.createOffer());
+
+ await new Promise(r => t.step_timeout(r, 500));
+ }, 'setLocalDescription() with no transports should not cause iceGatheringState to change');
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+ pc1.addTransceiver('audio', { direction: 'recvonly' });
+ await initialOfferAnswerWithIceGatheringStateTransitions(
+ pc1, pc2);
+ await pc1.setLocalDescription(await pc1.createOffer({iceRestart: true}));
+ await iceGatheringStateTransitions(pc1, 'gathering', 'complete');
+ }, 'setLocalDescription(reoffer) with a new transport should cause iceGatheringState to go to "checking" and then "complete"');
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+ expectNoMoreGatheringStateChanges(t, pc2);
+ pc1.addTransceiver('audio', { direction: 'recvonly' });
+ const offer = await pc1.createOffer();
+ await pc2.setRemoteDescription(offer);
+ await pc2.setRemoteDescription(offer);
+ await pc2.setRemoteDescription({type: 'rollback'});
+ await pc2.setRemoteDescription(offer);
+ }, 'sRD does not cause ICE gathering state changes');
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+ pc1.addTransceiver('audio', { direction: 'recvonly' });
+ await initialOfferAnswerWithIceGatheringStateTransitions(
+ pc1, pc2);
+
+ const pc1waiter = iceGatheringStateTransitions(pc1, 'new');
+ const pc2waiter = iceGatheringStateTransitions(pc2, 'new');
+ pc1.getTransceivers()[0].stop();
+ await pc1.setLocalDescription(await pc1.createOffer());
+ await pc2.setRemoteDescription(pc1.localDescription);
+ await pc2.setLocalDescription(await pc2.createAnswer());
+ assert_equals(pc2.getTransceivers().length, 0,
+ 'PC2 transceivers should be invisible after negotiation');
+ assert_equals(pc2.iceGatheringState, 'new');
+ await pc2waiter;
+ await pc1.setRemoteDescription(pc2.localDescription);
+ assert_equals(pc1.getTransceivers().length, 0,
+ 'PC1 transceivers should be invisible after negotiation');
+ assert_equals(pc1.iceGatheringState, 'new');
+ await pc1waiter;
+ }, 'renegotiation that closes all transports should result in ICE gathering state "new"');
+
+ /*
+ 4.3.2. RTCIceGatheringState Enum
+ new
+ Any of the RTCIceTransports are in the "new" gathering state and none
+ of the transports are in the "gathering" state, or there are no
+ transports.
+
+ gathering
+ Any of the RTCIceTransport s are in the gathering state.
+
+ complete
+ At least one RTCIceTransport exists, and all RTCIceTransports are
+ in the completed gathering state.
+
+ 5.6. RTCIceGathererState
+ gathering
+ The RTCIceTransport is in the process of gathering candidates.
+
+ complete
+ The RTCIceTransport has completed gathering and the end-of-candidates
+ indication for this transport has been sent. It will not gather candidates
+ again until an ICE restart causes it to restart.
+ */
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+
+ t.add_cleanup(() => pc2.close());
+
+ const onIceGatheringStateChange = t.step_func(() => {
+ const { iceGatheringState } = pc2;
+
+ if(iceGatheringState === 'gathering') {
+ const iceTransport = pc2.sctp.transport.iceTransport;
+
+ assert_equals(iceTransport.gatheringState, 'gathering',
+ 'Expect ICE transport to be in gathering gatheringState when iceGatheringState is gathering');
+
+ } else if(iceGatheringState === 'complete') {
+ const iceTransport = pc2.sctp.transport.iceTransport;
+
+ assert_equals(iceTransport.gatheringState, 'complete',
+ 'Expect ICE transport to be in complete gatheringState when iceGatheringState is complete');
+
+ t.done();
+ }
+ });
+
+ pc1.createDataChannel('test');
+
+ // Spec bug w3c/webrtc-pc#1382
+ // Because sctp is only defined when answer is set, we listen
+ // to pc2 so that we can be confident that sctp is defined
+ // when icegatheringstatechange event is fired.
+ pc2.addEventListener('icegatheringstatechange', onIceGatheringStateChange);
+
+
+ exchangeIceCandidates(pc1, pc2);
+
+ await pc1.setLocalDescription();
+ assert_equals(pc1.sctp.transport.iceTransport.gatheringState, 'new');
+ await pc2.setRemoteDescription(pc1.localDescription);
+
+ await exchangeAnswer(pc1, pc2);
+ }, 'connection with one data channel should eventually have connected connection state');
+
+ /*
+ TODO
+ 5.6. RTCIceTransport Interface
+ new
+ The RTCIceTransport was just created, and has not started gathering
+ candidates yet.
+ */
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-mandatory-getStats.https.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-mandatory-getStats.https.html
new file mode 100644
index 0000000000..099fba8eaf
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-mandatory-getStats.https.html
@@ -0,0 +1,277 @@
+<!doctype html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<title>Mandatory-to-implement stats compliance (a subset of webrtc-stats)</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src="RTCPeerConnection-helper.js"></script>
+<script src="dictionary-helper.js"></script>
+<script src="RTCStats-helper.js"></script>
+<script>
+'use strict';
+
+// From https://w3c.github.io/webrtc-pc/#mandatory-to-implement-stats
+
+const mandatory = {
+ RTCRtpStreamStats: [
+ "ssrc",
+ "kind",
+ "transportId",
+ "codecId",
+ ],
+ RTCReceivedRtpStreamStats: [
+ "packetsReceived",
+ "packetsLost",
+ "jitter",
+ ],
+ RTCInboundRtpStreamStats: [
+ "trackIdentifier",
+ "remoteId",
+ "framesDecoded",
+ "framesDropped",
+ "nackCount",
+ "framesReceived",
+ "bytesReceived",
+ "totalAudioEnergy",
+ "totalSamplesDuration",
+ "packetsDiscarded",
+ ],
+ RTCRemoteInboundRtpStreamStats: [
+ "localId",
+ "roundTripTime",
+ ],
+ RTCSentRtpStreamStats: [
+ "packetsSent",
+ "bytesSent"
+ ],
+ RTCOutboundRtpStreamStats: [
+ "remoteId",
+ "framesEncoded",
+ "nackCount",
+ "framesSent"
+ ],
+ RTCRemoteOutboundRtpStreamStats: [
+ "localId",
+ "remoteTimestamp",
+ ],
+ RTCPeerConnectionStats: [
+ "dataChannelsOpened",
+ "dataChannelsClosed",
+ ],
+ RTCDataChannelStats: [
+ "label",
+ "protocol",
+ "dataChannelIdentifier",
+ "state",
+ "messagesSent",
+ "bytesSent",
+ "messagesReceived",
+ "bytesReceived",
+ ],
+ RTCMediaSourceStats: [
+ "trackIdentifier",
+ "kind"
+ ],
+ RTCAudioSourceStats: [
+ "totalAudioEnergy",
+ "totalSamplesDuration"
+ ],
+ RTCVideoSourceStats: [
+ "width",
+ "height",
+ "framesPerSecond"
+ ],
+ RTCCodecStats: [
+ "payloadType",
+ /* codecType is part of MTI but is not systematically set
+ per https://www.w3.org/TR/webrtc-stats/#dom-rtccodecstats-codectype
+ If the dictionary member is not present, it means that
+ this media format can be both encoded and decoded. */
+ // "codecType",
+ "mimeType",
+ "clockRate",
+ "channels",
+ "sdpFmtpLine",
+ ],
+ RTCTransportStats: [
+ "bytesSent",
+ "bytesReceived",
+ "selectedCandidatePairId",
+ "localCertificateId",
+ "remoteCertificateId",
+ ],
+ RTCIceCandidatePairStats: [
+ "transportId",
+ "localCandidateId",
+ "remoteCandidateId",
+ "state",
+ "nominated",
+ "bytesSent",
+ "bytesReceived",
+ "totalRoundTripTime",
+ "currentRoundTripTime"
+ ],
+ RTCIceCandidateStats: [
+ "address",
+ "port",
+ "protocol",
+ "candidateType",
+ "url",
+ ],
+ RTCCertificateStats: [
+ "fingerprint",
+ "fingerprintAlgorithm",
+ "base64Certificate",
+ /* issuerCertificateId is part of MTI but is not systematically set
+ per https://www.w3.org/TR/webrtc-stats/#dom-rtccertificatestats-issuercertificateid
+ If the current certificate is at the end of the chain
+ (i.e. a self-signed certificate), this will not be set. */
+ // "issuerCertificateId",
+ ],
+};
+
+// From https://w3c.github.io/webrtc-stats/webrtc-stats.html#rtcstatstype-str*
+
+const dictionaryNames = {
+ "codec": "RTCCodecStats",
+ "inbound-rtp": "RTCInboundRtpStreamStats",
+ "outbound-rtp": "RTCOutboundRtpStreamStats",
+ "remote-inbound-rtp": "RTCRemoteInboundRtpStreamStats",
+ "remote-outbound-rtp": "RTCRemoteOutboundRtpStreamStats",
+ "csrc": "RTCRtpContributingSourceStats",
+ "peer-connection": "RTCPeerConnectionStats",
+ "data-channel": "RTCDataChannelStats",
+ "media-source": {
+ audio: "RTCAudioSourceStats",
+ video: "RTCVideoSourceStats"
+ },
+ "track": {
+ video: "RTCSenderVideoTrackAttachmentStats",
+ audio: "RTCSenderAudioTrackAttachmentStats"
+ },
+ "sender": {
+ audio: "RTCAudioSenderStats",
+ video: "RTCVideoSenderStats"
+ },
+ "receiver": {
+ audio: "RTCAudioReceiverStats",
+ video: "RTCVideoReceiverStats",
+ },
+ "transport": "RTCTransportStats",
+ "candidate-pair": "RTCIceCandidatePairStats",
+ "local-candidate": "RTCIceCandidateStats",
+ "remote-candidate": "RTCIceCandidateStats",
+ "certificate": "RTCCertificateStats",
+};
+
+// From https://w3c.github.io/webrtc-stats/webrtc-stats.html (webidl)
+
+const parents = {
+ RTCVideoSourceStats: "RTCMediaSourceStats",
+ RTCAudioSourceStats: "RTCMediaSourceStats",
+ RTCReceivedRtpStreamStats: "RTCRtpStreamStats",
+ RTCInboundRtpStreamStats: "RTCReceivedRtpStreamStats",
+ RTCRemoteInboundRtpStreamStats: "RTCReceivedRtpStreamStats",
+ RTCSentRtpStreamStats: "RTCRtpStreamStats",
+ RTCOutboundRtpStreamStats: "RTCSentRtpStreamStats",
+ RTCRemoteOutboundRtpStreamStats : "RTCSentRtpStreamStats",
+};
+
+const remaining = JSON.parse(JSON.stringify(mandatory));
+for (const dictName in remaining) {
+ remaining[dictName] = new Set(remaining[dictName]);
+}
+
+async function getAllStats(t, pc) {
+ // Try to obtain as many stats as possible, waiting up to 20 seconds for
+ // roundTripTime of RTCRemoteInboundRtpStreamStats and
+ // remoteTimestamp of RTCRemoteOutboundRtpStreamStats which can take
+ // several RTCP messages to calculate.
+ let stats;
+ let remoteInboundFound = false;
+ let remoteOutboundFound = false;
+ for (let i = 0; i < 20; i++) {
+ stats = await pc.getStats();
+ const values = [...stats.values()];
+ const [remoteInboundAudio, remoteInboundVideo] = ["audio", "video"].map(
+ kind => values.find(s =>
+ s.type == "remote-inbound-rtp" && s.kind == kind));
+ if (remoteInboundAudio && "roundTripTime" in remoteInboundAudio &&
+ remoteInboundVideo && "roundTripTime" in remoteInboundVideo) {
+ remoteInboundFound = true;
+ }
+ const [remoteOutboundAudio, remoteOutboundVideo] = ["audio", "video"].map(
+ kind => values.find(s =>
+ s.type == "remote-outbound-rtp" && s.kind == kind));
+ if (remoteOutboundAudio && "remoteTimestamp" in remoteOutboundAudio &&
+ remoteOutboundVideo && "remoteTimestamp" in remoteOutboundVideo) {
+ remoteOutboundFound = true;
+ }
+ if (remoteInboundFound && remoteOutboundFound) {
+ return stats;
+ }
+ await new Promise(r => t.step_timeout(r, 1000));
+ }
+ return stats;
+}
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const dc1 = pc1.createDataChannel("dummy", {negotiated: true, id: 0});
+ const dc2 = pc2.createDataChannel("dummy", {negotiated: true, id: 0});
+
+ const stream = await getNoiseStream({video: true, audio:true});
+ for (const track of stream.getTracks()) {
+ pc1.addTrack(track, stream);
+ pc2.addTrack(track, stream);
+ t.add_cleanup(() => track.stop());
+ }
+ exchangeIceCandidates(pc1, pc2);
+ await exchangeOfferAnswer(pc1, pc2);
+ const stats = await getAllStats(t, pc1);
+
+ // The focus of this test is not API correctness, but rather to provide an
+ // accessible metric of implementation progress by dictionary member. We count
+ // whether we've seen each dictionary's mandatory members in getStats().
+
+ test(t => {
+ for (const stat of stats.values()) {
+ let dictName = dictionaryNames[stat.type];
+ if (!dictName) continue;
+ if (typeof dictName == "object") {
+ dictName = dictName[stat.kind];
+ }
+
+ assert_equals(typeof dictName, "string", "Test error. String.");
+ if (dictName && mandatory[dictName]) {
+ do {
+ const memberNames = mandatory[dictName];
+ const remainingNames = remaining[dictName];
+ assert_true(memberNames.length > 0, "Test error. Parent not found.");
+ for (const memberName of memberNames) {
+ if (memberName in stat) {
+ assert_not_equals(stat[memberName], undefined, "Not undefined");
+ remainingNames.delete(memberName);
+ }
+ }
+ dictName = parents[dictName];
+ } while (dictName);
+ }
+ }
+ }, "Validating stats");
+
+ for (const dictName in mandatory) {
+ for (const memberName of mandatory[dictName]) {
+ test(t => {
+ assert_true(!remaining[dictName].has(memberName),
+ `Is ${memberName} present`);
+ }, `${dictName}'s ${memberName}`);
+ }
+ }
+}, 'getStats succeeds');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-ondatachannel.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-ondatachannel.html
new file mode 100644
index 0000000000..08f206fb02
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-ondatachannel.html
@@ -0,0 +1,374 @@
+<!doctype html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<title>RTCPeerConnection.prototype.ondatachannel</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="RTCPeerConnection-helper.js"></script>
+<script>
+'use strict';
+
+// Test is based on the following revision:
+// https://rawgit.com/w3c/webrtc-pc/1cc5bfc3ff18741033d804c4a71f7891242fb5b3/webrtc.html
+
+// The following helper functions are called from RTCPeerConnection-helper.js:
+// exchangeIceCandidates
+// exchangeOfferAnswer
+// createDataChannelPair
+
+/*
+ 6.2. RTCDataChannel
+ When an underlying data transport is to be announced (the other peer created a channel with
+ negotiated unset or set to false), the user agent of the peer that did not initiate the
+ creation process MUST queue a task to run the following steps:
+ 2. Let channel be a newly created RTCDataChannel object.
+ 7. Set channel's [[ReadyState]] to open (but do not fire the open event, yet).
+ 8. Fire a datachannel event named datachannel with channel at the RTCPeerConnection object.
+
+ 6.3. RTCDataChannelEvent
+ Firing a datachannel event named e with an RTCDataChannel channel means that an event with the
+ name e, which does not bubble (except where otherwise stated) and is not cancelable (except
+ where otherwise stated), and which uses the RTCDataChannelEvent interface with the channel
+ attribute set to channel, MUST be created and dispatched at the given target.
+
+ interface RTCDataChannelEvent : Event {
+ readonly attribute RTCDataChannel channel;
+ };
+ */
+promise_test(async (t) => {
+ const resolver = new Resolver();
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+
+ let eventCount = 0;
+
+ pc2.ondatachannel = t.step_func((event) => {
+ eventCount++;
+ assert_equals(eventCount, 1,
+ 'Expect data channel event to fire exactly once');
+
+ assert_true(event instanceof RTCDataChannelEvent,
+ 'Expect event to be instance of RTCDataChannelEvent');
+
+ assert_equals(event.bubbles, false);
+ assert_equals(event.cancelable, false);
+
+ const dc = event.channel;
+ assert_true(dc instanceof RTCDataChannel,
+ 'Expect channel to be instance of RTCDataChannel');
+
+ // The channel should be in the 'open' state already.
+ // See: https://github.com/w3c/webrtc-pc/pull/1851
+ assert_equals(dc.readyState, 'open',
+ 'Expect channel ready state to be open');
+
+ resolver.resolve();
+ });
+
+ pc1.createDataChannel('fire-me!');
+
+ exchangeIceCandidates(pc1, pc2);
+ await exchangeOfferAnswer(pc1, pc2);
+
+ await resolver;
+}, 'Data channel event should fire when new data channel is announced to the remote peer');
+
+/*
+ Since the channel should be in the 'open' state when dispatching via the 'datachannel' event,
+ we should be able to send data in the event handler.
+ */
+promise_test(async (t) => {
+ const resolver = new Resolver();
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+
+ const message = 'meow meow!';
+
+ pc2.ondatachannel = t.step_func((event) => {
+ const dc2 = event.channel;
+ dc2.send(message);
+ });
+
+ const dc1 = pc1.createDataChannel('fire-me!');
+ dc1.onmessage = t.step_func((event) => {
+ assert_equals(event.data, message,
+ 'Received data should be equal to sent data');
+
+ resolver.resolve();
+ });
+
+ exchangeIceCandidates(pc1, pc2);
+ await exchangeOfferAnswer(pc1, pc2);
+
+ await resolver;
+}, 'Should be able to send data in a datachannel event handler');
+
+/*
+ 6.2. RTCDataChannel
+ When an underlying data transport is to be announced (the other peer created a channel with
+ negotiated unset or set to false), the user agent of the peer that did not initiate the
+ creation process MUST queue a task to run the following steps:
+ 8. Fire a datachannel event named datachannel with channel at the RTCPeerConnection object.
+ 9. If the channel's [[ReadyState]] is still open, announce the data channel as open.
+ */
+promise_test(async (t) => {
+ const resolver = new Resolver();
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+
+ pc2.ondatachannel = t.step_func((event) => {
+ const dc = event.channel;
+ dc.onopen = t.step_func(() => {
+ assert_unreached('Open event should not fire');
+ });
+
+ // This should prevent triggering the 'open' event
+ dc.close();
+
+ // Wait a bit to ensure the 'open' event does NOT fire
+ t.step_timeout(() => resolver.resolve(), 500);
+ });
+
+ pc1.createDataChannel('fire-me!');
+
+ exchangeIceCandidates(pc1, pc2);
+ await exchangeOfferAnswer(pc1, pc2);
+
+ await resolver;
+}, 'Open event should not be raised when closing the channel in the datachannel event');
+
+// Added this test as a result of the discussion in
+// https://github.com/w3c/webrtc-pc/pull/1851#discussion_r185976747
+promise_test(async (t) => {
+ const resolver = new Resolver();
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+
+ pc2.ondatachannel = t.step_func((event) => {
+ const dc = event.channel;
+ dc.onopen = t.step_func((event) => {
+ resolver.resolve();
+ });
+
+ // This should NOT prevent triggering the 'open' event since it enqueues at least two tasks
+ t.step_timeout(() => {
+ t.step_timeout(() => {
+ dc.close()
+ }, 1);
+ }, 1);
+ });
+
+ pc1.createDataChannel('fire-me!');
+
+ exchangeIceCandidates(pc1, pc2);
+ await exchangeOfferAnswer(pc1, pc2);
+
+ await resolver;
+}, 'Open event should be raised when closing the channel in the datachannel event after ' +
+ 'enqueuing a task');
+
+
+/*
+ Combination of the two tests above (send and close).
+ */
+promise_test(async (t) => {
+ const resolver = new Resolver();
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+
+ const message = 'meow meow!';
+
+ pc2.ondatachannel = t.step_func((event) => {
+ const dc2 = event.channel;
+ dc2.onopen = t.step_func(() => {
+ assert_unreached('Open event should not fire');
+ });
+
+ // This should send but still prevent triggering the 'open' event
+ dc2.send(message);
+ dc2.close();
+ });
+
+ const dc1 = pc1.createDataChannel('fire-me!');
+ dc1.onmessage = t.step_func((event) => {
+ assert_equals(event.data, message,
+ 'Received data should be equal to sent data');
+
+ resolver.resolve();
+ });
+
+ exchangeIceCandidates(pc1, pc2);
+ await exchangeOfferAnswer(pc1, pc2);
+
+ await resolver;
+}, 'Open event should not be raised when sending and immediately closing the channel in the ' +
+ 'datachannel event');
+
+/*
+ 6.2. RTCDataChannel
+
+ interface RTCDataChannel : EventTarget {
+ readonly attribute USVString label;
+ readonly attribute boolean ordered;
+ readonly attribute unsigned short? maxPacketLifeTime;
+ readonly attribute unsigned short? maxRetransmits;
+ readonly attribute USVString protocol;
+ readonly attribute boolean negotiated;
+ readonly attribute unsigned short? id;
+ readonly attribute RTCDataChannelState readyState;
+ ...
+ };
+
+ When an underlying data transport is to be announced (the other peer created a channel with
+ negotiated unset or set to false), the user agent of the peer that did not initiate the
+ creation process MUST queue a task to run the following steps:
+ 2. Let channel be a newly created RTCDataChannel object.
+ 3. Let configuration be an information bundle received from the other peer as a part of the
+ process to establish the underlying data transport described by the WebRTC DataChannel
+ Protocol specification [RTCWEB-DATA-PROTOCOL].
+ 4. Initialize channel's [[DataChannelLabel]], [[Ordered]], [[MaxPacketLifeTime]],
+ [[MaxRetransmits]], [[DataChannelProtocol]], and [[DataChannelId]] internal slots to the
+ corresponding values in configuration.
+ 5. Initialize channel's [[Negotiated]] internal slot to false.
+ 7. Set channel's [[ReadyState]] slot to connecting.
+ 8. Fire a datachannel event named datachannel with channel at the RTCPeerConnection object.
+
+ Note: More exhaustive tests are defined in RTCDataChannel-dcep
+ */
+
+promise_test(async (t) => {
+ const resolver = new Resolver();
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+
+ const dc1 = pc1.createDataChannel('test', {
+ ordered: false,
+ maxRetransmits: 1,
+ protocol: 'custom'
+ });
+
+ assert_equals(dc1.label, 'test');
+ assert_equals(dc1.ordered, false);
+ assert_equals(dc1.maxPacketLifeTime, null);
+ assert_equals(dc1.maxRetransmits, 1);
+ assert_equals(dc1.protocol, 'custom');
+ assert_equals(dc1.negotiated, false);
+
+ pc2.ondatachannel = t.step_func((event) => {
+ const dc2 = event.channel;
+ assert_true(dc2 instanceof RTCDataChannel,
+ 'Expect channel to be instance of RTCDataChannel');
+
+ assert_equals(dc2.label, 'test');
+ assert_equals(dc2.ordered, false);
+ assert_equals(dc2.maxPacketLifeTime, null);
+ assert_equals(dc2.maxRetransmits, 1);
+ assert_equals(dc2.protocol, 'custom');
+ assert_equals(dc2.negotiated, false);
+ assert_equals(dc2.id, dc1.id);
+
+ resolver.resolve();
+ });
+
+ exchangeIceCandidates(pc1, pc2);
+ await exchangeOfferAnswer(pc1, pc2);
+
+ await resolver;
+}, 'In-band negotiated channel created on remote peer should match the same configuration as local ' +
+ 'peer');
+
+promise_test(async (t) => {
+ const resolver = new Resolver();
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+
+ const dc1 = pc1.createDataChannel('');
+
+ assert_equals(dc1.label, '');
+ assert_equals(dc1.ordered, true);
+ assert_equals(dc1.maxPacketLifeTime, null);
+ assert_equals(dc1.maxRetransmits, null);
+ assert_equals(dc1.protocol, '');
+ assert_equals(dc1.negotiated, false);
+
+ pc2.ondatachannel = t.step_func((event) => {
+ const dc2 = event.channel;
+ assert_true(dc2 instanceof RTCDataChannel,
+ 'Expect channel to be instance of RTCDataChannel');
+
+ assert_equals(dc2.label, '');
+ assert_equals(dc2.ordered, true);
+ assert_equals(dc2.maxPacketLifeTime, null);
+ assert_equals(dc2.maxRetransmits, null);
+ assert_equals(dc2.protocol, '');
+ assert_equals(dc2.negotiated, false);
+ assert_equals(dc2.id, dc1.id);
+
+ resolver.resolve();
+ });
+
+ exchangeIceCandidates(pc1, pc2);
+ await exchangeOfferAnswer(pc1, pc2);
+
+ await resolver;
+}, 'In-band negotiated channel created on remote peer should match the same (default) ' +
+ 'configuration as local peer');
+
+/*
+ 6.2. RTCDataChannel
+ Dictionary RTCDataChannelInit Members
+ negotiated
+ The default value of false tells the user agent to announce the
+ channel in-band and instruct the other peer to dispatch a corresponding
+ RTCDataChannel object. If set to true, it is up to the application
+ to negotiate the channel and create a RTCDataChannel object with the
+ same id at the other peer.
+ */
+promise_test(async (t) => {
+ const resolver = new Resolver();
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+
+ pc2.ondatachannel = t.unreached_func('datachannel event should not be fired');
+
+ pc1.createDataChannel('test', {
+ negotiated: true,
+ id: 42
+ });
+
+ exchangeIceCandidates(pc1, pc2);
+ await exchangeOfferAnswer(pc1, pc2);
+
+ // Wait a bit to ensure the 'datachannel' event does NOT fire
+ t.step_timeout(() => resolver.resolve(), 500);
+ await resolver;
+}, 'Negotiated channel should not fire datachannel event on remote peer');
+
+/*
+ Non-testable
+ 6.2. RTCDataChannel
+ When an underlying data transport is to be announced
+ 1. If the associated RTCPeerConnection object's [[isClosed]] slot
+ is true, abort these steps.
+
+ The above step is not testable because to reach it we would have to
+ close the peer connection just between receiving the in-band negotiated data
+ channel via DCEP and firing the datachannel event.
+ */
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-onicecandidateerror.https.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-onicecandidateerror.https.html
new file mode 100644
index 0000000000..096cc9dd1a
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-onicecandidateerror.https.html
@@ -0,0 +1,38 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerConnection.prototype.onicecandidateerror</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="RTCPeerConnection-helper.js"></script>
+<script>
+
+promise_test(async t => {
+ const config = {
+ iceServers: [{urls: "turn:123", username: "123", credential: "123"}]
+ };
+ const pc = new RTCPeerConnection(config);
+ t.add_cleanup(() => pc.close());
+ const onErrorPromise = addEventListenerPromise(t, pc, 'icecandidateerror', event => {
+ assert_true(event instanceof RTCPeerConnectionIceErrorEvent,
+ 'Expect event to be instance of RTCPeerConnectionIceErrorEvent');
+ // Do not hardcode any specific errors here. Instead only verify
+ // that all the fields contain something expected.
+ // Testing of event.errorText can be added later once it's content is
+ // specified in spec with more detail.
+ assert_true(event.errorCode >= 300 && event.errorCode <= 799, "errorCode");
+ if (event.port == 0) {
+ assert_equals(event.address, null);
+ } else {
+ assert_true(event.address.includes(".") || event.address.includes(":"));
+ }
+ assert_true(event.url.includes("123"), "url");
+ });
+ const stream = await getNoiseStream({audio:true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ pc.addTrack(stream.getAudioTracks()[0], stream);
+
+ await pc.setLocalDescription(await pc.createOffer());
+ await onErrorPromise;
+}, 'Surfacing onicecandidateerror');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-onnegotiationneeded.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-onnegotiationneeded.html
new file mode 100644
index 0000000000..6ede5ccebf
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-onnegotiationneeded.html
@@ -0,0 +1,627 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>Test RTCPeerConnection.prototype.onnegotiationneeded</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="RTCPeerConnection-helper.js"></script>
+<script>
+ 'use strict';
+
+ // Test is based on the following editor draft:
+ // https://w3c.github.io/webrtc-pc/archives/20170605/webrtc.html
+
+ // The following helper functions are called from RTCPeerConnection-helper.js:
+ // generateOffer
+ // generateAnswer
+ // generateAudioReceiveOnlyOffer
+ // test_never_resolve
+
+ // Listen to the negotiationneeded event on a peer connection
+ // Returns a promise that resolves when the first event is fired.
+ // The resolve result is a dictionary with event and nextPromise,
+ // which resolves when the next negotiationneeded event is fired.
+ // This allow us to promisify the event listening and assert whether
+ // an event is fired or not by testing whether a promise is resolved.
+ function awaitNegotiation(pc) {
+ if(pc.onnegotiationneeded) {
+ throw new Error('connection is already attached with onnegotiationneeded event handler');
+ }
+
+ function waitNextNegotiation() {
+ return new Promise(resolve => {
+ pc.onnegotiationneeded = event => {
+ const nextPromise = waitNextNegotiation();
+ resolve({ nextPromise, event });
+ }
+ });
+ }
+
+ return waitNextNegotiation();
+ }
+
+ // Return a promise that rejects if the first promise is resolved before second promise.
+ // Also rejects when either promise rejects.
+ function assert_first_promise_fulfill_after_second(promise1, promise2, message) {
+ if(!message) {
+ message = 'first promise is resolved before second promise';
+ }
+
+ return new Promise((resolve, reject) => {
+ let secondResolved = false;
+
+ promise1.then(() => {
+ if(secondResolved) {
+ resolve();
+ } else {
+ assert_unreached(message);
+ }
+ })
+ .catch(reject);
+
+ promise2.then(() => {
+ secondResolved = true;
+ }, reject);
+ });
+ }
+
+ /*
+ 4.7.3. Updating the Negotiation-Needed flag
+
+ To update the negotiation-needed flag
+ 5. Set connection's [[needNegotiation]] slot to true.
+ 6. Queue a task that runs the following steps:
+ 3. Fire a simple event named negotiationneeded at connection.
+
+ To check if negotiation is needed
+ 2. If connection has created any RTCDataChannels, and no m= section has
+ been negotiated yet for data, return "true".
+
+ 6.1. RTCPeerConnection Interface Extensions
+
+ createDataChannel
+ 14. If channel was the first RTCDataChannel created on connection,
+ update the negotiation-needed flag for connection.
+ */
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const negotiated = awaitNegotiation(pc);
+
+ pc.createDataChannel('test');
+ return negotiated;
+ }, 'Creating first data channel should fire negotiationneeded event');
+
+ test_never_resolve(t => {
+ const pc = new RTCPeerConnection();
+ const negotiated = awaitNegotiation(pc);
+
+ pc.createDataChannel('foo');
+ return negotiated
+ .then(({nextPromise}) => {
+ pc.createDataChannel('bar');
+ return nextPromise;
+ });
+ }, 'calling createDataChannel twice should fire negotiationneeded event once');
+
+ /*
+ 4.7.3. Updating the Negotiation-Needed flag
+ To check if negotiation is needed
+ 3. For each transceiver t in connection's set of transceivers, perform
+ the following checks:
+ 1. If t isn't stopped and isn't yet associated with an m= section
+ according to [JSEP] (section 3.4.1.), return "true".
+
+ 5.1. RTCPeerConnection Interface Extensions
+ addTransceiver
+ 9. Update the negotiation-needed flag for connection.
+ */
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const negotiated = awaitNegotiation(pc);
+
+ pc.addTransceiver('audio');
+ return negotiated;
+ }, 'addTransceiver() should fire negotiationneeded event');
+
+ /*
+ 4.7.3. Updating the Negotiation-Needed flag
+ To update the negotiation-needed flag
+ 4. If connection's [[needNegotiation]] slot is already true, abort these steps.
+ */
+ test_never_resolve(t => {
+ const pc = new RTCPeerConnection();
+ const negotiated = awaitNegotiation(pc);
+
+ pc.addTransceiver('audio');
+ return negotiated
+ .then(({nextPromise}) => {
+ pc.addTransceiver('video');
+ return nextPromise;
+ });
+ }, 'Calling addTransceiver() twice should fire negotiationneeded event once');
+
+ /*
+ 4.7.3. Updating the Negotiation-Needed flag
+ To update the negotiation-needed flag
+ 4. If connection's [[needNegotiation]] slot is already true, abort these steps.
+ */
+ test_never_resolve(t => {
+ const pc = new RTCPeerConnection();
+ const negotiated = awaitNegotiation(pc);
+
+ pc.createDataChannel('test');
+ return negotiated
+ .then(({nextPromise}) => {
+ pc.addTransceiver('video');
+ return nextPromise;
+ });
+ }, 'Calling both addTransceiver() and createDataChannel() should fire negotiationneeded event once');
+
+ /*
+ 4.7.3. Updating the Negotiation-Needed flag
+ To update the negotiation-needed flag
+ 2. If connection's signaling state is not "stable", abort these steps.
+ */
+ test_never_resolve(t => {
+ const pc = new RTCPeerConnection();
+ let negotiated;
+
+ return generateAudioReceiveOnlyOffer(pc)
+ .then(offer => {
+ pc.setLocalDescription(offer);
+ negotiated = awaitNegotiation(pc);
+ })
+ .then(() => negotiated)
+ .then(({nextPromise}) => {
+ assert_equals(pc.signalingState, 'have-local-offer');
+ pc.createDataChannel('test');
+ return nextPromise;
+ });
+ }, 'negotiationneeded event should not fire if signaling state is not stable');
+
+ /*
+ 4.4.1.6. Set the RTCSessionSessionDescription
+ 2.2.10. If connection's signaling state is now stable, update the negotiation-needed
+ flag. If connection's [[NegotiationNeeded]] slot was true both before and after
+ this update, queue a task that runs the following steps:
+ 2. If connection's [[NegotiationNeeded]] slot is false, abort these steps.
+ 3. Fire a simple event named negotiationneeded at connection.
+ */
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+
+ t.add_cleanup(() => pc.close());
+
+ pc.addTransceiver('audio');
+ await new Promise(resolve => pc.onnegotiationneeded = resolve);
+
+ const offer = await pc.createOffer();
+ await pc.setLocalDescription(offer);
+ let fired = false;
+ pc.onnegotiationneeded = e => fired = true;
+ pc.createDataChannel('test');
+ await pc.setRemoteDescription(await generateAnswer(offer));
+ await undefined;
+ assert_false(fired, "negotiationneeded should not fire until the next iteration of the event loop after SRD success");
+ await new Promise(resolve => pc.onnegotiationneeded = resolve);
+ }, 'negotiationneeded event should fire only after signaling state goes back to stable after setRemoteDescription');
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ pc.addTransceiver('audio');
+ await new Promise(resolve => pc.onnegotiationneeded = resolve);
+
+ let fired = false;
+ pc.onnegotiationneeded = e => fired = true;
+ await pc.setRemoteDescription(await generateOffer());
+ pc.createDataChannel('test');
+ await pc.setLocalDescription(await pc.createAnswer());
+ await undefined;
+ assert_false(fired, "negotiationneeded should not fire until the next iteration of the event loop after SLD success");
+
+ await new Promise(resolve => pc.onnegotiationneeded = resolve);
+ }, 'negotiationneeded event should fire only after signaling state goes back to stable after setLocalDescription');
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+
+ t.add_cleanup(() => pc.close());
+
+ pc.addTransceiver('audio');
+ await new Promise(resolve => pc.onnegotiationneeded = resolve);
+
+ const offer = await pc.createOffer();
+ await pc.setLocalDescription(offer);
+ let fired = false;
+ pc.onnegotiationneeded = e => fired = true;
+ pc.createDataChannel('test');
+ const p = pc.setRemoteDescription(await generateAnswer(offer));
+ await new Promise(resolve => pc.onsignalingstatechange = resolve);
+ assert_false(fired, "negotiationneeded should not fire before signalingstatechange fires");
+ await new Promise(resolve => pc.onnegotiationneeded = resolve);
+ await p;
+ }, 'negotiationneeded event should fire only after signalingstatechange event fires from setRemoteDescription');
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ pc.addTransceiver('audio');
+ await new Promise(resolve => pc.onnegotiationneeded = resolve);
+
+ let fired = false;
+ pc.onnegotiationneeded = e => fired = true;
+ await pc.setRemoteDescription(await generateOffer());
+ pc.createDataChannel('test');
+
+ const p = pc.setLocalDescription(await pc.createAnswer());
+ await new Promise(resolve => pc.onsignalingstatechange = resolve);
+ assert_false(fired, "negotiationneeded should not fire until the next iteration of the event loop after returning to stable");
+ await new Promise(resolve => pc.onnegotiationneeded = resolve);
+ await p;
+ }, 'negotiationneeded event should fire only after signalingstatechange event fires from setLocalDescription');
+
+ /*
+ 5.1. RTCPeerConnection Interface Extensions
+
+ addTrack
+ 10. Update the negotiation-needed flag for connection.
+ */
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const stream = await getNoiseStream({ audio: true });
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const [track] = stream.getTracks();
+ pc.addTrack(track, stream);
+
+ await new Promise(resolve => pc.onnegotiationneeded = resolve);
+ }, 'addTrack should cause negotiationneeded to fire');
+
+ /*
+ 5.1. RTCPeerConnection Interface Extensions
+
+ removeTrack
+ 12. Update the negotiation-needed flag for connection.
+ */
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const stream = await getNoiseStream({ audio: true });
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const [track] = stream.getTracks();
+ const sender = pc.addTrack(track, stream);
+
+ await new Promise(resolve => pc.onnegotiationneeded = resolve);
+ pc.onnegotiationneeded = t.step_func(() => {
+ assert_unreached('onnegotiationneeded misfired');
+ });
+
+ const offer = await pc.createOffer();
+ await pc.setLocalDescription(offer);
+
+ const answer = await generateAnswer(offer);
+ await pc.setRemoteDescription(answer);
+
+ pc.removeTrack(sender);
+ await new Promise(resolve => pc.onnegotiationneeded = resolve)
+ }, 'removeTrack should cause negotiationneeded to fire on the caller');
+
+ /*
+ 5.1. RTCPeerConnection Interface Extensions
+
+ removeTrack
+ 12. Update the negotiation-needed flag for connection.
+ */
+ promise_test(async t => {
+ const caller = new RTCPeerConnection();
+ t.add_cleanup(() => caller.close());
+ caller.addTransceiver('audio', {direction:'recvonly'});
+ const offer = await caller.createOffer();
+
+ const callee = new RTCPeerConnection();
+ t.add_cleanup(() => callee.close());
+
+ const stream = await getNoiseStream({ audio: true });
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const [track] = stream.getTracks();
+ const sender = callee.addTrack(track, stream);
+
+ await new Promise(resolve => callee.onnegotiationneeded = resolve);
+ callee.onnegotiationneeded = t.step_func(() => {
+ assert_unreached('onnegotiationneeded misfired');
+ });
+
+ await callee.setRemoteDescription(offer);
+ const answer = await callee.createAnswer();
+ callee.setLocalDescription(answer);
+
+ callee.removeTrack(sender);
+ await new Promise(resolve => callee.onnegotiationneeded = resolve)
+ }, 'removeTrack should cause negotiationneeded to fire on the callee');
+
+ /*
+ 5.4. RTCRtpTransceiver Interface
+
+ setDirection
+ 7. Update the negotiation-needed flag for connection.
+ */
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const transceiver = pc.addTransceiver('audio', {direction:'sendrecv'});
+ const offer = await pc.createOffer();
+ await pc.setLocalDescription(offer);
+ const answer = await generateAnswer(offer);
+ await pc.setRemoteDescription(answer);
+ transceiver.direction = 'recvonly';
+ await new Promise(resolve => pc.onnegotiationneeded = resolve);
+ }, 'Updating the direction of the transceiver should cause negotiationneeded to fire');
+
+ /*
+ 5.2. RTCRtpSender Interface
+
+ setStreams
+ 7. Update the negotiation-needed flag for connection.
+ */
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const transceiver = pc.addTransceiver('audio', {direction:'sendrecv'});
+ const offer = await pc.createOffer();
+ await pc.setLocalDescription(offer);
+ const answer = await generateAnswer(offer);
+ await pc.setRemoteDescription(answer);
+
+ const stream = new MediaStream();
+ transceiver.sender.setStreams(stream);
+ await new Promise(resolve => pc.onnegotiationneeded = resolve);
+ }, 'Calling setStreams should cause negotiationneeded to fire');
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const transceiver = pc.addTransceiver('audio', {direction:'sendrecv'});
+ const stream = new MediaStream();
+ transceiver.sender.setStreams(stream);
+ await new Promise(resolve => pc.onnegotiationneeded = resolve);
+
+ const offer = await pc.createOffer();
+ await pc.setLocalDescription(offer);
+ const answer = await generateAnswer(offer);
+ await pc.setRemoteDescription(answer);
+
+ const stream2 = new MediaStream();
+ transceiver.sender.setStreams(stream2);
+ await new Promise(resolve => pc.onnegotiationneeded = resolve);
+ }, 'Calling setStreams with a different stream as before should cause negotiationneeded to fire');
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const transceiver = pc.addTransceiver('audio', {direction:'sendrecv'});
+ const stream = new MediaStream();
+ transceiver.sender.setStreams(stream);
+ await new Promise(resolve => pc.onnegotiationneeded = resolve);
+
+ const offer = await pc.createOffer();
+ await pc.setLocalDescription(offer);
+ const answer = await generateAnswer(offer);
+ await pc.setRemoteDescription(answer);
+
+ const stream2 = new MediaStream();
+ transceiver.sender.setStreams(stream, stream2);
+ await new Promise(resolve => pc.onnegotiationneeded = resolve);
+ }, 'Calling setStreams with an additional stream should cause negotiationneeded to fire');
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const transceiver = pc.addTransceiver('audio', {direction:'sendrecv'});
+ const stream1 = new MediaStream();
+ const stream2 = new MediaStream();
+ transceiver.sender.setStreams(stream1, stream2);
+ await new Promise(resolve => pc.onnegotiationneeded = resolve);
+
+ const offer = await pc.createOffer();
+ await pc.setLocalDescription(offer);
+ const answer = await generateAnswer(offer);
+ await pc.setRemoteDescription(answer);
+
+ transceiver.sender.setStreams(stream2);
+ await new Promise(resolve => pc.onnegotiationneeded = resolve);
+ }, 'Calling setStreams with a stream removed should cause negotiationneeded to fire');
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const transceiver = pc.addTransceiver('audio', {direction:'sendrecv'});
+ const stream1 = new MediaStream();
+ const stream2 = new MediaStream();
+ transceiver.sender.setStreams(stream1, stream2);
+ await new Promise(resolve => pc.onnegotiationneeded = resolve);
+
+ const offer = await pc.createOffer();
+ await pc.setLocalDescription(offer);
+ const answer = await generateAnswer(offer);
+ await pc.setRemoteDescription(answer);
+
+ transceiver.sender.setStreams();
+ await new Promise(resolve => pc.onnegotiationneeded = resolve);
+ }, 'Calling setStreams with all streams removed should cause negotiationneeded to fire');
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const transceiver = pc.addTransceiver('audio', {direction:'sendrecv'});
+ const stream = new MediaStream();
+ transceiver.sender.setStreams(stream);
+ await new Promise(resolve => pc.onnegotiationneeded = resolve);
+
+ const offer = await pc.createOffer();
+ await pc.setLocalDescription(offer);
+ const answer = await generateAnswer(offer);
+ await pc.setRemoteDescription(answer);
+
+ transceiver.sender.setStreams(stream);
+ const event = await Promise.race([
+ new Promise(r => pc.onnegotiationneeded = r),
+ new Promise(r => t.step_timeout(r, 10))
+ ]);
+ assert_equals(event, undefined, "No negotiationneeded event");
+ }, 'Calling setStreams with the same stream as before should not cause negotiationneeded to fire');
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const transceiver = pc.addTransceiver('audio', {direction:'sendrecv'});
+ const stream = new MediaStream();
+ transceiver.sender.setStreams(stream);
+ await new Promise(resolve => pc.onnegotiationneeded = resolve);
+
+ const offer = await pc.createOffer();
+ await pc.setLocalDescription(offer);
+ const answer = await generateAnswer(offer);
+ await pc.setRemoteDescription(answer);
+
+ transceiver.sender.setStreams(stream, stream);
+ const event = await Promise.race([
+ new Promise(r => pc.onnegotiationneeded = r),
+ new Promise(r => t.step_timeout(r, 10))
+ ]);
+ assert_equals(event, undefined, "No negotiationneeded event");
+ }, 'Calling setStreams with duplicates of the same stream as before should not cause negotiationneeded to fire');
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const transceiver = pc.addTransceiver('audio', {direction:'sendrecv'});
+ const stream1 = new MediaStream();
+ const stream2 = new MediaStream();
+ transceiver.sender.setStreams(stream1, stream2);
+ await new Promise(resolve => pc.onnegotiationneeded = resolve);
+
+ const offer = await pc.createOffer();
+ await pc.setLocalDescription(offer);
+ const answer = await generateAnswer(offer);
+ await pc.setRemoteDescription(answer);
+
+ transceiver.sender.setStreams(stream2, stream1);
+ const event = await Promise.race([
+ new Promise(r => pc.onnegotiationneeded = r),
+ new Promise(r => t.step_timeout(r, 10))
+ ]);
+ assert_equals(event, undefined, "No negotiationneeded event");
+ }, 'Calling setStreams with the same streams as before in a different order should not cause negotiationneeded to fire');
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const transceiver = pc.addTransceiver('audio', {direction:'sendrecv'});
+ const stream1 = new MediaStream();
+ const stream2 = new MediaStream();
+ transceiver.sender.setStreams(stream1, stream2);
+ await new Promise(resolve => pc.onnegotiationneeded = resolve);
+
+ const offer = await pc.createOffer();
+ await pc.setLocalDescription(offer);
+ const answer = await generateAnswer(offer);
+ await pc.setRemoteDescription(answer);
+
+ transceiver.sender.setStreams(stream1, stream2, stream1);
+ const event = await Promise.race([
+ new Promise(r => pc.onnegotiationneeded = r),
+ new Promise(r => t.step_timeout(r, 10))
+ ]);
+ assert_equals(event, undefined, "No negotiationneeded event");
+ }, 'Calling setStreams with duplicates of the same streams as before should not cause negotiationneeded to fire');
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ let negotiationCount = 0;
+ pc1.onnegotiationneeded = async () => {
+ negotiationCount++;
+ await pc1.setLocalDescription(await pc1.createOffer());
+ await pc2.setRemoteDescription(pc1.localDescription);
+ await pc2.setLocalDescription(await pc2.createAnswer());
+ await pc1.setRemoteDescription(pc2.localDescription);
+ }
+
+ pc1.addTransceiver("video");
+ await new Promise(r => pc1.onsignalingstatechange = () => pc1.signalingState == "stable" && r());
+ pc1.addTransceiver("audio");
+ await new Promise(r => pc1.onsignalingstatechange = () => pc1.signalingState == "stable" && r());
+ assert_equals(negotiationCount, 2);
+ }, 'Adding two transceivers, one at a time, results in the expected number of negotiationneeded events');
+
+ /*
+ TODO
+ 4.7.3. Updating the Negotiation-Needed flag
+
+ To update the negotiation-needed flag
+ 3. If the result of checking if negotiation is needed is "false",
+ clear the negotiation-needed flag by setting connection's
+ [[needNegotiation]] slot to false, and abort these steps.
+ 6. Queue a task that runs the following steps:
+ 2. If connection's [[needNegotiation]] slot is false, abort these steps.
+
+ To check if negotiation is needed
+ 3. For each transceiver t in connection's set of transceivers, perform
+ the following checks:
+ 2. If t isn't stopped and is associated with an m= section according
+ to [JSEP] (section 3.4.1.), then perform the following checks:
+ 1. If t's direction is "sendrecv" or "sendonly", and the
+ associated m= section in connection's currentLocalDescription
+ doesn't contain an "a=msid" line, return "true".
+ 2. If connection's currentLocalDescription if of type "offer",
+ and the direction of the associated m= section in neither the
+ offer nor answer matches t's direction, return "true".
+ 3. If connection's currentLocalDescription if of type "answer",
+ and the direction of the associated m= section in the answer
+ does not match t's direction intersected with the offered
+ direction (as described in [JSEP] (section 5.3.1.)),
+ return "true".
+ 3. If t is stopped and is associated with an m= section according
+ to [JSEP] (section 3.4.1.), but the associated m= section is
+ not yet rejected in connection's currentLocalDescription or
+ currentRemoteDescription , return "true".
+ 4. If all the preceding checks were performed and "true" was not returned,
+ nothing remains to be negotiated; return "false".
+
+ 4.3.1. RTCPeerConnection Operation
+
+ When the RTCPeerConnection() constructor is invoked
+ 7. Let connection have a [[needNegotiation]] internal slot, initialized to false.
+
+ 5.4. RTCRtpTransceiver Interface
+
+ stop
+ 11. Update the negotiation-needed flag for connection.
+
+ Untestable
+ 4.7.3. Updating the Negotiation-Needed flag
+ 1. If connection's [[isClosed]] slot is true, abort these steps.
+ 6. Queue a task that runs the following steps:
+ 1. If connection's [[isClosed]] slot is true, abort these steps.
+ */
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-onsignalingstatechanged.https.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-onsignalingstatechanged.https.html
new file mode 100644
index 0000000000..ad92bf5fc6
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-onsignalingstatechanged.https.html
@@ -0,0 +1,71 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerConnection onsignalingstatechanged</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="RTCPeerConnection-helper.js"></script>
+<script>
+
+promise_test(async t => {
+ const [track] = (await getNoiseStream({video: true})).getTracks();
+ t.add_cleanup(() => track.stop());
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+ pc1.addTrack(track, new MediaStream());
+ await pc1.setLocalDescription(await pc1.createOffer());
+ const events = [];
+ pc2.onsignalingstatechange = t.step_func(e => {
+ const [transceiver] = pc2.getTransceivers();
+ assert_equals(transceiver.currentDirection, null);
+ events.push(pc2.signalingState);
+ });
+ await pc2.setRemoteDescription(pc1.localDescription);
+ assert_equals(events.length, 1, "event fired");
+ assert_equals(events[0], "have-remote-offer");
+
+ pc2.onsignalingstatechange = t.step_func(e => {
+ const [transceiver] = pc2.getTransceivers();
+ assert_equals(transceiver.currentDirection, "recvonly");
+ events.push(pc2.signalingState);
+ });
+ await pc2.setLocalDescription(await pc2.createAnswer());
+ assert_equals(events.length, 2, "event fired");
+ assert_equals(events[1], "stable");
+}, 'Negotiation methods fire signalingstatechange events');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ const stream = await getNoiseStream({ audio: true });
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+
+ stream.getTracks().forEach(track => pc1.addTrack(track, stream));
+ exchangeIceCandidates(pc1, pc2);
+ exchangeOfferAnswer(pc1, pc2);
+ await listenToIceConnected(pc2);
+
+ pc2.onsignalingstatechange = t.unreached_func();
+ pc2.close();
+ assert_equals(pc2.signalingState, 'closed');
+ await new Promise(r => t.step_timeout(r, 100));
+}, 'Closing a PeerConnection should not fire signalingstatechange event');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ pc2.addTransceiver('video');
+
+ pc1.ontrack = t.unreached_func();
+ pc1.onsignalingstatechange = t.step_func(e => {
+ pc1.ontrack = null;
+ });
+ await pc1.setRemoteDescription(await pc2.createOffer());
+}, 'signalingstatechange is the first event to fire');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-ontrack.https.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-ontrack.https.html
new file mode 100644
index 0000000000..ccdd29f6a5
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-ontrack.https.html
@@ -0,0 +1,258 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerConnection.prototype.ontrack</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="RTCPeerConnection-helper.js"></script>
+<script>
+ 'use strict';
+
+ // Test is based on the following editor draft:
+ // https://w3c.github.io/webrtc-pc/archives/20170605/webrtc.html
+
+ // The following helper functions are called from RTCPeerConnection-helper.js:
+ // getTrackFromUserMedia
+
+ /*
+ 4.3.1.6. Set the RTCSessionSessionDescription
+ 2.2.8. If description is set as a remote description, then run the following
+ steps for each media description in description:
+ 3. Set transceiver's mid value to the mid of the corresponding media
+ description. If the media description has no MID, and transceiver's
+ mid is unset, generate a random value as described in [JSEP] (section 5.9.).
+ 4. If the direction of the media description is sendrecv or sendonly, and
+ transceiver.receiver.track has not yet been fired in a track event,
+ process the remote track for the media description, given transceiver.
+
+ 5.1.1. Processing Remote MediaStreamTracks
+ To process the remote track for an incoming media description [JSEP]
+ (section 5.9.) given RTCRtpTransceiver transceiver, the user agent MUST
+ run the following steps:
+
+ 1. Let connection be the RTCPeerConnection object associated with transceiver.
+ 2. Let streams be a list of MediaStream objects that the media description
+ indicates the MediaStreamTrack belongs to.
+ 3. Add track to all MediaStream objects in streams.
+ 4. Queue a task to fire an event named track with transceiver, track, and
+ streams at the connection object.
+
+ 5.7. RTCTrackEvent
+ [Constructor(DOMString type, RTCTrackEventInit eventInitDict)]
+ interface RTCTrackEvent : Event {
+ readonly attribute RTCRtpReceiver receiver;
+ readonly attribute MediaStreamTrack track;
+ [SameObject]
+ readonly attribute FrozenArray<MediaStream> streams;
+ readonly attribute RTCRtpTransceiver transceiver;
+ };
+
+ [mediacapture-main]
+ 4.2. MediaStream
+ interface MediaStream : EventTarget {
+ readonly attribute DOMString id;
+ sequence<MediaStreamTrack> getTracks();
+ ...
+ };
+
+ [mediacapture-main]
+ 4.3. MediaStreamTrack
+ interface MediaStreamTrack : EventTarget {
+ readonly attribute DOMString kind;
+ readonly attribute DOMString id;
+ ...
+ };
+ */
+
+ function validateTrackEvent(trackEvent) {
+ const { receiver, track, streams, transceiver } = trackEvent;
+
+ assert_true(track instanceof MediaStreamTrack,
+ 'Expect track to be instance of MediaStreamTrack');
+
+ assert_true(Array.isArray(streams),
+ 'Expect streams to be an array');
+
+ for(const mediaStream of streams) {
+ assert_true(mediaStream instanceof MediaStream,
+ 'Expect elements in streams to be instance of MediaStream');
+
+ assert_true(mediaStream.getTracks().includes(track),
+ 'Expect each mediaStream to have track as one of their tracks');
+ }
+
+ assert_true(receiver instanceof RTCRtpReceiver,
+ 'Expect trackEvent.receiver to be defined and is instance of RTCRtpReceiver');
+
+ assert_equals(receiver.track, track,
+ 'Expect trackEvent.receiver.track to be the same as trackEvent.track');
+
+ assert_true(transceiver instanceof RTCRtpTransceiver,
+ 'Expect trackEvent.transceiver to be defined and is instance of RTCRtpTransceiver');
+
+ assert_equals(transceiver.receiver, receiver,
+ 'Expect trackEvent.transceiver.receiver to be the same as trackEvent.receiver');
+ }
+
+ // tests that ontrack is called and parses the msid information from the SDP and creates
+ // the streams with matching identifiers.
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+
+ t.add_cleanup(() => pc.close());
+
+ // Fail the test if the ontrack event handler is not implemented
+ assert_idl_attribute(pc, 'ontrack', 'Expect pc to have ontrack event handler attribute');
+
+ const sdp = `v=0
+o=- 166855176514521964 2 IN IP4 127.0.0.1
+s=-
+t=0 0
+a=msid-semantic:WMS *
+m=audio 9 UDP/TLS/RTP/SAVPF 111
+c=IN IP4 0.0.0.0
+a=rtcp:9 IN IP4 0.0.0.0
+a=ice-ufrag:someufrag
+a=ice-pwd:somelongpwdwithenoughrandomness
+a=fingerprint:sha-256 8C:71:B3:8D:A5:38:FD:8F:A4:2E:A2:65:6C:86:52:BC:E0:6E:94:F2:9F:7C:4D:B5:DF:AF:AA:6F:44:90:8D:F4
+a=setup:actpass
+a=rtcp-mux
+a=mid:mid1
+a=sendonly
+a=rtpmap:111 opus/48000/2
+a=msid:stream1 track1
+a=ssrc:1001 cname:some
+`;
+
+ const trackEventPromise = addEventListenerPromise(t, pc, 'track');
+ await pc.setRemoteDescription({ type: 'offer', sdp });
+ const trackEvent = await trackEventPromise;
+ const { streams, track, transceiver } = trackEvent;
+
+ assert_equals(streams.length, 1,
+ 'the track belongs to one MediaStream');
+
+ const [stream] = streams;
+ assert_equals(stream.id, 'stream1',
+ 'Expect stream.id to be the same as specified in the a=msid line');
+
+ assert_equals(track.kind, 'audio',
+ 'Expect track.kind to be audio');
+
+ validateTrackEvent(trackEvent);
+
+ assert_equals(transceiver.direction, 'recvonly',
+ 'Expect transceiver.direction to be reverse of sendonly (recvonly)');
+ }, 'setRemoteDescription should trigger ontrack event when the MSID of the stream is is parsed.');
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+
+ t.add_cleanup(() => pc.close());
+
+ assert_idl_attribute(pc, 'ontrack', 'Expect pc to have ontrack event handler attribute');
+
+ const sdp = `v=0
+o=- 166855176514521964 2 IN IP4 127.0.0.1
+s=-
+t=0 0
+a=msid-semantic:WMS *
+m=audio 9 UDP/TLS/RTP/SAVPF 111
+c=IN IP4 0.0.0.0
+a=rtcp:9 IN IP4 0.0.0.0
+a=ice-ufrag:someufrag
+a=ice-pwd:somelongpwdwithenoughrandomness
+a=fingerprint:sha-256 8C:71:B3:8D:A5:38:FD:8F:A4:2E:A2:65:6C:86:52:BC:E0:6E:94:F2:9F:7C:4D:B5:DF:AF:AA:6F:44:90:8D:F4
+a=setup:actpass
+a=rtcp-mux
+a=mid:mid1
+a=recvonly
+a=rtpmap:111 opus/48000/2
+a=msid:stream1 track1
+a=ssrc:1001 cname:some
+`;
+
+ pc.ontrack = t.unreached_func('ontrack event should not fire for track with recvonly direction');
+
+ await pc.setRemoteDescription({ type: 'offer', sdp });
+ await new Promise(resolve => t.step_timeout(resolve, 100));
+ }, 'setRemoteDescription() with m= line of recvonly direction should not trigger track event');
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+
+ t.add_cleanup(() => pc2.close());
+
+ const [track, mediaStream] = await getTrackFromUserMedia('audio');
+ pc1.addTrack(track, mediaStream);
+ const trackEventPromise = addEventListenerPromise(t, pc2, 'track');
+ await pc2.setRemoteDescription(await pc1.createOffer());
+ const trackEvent = await trackEventPromise;
+
+ assert_equals(trackEvent.track.kind, 'audio',
+ 'Expect track.kind to be audio');
+
+ validateTrackEvent(trackEvent);
+ }, 'addTrack() should cause remote connection to fire ontrack when setRemoteDescription()');
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+
+ t.add_cleanup(() => pc2.close());
+
+ pc1.addTransceiver('video');
+
+ const trackEventPromise = addEventListenerPromise(t, pc2, 'track');
+ await pc2.setRemoteDescription(await pc1.createOffer());
+ const trackEvent = await trackEventPromise;
+ const { track } = trackEvent;
+
+ assert_equals(track.kind, 'video',
+ 'Expect track.kind to be video');
+
+ validateTrackEvent(trackEvent);
+ }, `addTransceiver('video') should cause remote connection to fire ontrack when setRemoteDescription()`);
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+
+ t.add_cleanup(() => pc2.close());
+
+ pc1.addTransceiver('audio', { direction: 'inactive' });
+ pc2.ontrack = t.unreached_func('ontrack event should not fire for track with inactive direction');
+
+ await pc2.setRemoteDescription(await pc1.createOffer());
+ await new Promise(resolve => t.step_timeout(resolve, 100));
+ }, `addTransceiver() with inactive direction should not cause remote connection to fire ontrack when setRemoteDescription()`);
+
+ ["audio", "video"].forEach(type => promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const checkNoUnexpectedTrack = ({track}) => {
+ assert_equals(track.kind, type, `ontrack event should not fire for ${track.kind}`);
+ };
+
+ pc2.ontrack = t.step_func(checkNoUnexpectedTrack);
+ pc1.ontrack = t.step_func(checkNoUnexpectedTrack);
+
+ await pc1.setLocalDescription(await pc1.createOffer(
+ { offerToReceiveVideo: true, offerToReceiveAudio: true }));
+
+ pc2.addTrack(...await getTrackFromUserMedia(type));
+
+ await pc2.setRemoteDescription(pc1.localDescription);
+ await pc2.setLocalDescription(await pc2.createAnswer());
+ await pc1.setRemoteDescription(pc2.localDescription);
+
+ await new Promise(resolve => t.step_timeout(resolve, 100));
+ }, `Using offerToReceiveAudio and offerToReceiveVideo should only cause a ${type} track event to fire, if ${type} was the only type negotiated`));
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-operations.https.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-operations.https.html
new file mode 100644
index 0000000000..28ae3afcd7
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-operations.https.html
@@ -0,0 +1,425 @@
+<!doctype html>
+<meta charset=utf-8>
+<title></title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script>
+'use strict';
+
+// Helpers to test APIs "return a promise rejected with a newly created" error.
+// Strictly speaking this means already-rejected upon return.
+function promiseState(p) {
+ const t = {};
+ return Promise.race([p, t])
+ .then(v => (v === t)? "pending" : "fulfilled", () => "rejected");
+}
+
+// However, to allow promises to be used in implementations, this helper adds
+// some slack: returning a pending promise will pass, provided it is rejected
+// before the end of the current run of the event loop (i.e. on microtask queue
+// before next task).
+async function promiseStateFinal(p) {
+ for (let i = 0; i < 20; i++) {
+ await promiseState(p);
+ }
+ return promiseState(p);
+}
+
+[promiseState, promiseStateFinal].forEach(f => promise_test(async t => {
+ assert_equals(await f(Promise.resolve()), "fulfilled");
+ assert_equals(await f(Promise.reject()), "rejected");
+ assert_equals(await f(new Promise(() => {})), "pending");
+}, `${f.name} helper works`));
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ await pc.setRemoteDescription(await pc.createOffer());
+ const p = pc.createOffer();
+ const haveState = promiseStateFinal(p);
+ try {
+ await p;
+ assert_unreached("Control. Must not succeed");
+ } catch (e) {
+ assert_equals(e.name, "InvalidStateError");
+ }
+ assert_equals(await haveState, "rejected", "promise rejected on same task");
+}, "createOffer must detect InvalidStateError synchronously when chain is empty (prerequisite)");
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const p = pc.createAnswer();
+ const haveState = promiseStateFinal(p);
+ try {
+ await p;
+ assert_unreached("Control. Must not succeed");
+ } catch (e) {
+ assert_equals(e.name, "InvalidStateError");
+ }
+ assert_equals(await haveState, "rejected", "promise rejected on same task");
+}, "createAnswer must detect InvalidStateError synchronously when chain is empty (prerequisite)");
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const p = pc.setLocalDescription({type: "rollback"});
+ const haveState = promiseStateFinal(p);
+ try {
+ await p;
+ assert_unreached("Control. Must not succeed");
+ } catch (e) {
+ assert_equals(e.name, "InvalidStateError");
+ }
+ assert_equals(await haveState, "rejected", "promise rejected on same task");
+}, "SLD(rollback) must detect InvalidStateError synchronously when chain is empty");
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const p = pc.addIceCandidate();
+ const haveState = promiseStateFinal(p);
+ try {
+ await p;
+ assert_unreached("Control. Must not succeed");
+ } catch (e) {
+ assert_equals(e.name, "InvalidStateError");
+ }
+ assert_equals(pc.remoteDescription, null, "no remote desciption");
+ assert_equals(await haveState, "rejected", "promise rejected on same task");
+}, "addIceCandidate must detect InvalidStateError synchronously when chain is empty");
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const transceiver = pc.addTransceiver("audio");
+ transceiver.stop();
+ const p = transceiver.sender.replaceTrack(null);
+ const haveState = promiseStateFinal(p);
+ try {
+ await p;
+ assert_unreached("Control. Must not succeed");
+ } catch (e) {
+ assert_equals(e.name, "InvalidStateError");
+ }
+ assert_equals(await haveState, "rejected", "promise rejected on same task");
+}, "replaceTrack must detect InvalidStateError synchronously when chain is empty and transceiver is stopped");
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const transceiver = pc.addTransceiver("audio");
+ transceiver.stop();
+ const parameters = transceiver.sender.getParameters();
+ const p = transceiver.sender.setParameters(parameters);
+ const haveState = promiseStateFinal(p);
+ try {
+ await p;
+ assert_unreached("Control. Must not succeed");
+ } catch (e) {
+ assert_equals(e.name, "InvalidStateError");
+ }
+ assert_equals(await haveState, "rejected", "promise rejected on same task");
+}, "setParameters must detect InvalidStateError synchronously always when transceiver is stopped");
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const {track} = new RTCPeerConnection().addTransceiver("audio").receiver;
+ assert_not_equals(track, null);
+ const p = pc.getStats(track);
+ const haveState = promiseStateFinal(p);
+ try {
+ await p;
+ assert_unreached("Control. Must not succeed");
+ } catch (e) {
+ assert_equals(e.name, "InvalidAccessError");
+ }
+ assert_equals(await haveState, "rejected", "promise rejected on same task");
+}, "pc.getStats must detect InvalidAccessError synchronously always");
+
+// Helper builds on above tests to check if operations queue is empty or not.
+//
+// Meaning of "empty": Because this helper uses the sloppy promiseStateFinal,
+// it may not detect operations on the chain unless they block the current run
+// of the event loop. In other words, it may not detect operations on the chain
+// that resolve on the emptying of the microtask queue at the end of this run of
+// the event loop.
+
+async function isOperationsChainEmpty(pc) {
+ let p, error;
+ const signalingState = pc.signalingState;
+ if (signalingState == "have-remote-offer") {
+ p = pc.createOffer();
+ } else {
+ p = pc.createAnswer();
+ }
+ const state = await promiseStateFinal(p);
+ try {
+ await p;
+ // This helper tries to avoid side-effects by always failing,
+ // but createAnswer above may succeed if chained after an SRD
+ // that changes the signaling state on us. Ignore that success.
+ if (signalingState == pc.signalingState) {
+ assert_unreached("Control. Must not succeed");
+ }
+ } catch (e) {
+ assert_equals(e.name, "InvalidStateError",
+ "isOperationsChainEmpty is working");
+ }
+ return state == "rejected";
+}
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ assert_true(await isOperationsChainEmpty(pc), "Empty to start");
+}, "isOperationsChainEmpty detects empty in stable");
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ await pc.setLocalDescription(await pc.createOffer());
+ assert_true(await isOperationsChainEmpty(pc), "Empty to start");
+}, "isOperationsChainEmpty detects empty in have-local-offer");
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ await pc.setRemoteDescription(await pc.createOffer());
+ assert_true(await isOperationsChainEmpty(pc), "Empty to start");
+}, "isOperationsChainEmpty detects empty in have-remote-offer");
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const p = pc.createOffer();
+ assert_false(await isOperationsChainEmpty(pc), "Non-empty chain");
+ await p;
+}, "createOffer uses operations chain");
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ await pc.setRemoteDescription(await pc.createOffer());
+ const p = pc.createAnswer();
+ assert_false(await isOperationsChainEmpty(pc), "Non-empty chain");
+ await p;
+}, "createAnswer uses operations chain");
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const offer = await pc.createOffer();
+ assert_true(await isOperationsChainEmpty(pc), "Empty before");
+ const p = pc.setLocalDescription(offer);
+ assert_false(await isOperationsChainEmpty(pc), "Non-empty chain");
+ await p;
+}, "setLocalDescription uses operations chain");
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const offer = await pc.createOffer();
+ assert_true(await isOperationsChainEmpty(pc), "Empty before");
+ const p = pc.setRemoteDescription(offer);
+ assert_false(await isOperationsChainEmpty(pc), "Non-empty chain");
+ await p;
+}, "setRemoteDescription uses operations chain");
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ pc1.addTransceiver("video");
+ const offer = await pc1.createOffer();
+ await pc1.setLocalDescription(offer);
+ const {candidate} = await new Promise(r => pc1.onicecandidate = r);
+ await pc2.setRemoteDescription(offer);
+ const p = pc2.addIceCandidate(candidate);
+ assert_false(await isOperationsChainEmpty(pc2), "Non-empty chain");
+ await p;
+}, "addIceCandidate uses operations chain");
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const transceiver = pc.addTransceiver("audio");
+ await new Promise(r => pc.onnegotiationneeded = r);
+ assert_true(await isOperationsChainEmpty(pc), "Empty chain");
+ await new Promise(r => t.step_timeout(r, 0));
+ assert_true(await isOperationsChainEmpty(pc), "Empty chain");
+}, "Firing of negotiationneeded does NOT use operations chain");
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ pc1.addTransceiver("audio");
+ pc1.addTransceiver("video");
+ const offer = await pc1.createOffer();
+ await pc1.setLocalDescription(offer);
+ const candidates = [];
+ for (let c; (c = (await new Promise(r => pc1.onicecandidate = r)).candidate);) {
+ candidates.push(c);
+ }
+ pc2.addTransceiver("video");
+ let fired = false;
+ const p = new Promise(r => pc2.onnegotiationneeded = () => r(fired = true));
+ await Promise.all([
+ pc2.setRemoteDescription(offer),
+ ...candidates.map(candidate => pc2.addIceCandidate(candidate)),
+ pc2.setLocalDescription()
+ ]);
+ assert_false(fired, "Negotiationneeded mustn't have fired yet.");
+ await new Promise(r => t.step_timeout(r, 0));
+ assert_true(fired, "Negotiationneeded must have fired by now.");
+ await p;
+}, "Negotiationneeded only fires once operations chain is empty");
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const transceiver = pc.addTransceiver("audio");
+ await new Promise(r => pc.onnegotiationneeded = r);
+ // Note: since the negotiationneeded event is fired from a chained synchronous
+ // function in the spec, queue a task before doing our precheck.
+ await new Promise(r => t.step_timeout(r, 0));
+ assert_true(await isOperationsChainEmpty(pc), "Empty chain");
+ const p = transceiver.sender.replaceTrack(null);
+ assert_false(await isOperationsChainEmpty(pc), "Non-empty chain");
+ await p;
+}, "replaceTrack uses operations chain");
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const transceiver = pc.addTransceiver("audio");
+ await new Promise(r => pc.onnegotiationneeded = r);
+ await new Promise(r => t.step_timeout(r, 0));
+ assert_true(await isOperationsChainEmpty(pc), "Empty chain");
+ const parameters = transceiver.sender.getParameters();
+ const p = transceiver.sender.setParameters(parameters);
+ const haveState = promiseStateFinal(p);
+ assert_true(await isOperationsChainEmpty(pc), "Empty chain");
+ assert_equals(await haveState, "pending", "Method is async");
+ await p;
+}, "setParameters does NOT use the operations chain");
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const p = pc.getStats();
+ const haveState = promiseStateFinal(p);
+ assert_true(await isOperationsChainEmpty(pc), "Empty chain");
+ assert_equals(await haveState, "pending", "Method is async");
+ await p;
+}, "pc.getStats does NOT use the operations chain");
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const {sender} = pc.addTransceiver("audio");
+ await new Promise(r => pc.onnegotiationneeded = r);
+ await new Promise(r => t.step_timeout(r, 0));
+ assert_true(await isOperationsChainEmpty(pc), "Empty chain");
+ const p = sender.getStats();
+ const haveState = promiseStateFinal(p);
+ assert_true(await isOperationsChainEmpty(pc), "Empty chain");
+ assert_equals(await haveState, "pending", "Method is async");
+ await p;
+}, "sender.getStats does NOT use the operations chain");
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const {receiver} = pc.addTransceiver("audio");
+ await new Promise(r => pc.onnegotiationneeded = r);
+ await new Promise(r => t.step_timeout(r, 0));
+ assert_true(await isOperationsChainEmpty(pc), "Empty chain");
+ const p = receiver.getStats();
+ const haveState = promiseStateFinal(p);
+ assert_true(await isOperationsChainEmpty(pc), "Empty chain");
+ assert_equals(await haveState, "pending", "Method is async");
+ await p;
+}, "receiver.getStats does NOT use the operations chain");
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ pc1.addTransceiver("video");
+ const offer = await pc1.createOffer();
+ await pc1.setLocalDescription(offer);
+ const {candidate} = await new Promise(r => pc1.onicecandidate = r);
+ try {
+ await pc2.addIceCandidate(candidate);
+ assert_unreached("Control. Must not succeed");
+ } catch (e) {
+ assert_equals(e.name, "InvalidStateError");
+ }
+ const p = pc2.setRemoteDescription(offer);
+ await pc2.addIceCandidate(candidate);
+ await p;
+}, "addIceCandidate chains onto SRD, fails before");
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const offer = await pc.createOffer();
+ pc.addTransceiver("video");
+ await new Promise(r => pc.onnegotiationneeded = r);
+ const p = (async () => {
+ await pc.setLocalDescription();
+ })();
+ await new Promise(r => t.step_timeout(r, 0));
+ await pc.setRemoteDescription(offer);
+ await p;
+}, "Operations queue not vulnerable to recursion by chained negotiationneeded");
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ pc1.addTransceiver("video");
+ await Promise.all([
+ pc1.createOffer(),
+ pc1.setLocalDescription({type: "offer"})
+ ]);
+ await Promise.all([
+ pc2.setRemoteDescription(pc1.localDescription),
+ pc2.createAnswer(),
+ pc2.setLocalDescription({type: "answer"})
+ ]);
+ await pc1.setRemoteDescription(pc2.localDescription);
+}, "Pack operations queue with implicit offer and answer");
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const state = (pc, s) => new Promise(r => pc.onsignalingstatechange =
+ () => pc.signalingState == s && r());
+ pc1.addTransceiver("video");
+ pc1.createOffer();
+ pc1.setLocalDescription({type: "offer"});
+ await state(pc1, "have-local-offer");
+ pc2.setRemoteDescription(pc1.localDescription);
+ pc2.createAnswer();
+ pc2.setLocalDescription({type: "answer"});
+ await state(pc2, "stable");
+ await pc1.setRemoteDescription(pc2.localDescription);
+}, "Negotiate solely by operations queue and signaling state");
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-perfect-negotiation-helper.js b/testing/web-platform/tests/webrtc/RTCPeerConnection-perfect-negotiation-helper.js
new file mode 100644
index 0000000000..ed647bbe78
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-perfect-negotiation-helper.js
@@ -0,0 +1,153 @@
+'use strict'
+
+function peer(other, polite, fail = null) {
+ const send = (tgt, msg) => tgt.postMessage(JSON.parse(JSON.stringify(msg)),
+ "*");
+ if (!fail) fail = e => send(window.parent, {error: `${e.name}: ${e.message}`});
+ const pc = new RTCPeerConnection();
+
+ if (!window.assert_equals) {
+ window.assert_equals = (a, b, msg) => a === b ||
+ fail(new Error(`${msg} expected ${b} but got ${a}`));
+ }
+
+ const commands = {
+ async addTransceiver() {
+ const transceiver = pc.addTransceiver("video");
+ await new Promise(r => pc.addEventListener("negotiated", r, {once: true}));
+ if (!transceiver.currentDirection) {
+ // Might have just missed the negotiation train. Catch next one.
+ await new Promise(r => pc.addEventListener("negotiated", r, {once: true}));
+ }
+ assert_equals(transceiver.currentDirection, "sendonly", "have direction");
+ return pc.getTransceivers().length;
+ },
+ async simpleConnect() {
+ const p = commands.addTransceiver();
+ await new Promise(r => pc.oniceconnectionstatechange =
+ () => pc.iceConnectionState == "connected" && r());
+ return await p;
+ },
+ async getNumTransceivers() {
+ return pc.getTransceivers().length;
+ },
+ };
+
+ try {
+ pc.addEventListener("icecandidate", ({candidate}) => send(other,
+ {candidate}));
+ let makingOffer = false, ignoreIceCandidateFailures = false;
+ let srdAnswerPending = false;
+ pc.addEventListener("negotiationneeded", async () => {
+ try {
+ assert_equals(pc.signalingState, "stable", "negotiationneeded always fires in stable state");
+ assert_equals(makingOffer, false, "negotiationneeded not already in progress");
+ makingOffer = true;
+ await pc.setLocalDescription();
+ assert_equals(pc.signalingState, "have-local-offer", "negotiationneeded not racing with onmessage");
+ assert_equals(pc.localDescription.type, "offer", "negotiationneeded SLD worked");
+ send(other, {description: pc.localDescription});
+ } catch (e) {
+ fail(e);
+ } finally {
+ makingOffer = false;
+ }
+ });
+ window.onmessage = async ({data: {description, candidate, run}}) => {
+ try {
+ if (description) {
+ // If we have a setRemoteDescription() answer operation pending, then
+ // we will be "stable" by the time the next setRemoteDescription() is
+ // executed, so we count this being stable when deciding whether to
+ // ignore the offer.
+ let isStable =
+ pc.signalingState == "stable" ||
+ (pc.signalingState == "have-local-offer" && srdAnswerPending);
+ const ignoreOffer = description.type == "offer" && !polite &&
+ (makingOffer || !isStable);
+ if (ignoreOffer) {
+ ignoreIceCandidateFailures = true;
+ return;
+ }
+ if (description.type == "answer")
+ srdAnswerPending = true;
+ await pc.setRemoteDescription(description);
+ ignoreIceCandidateFailures = false;
+ srdAnswerPending = false;
+ if (description.type == "offer") {
+ assert_equals(pc.signalingState, "have-remote-offer", "Remote offer");
+ assert_equals(pc.remoteDescription.type, "offer", "SRD worked");
+ await pc.setLocalDescription();
+ assert_equals(pc.signalingState, "stable", "onmessage not racing with negotiationneeded");
+ assert_equals(pc.localDescription.type, "answer", "onmessage SLD worked");
+ send(other, {description: pc.localDescription});
+ } else {
+ assert_equals(pc.remoteDescription.type, "answer", "Answer was set");
+ assert_equals(pc.signalingState, "stable", "answered");
+ pc.dispatchEvent(new Event("negotiated"));
+ }
+ } else if (candidate) {
+ try {
+ await pc.addIceCandidate(candidate);
+ } catch (e) {
+ if (!ignoreIceCandidateFailures) throw e;
+ }
+ } else if (run) {
+ send(window.parent, {[run.id]: await commands[run.cmd]() || 0});
+ }
+ } catch (e) {
+ fail(e);
+ }
+ };
+ } catch (e) {
+ fail(e);
+ }
+ return pc;
+}
+
+async function setupPeerIframe(t, polite) {
+ const iframe = document.createElement("iframe");
+ t.add_cleanup(() => iframe.remove());
+ iframe.srcdoc =
+ `<html\><script\>(${peer.toString()})(window.parent, ${polite});</script\></html\>`;
+ document.documentElement.appendChild(iframe);
+
+ const failCatcher = t.step_func(({data}) =>
+ ("error" in data) && assert_unreached(`Error in iframe: ${data.error}`));
+ window.addEventListener("message", failCatcher);
+ t.add_cleanup(() => window.removeEventListener("message", failCatcher));
+ await new Promise(r => iframe.onload = r);
+ return iframe;
+}
+
+function setupPeerTopLevel(t, other, polite) {
+ const pc = peer(other, polite, t.step_func(e => { throw e; }));
+ t.add_cleanup(() => { pc.close(); window.onmessage = null; });
+}
+
+let counter = 0;
+async function run(target, cmd) {
+ const id = `result${counter++}`;
+ target.postMessage({run: {cmd, id}}, "*");
+ return new Promise(r => window.addEventListener("message",
+ function listen({data}) {
+ if (!(id in data)) return;
+ window.removeEventListener("message", listen);
+ r(data[id]);
+ }));
+}
+
+let iframe;
+async function setupAB(t, politeA, politeB) {
+ iframe = await setupPeerIframe(t, politeB);
+ return setupPeerTopLevel(t, iframe.contentWindow, politeA);
+}
+const runA = cmd => run(window, cmd);
+const runB = cmd => run(iframe.contentWindow, cmd);
+const runBoth = (cmdA, cmdB = cmdA) => Promise.all([runA(cmdA), runB(cmdB)]);
+
+async function promise_test_both_roles(f, name) {
+ promise_test(async t => f(t, await setupAB(t, true, false)), name);
+ promise_test(async t => f(t, await setupAB(t, false, true)),
+ `${name} with roles reversed`);
+}
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-perfect-negotiation-stress-glare-linear.https.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-perfect-negotiation-stress-glare-linear.https.html
new file mode 100644
index 0000000000..cf8bdf22e2
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-perfect-negotiation-stress-glare-linear.https.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<title></title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src="RTCPeerConnection-perfect-negotiation-helper.js"></script>
+<script>
+'use strict';
+
+promise_test_both_roles(async (t, pc) => {
+ const ps = [];
+ for (let i = 10; i > 0; i--) {
+ ps.push(runBoth("addTransceiver"));
+ await new Promise(r => t.step_timeout(r, 0));
+ }
+ ps.push(runBoth("addTransceiver"));
+ await Promise.all(ps);
+ const [numA, numB] = await runBoth("getNumTransceivers");
+ assert_equals(numA, 22, "22 transceivers on side A");
+ assert_equals(numB, 22, "22 transceivers on side B");
+}, "Perfect negotiation stress glare linear");
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-perfect-negotiation-stress-glare.https.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-perfect-negotiation-stress-glare.https.html
new file mode 100644
index 0000000000..6134eb2006
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-perfect-negotiation-stress-glare.https.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<title></title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src="RTCPeerConnection-perfect-negotiation-helper.js"></script>
+<script>
+'use strict';
+
+promise_test_both_roles(async (t, pc) => {
+ const ps = [];
+ for (let i = 10; i > 0; i--) {
+ ps.push(runBoth("addTransceiver"));
+ await new Promise(r => t.step_timeout(r, i - 1));
+ }
+ ps.push(runBoth("addTransceiver"));
+ await Promise.all(ps);
+ const [numA, numB] = await runBoth("getNumTransceivers");
+ assert_equals(numA, 22, "22 transceivers on side A");
+ assert_equals(numB, 22, "22 transceivers on side B");
+}, "Perfect negotiation stress glare");
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-perfect-negotiation.https.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-perfect-negotiation.https.html
new file mode 100644
index 0000000000..d01b116162
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-perfect-negotiation.https.html
@@ -0,0 +1,22 @@
+<!doctype html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<title></title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src="RTCPeerConnection-perfect-negotiation-helper.js"></script>
+<script>
+'use strict';
+
+promise_test_both_roles(async (t, pc) => {
+ assert_equals(await runA("simpleConnect"), 1, "one transceiver");
+ assert_equals(await runB("addTransceiver"), 2, "two transceivers");
+}, "Perfect negotiation setup connects");
+
+promise_test_both_roles(async (t, pc) => {
+ await runBoth("addTransceiver");
+ const [numA, numB] = await runBoth("getNumTransceivers");
+ assert_equals(numA, 2, "two transceivers on side A");
+ assert_equals(numB, 2, "two transceivers on side B");
+}, "Perfect negotiation glare");
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-plan-b-is-not-supported.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-plan-b-is-not-supported.html
new file mode 100644
index 0000000000..bde6b1b003
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-plan-b-is-not-supported.html
@@ -0,0 +1,28 @@
+<!doctype html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<title></title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script>
+'use strict';
+
+promise_test(async t => {
+ // Plan B is a legacy feature that should not be supported on a modern
+ // browser. To pass this test you must either ignore sdpSemantics altogether
+ // (and construct with Unified Plan despite us asking for Plan B) or throw an
+ // exception.
+ let pc = null;
+ try {
+ pc = new RTCPeerConnection({sdpSemantics:"plan-b"});
+ t.add_cleanup(() => pc.close());
+ } catch (e) {
+ // Test passed!
+ return;
+ }
+ // If we did not throw, we must not have gotten what we asked for. If
+ // sdpSemantics is not recognized by the browser it will be undefined here.
+ assert_not_equals(pc.getConfiguration().sdpSemantics, "plan-b");
+}, 'Plan B is not supported');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-relay-canvas.https.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-relay-canvas.https.html
new file mode 100644
index 0000000000..78df2ee82d
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-relay-canvas.https.html
@@ -0,0 +1,84 @@
+<!doctype html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<title>Relay canvas via PeerConnections</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src="RTCPeerConnection-helper.js"></script>
+<script>
+ 'use strict';
+
+// This test checks that canvas capture works relayed between several peer connections.
+
+function GreenFrameWebGL(width, height) {
+ const canvas =
+ Object.assign(document.createElement('canvas'), {width, height});
+ const ctx = canvas.getContext('webgl');
+ assert_not_equals(ctx, null, "webgl is a prerequisite for this test");
+ requestAnimationFrame(function draw () {
+ ctx.clearColor(0.0, 1.0, 0.0, 1.0);
+ ctx.clear(ctx.COLOR_BUFFER_BIT);
+ requestAnimationFrame(draw);
+ });
+ return canvas.captureStream();
+}
+
+
+
+promise_test(async t => {
+
+ // Build a chain
+ // canvas -track-> pc1 -network-> pcRelayIn -track->
+ // pcRelayOut -network-> pc2 -track-> video
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pcRelayIn = new RTCPeerConnection();
+ t.add_cleanup(() => pcRelayIn.close());
+
+ const pcRelayOut = new RTCPeerConnection();
+ t.add_cleanup(() => pcRelayOut.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ // Attach canvas to pc1.
+ const stream = GreenFrameWebGL(640, 480);
+ const [track] = stream.getTracks();
+ pc1.addTrack(track);
+
+ const v = document.createElement('video');
+ v.autoplay = true;
+
+ // Setup pc1->pcRelayIn video stream.
+ const haveTrackEvent1 = new Promise(r => pcRelayIn.ontrack = r);
+ exchangeIceCandidates(pc1, pcRelayIn);
+ await pc1.setLocalDescription();
+ await pcRelayIn.setRemoteDescription(pc1.localDescription);
+ await pcRelayIn.setLocalDescription();
+ await pc1.setRemoteDescription(pcRelayIn.localDescription);
+
+ // Plug output of pcRelayIn to pcRelayOut.
+ pcRelayOut.addTrack((await haveTrackEvent1).track);
+
+ // Setup pcRelayOut->pc2 video stream.
+ const haveTrackEvent2 = new Promise(r => pc2.ontrack = r);
+ exchangeIceCandidates(pcRelayOut, pc2);
+ await pcRelayOut.setLocalDescription();
+ await pc2.setRemoteDescription(pcRelayOut.localDescription);
+ await pc2.setLocalDescription();
+ await pcRelayOut.setRemoteDescription(pc2.localDescription);
+
+ // Display pc2 received track in video element.
+ v.srcObject = new MediaStream([(await haveTrackEvent2).track]);
+ await new Promise(r => v.onloadedmetadata = r);
+
+ // Wait some time to ensure that frames got through.
+ await new Promise(resolve => t.step_timeout(resolve, 1000));
+
+ // Uses Helper.js GetVideoSignal to query |v| pixel value at a certain position.
+ const pixelValue = getVideoSignal(v);
+
+ // Expected value computed based on GetVideoSignal code, which takes green pixel data
+ // with coefficient 0.72.
+ assert_approx_equals(pixelValue, 0.72*255, 3);
+ }, "Two PeerConnections relaying a canvas source");
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-remote-track-mute.https.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-remote-track-mute.https.html
new file mode 100644
index 0000000000..c280a7d44d
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-remote-track-mute.https.html
@@ -0,0 +1,132 @@
+<!doctype html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<title>RTCPeerConnection-transceivers.https.html</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="RTCPeerConnection-helper.js"></script>
+<script>
+'use strict';
+
+// The following helper functions are called from RTCPeerConnection-helper.js:
+// exchangeOffer
+// exchangeOfferAndListenToOntrack
+// exchangeAnswer
+// exchangeAnswerAndListenToOntrack
+// addEventListenerPromise
+// createPeerConnectionWithCleanup
+// createTrackAndStreamWithCleanup
+// findTransceiverForSender
+
+promise_test(async t => {
+ const pc1 = createPeerConnectionWithCleanup(t);
+ pc1.addTrack(... await createTrackAndStreamWithCleanup(t));
+ const pc2 = createPeerConnectionWithCleanup(t);
+ exchangeIceCandidates(pc1, pc2);
+
+ const unmuteResolver = new Resolver();
+ let remoteTrack = null;
+ // The unmuting it timing sensitive so we hook up to the event directly
+ // instead of wrapping it in an EventWatcher which uses promises.
+ pc2.ontrack = t.step_func(e => {
+ remoteTrack = e.track;
+ assert_true(remoteTrack.muted, 'track is muted in ontrack');
+ remoteTrack.onunmute = t.step_func(e => {
+ assert_false(remoteTrack.muted, 'track is unmuted in onunmute');
+ unmuteResolver.resolve();
+ });
+ pc2.ontrack = t.step_func(e => {
+ assert_unreached('ontrack fired unexpectedly');
+ });
+ });
+ await exchangeOfferAnswer(pc1, pc2);
+ await unmuteResolver;
+}, 'ontrack: track goes from muted to unmuted');
+
+promise_test(async t => {
+ const pc1 = createPeerConnectionWithCleanup(t);
+ const pc1Sender = pc1.addTrack(... await createTrackAndStreamWithCleanup(t));
+ const localTransceiver = findTransceiverForSender(pc1, pc1Sender);
+ const pc2 = createPeerConnectionWithCleanup(t);
+ exchangeIceCandidates(pc1, pc2);
+
+ const e = await exchangeOfferAndListenToOntrack(t, pc1, pc2);
+ // Need to wait for the initial unmute event before renegotiating, otherwise
+ // there will be no transition from unmuted->muted.
+ const muteWatcher = new EventWatcher(t, e.track, ['mute', 'unmute']);
+ const unmutePromise = muteWatcher.wait_for('unmute');
+ await exchangeAnswer(pc1, pc2);
+ await unmutePromise;
+
+ const mutePromise = muteWatcher.wait_for('mute');
+ localTransceiver.direction = 'inactive';
+ await exchangeOfferAnswer(pc1, pc2);
+
+ await mutePromise;
+}, 'Changing transceiver direction to \'inactive\' mutes the remote track');
+
+promise_test(async t => {
+ const pc1 = createPeerConnectionWithCleanup(t);
+ const pc1Sender = pc1.addTrack(... await createTrackAndStreamWithCleanup(t));
+ const localTransceiver = findTransceiverForSender(pc1, pc1Sender);
+ const pc2 = createPeerConnectionWithCleanup(t);
+ exchangeIceCandidates(pc1, pc2);
+
+ const e = await exchangeOfferAndListenToOntrack(t, pc1, pc2);
+ const muteWatcher = new EventWatcher(t, e.track, ['mute', 'unmute']);
+ await exchangeAnswer(pc1, pc2);
+ await muteWatcher.wait_for('unmute');
+
+ const mutePromise = muteWatcher.wait_for('mute');
+ localTransceiver.direction = 'inactive';
+ await exchangeOfferAnswer(pc1, pc2);
+ await mutePromise;
+
+ const unmutePromise = muteWatcher.wait_for('unmute');
+ localTransceiver.direction = 'sendrecv';
+ await exchangeOfferAnswer(pc1, pc2);
+
+ await unmutePromise;
+}, 'Changing transceiver direction to \'sendrecv\' unmutes the remote track');
+
+promise_test(async t => {
+ const pc1 = createPeerConnectionWithCleanup(t);
+ const pc1Sender = pc1.addTrack(... await createTrackAndStreamWithCleanup(t));
+ const localTransceiver = findTransceiverForSender(pc1, pc1Sender);
+ const pc2 = createPeerConnectionWithCleanup(t);
+ exchangeIceCandidates(pc1, pc2);
+
+ const e = await exchangeOfferAndListenToOntrack(t, pc1, pc2);
+ // Need to wait for the initial unmute event before closing, otherwise
+ // there will be no transition from unmuted->muted.
+ const muteWatcher = new EventWatcher(t, e.track, ['mute', 'unmute']);
+ const unmutePromise = muteWatcher.wait_for('unmute');
+ await exchangeAnswer(pc1, pc2);
+ await unmutePromise;
+
+ const mutePromise = muteWatcher.wait_for('mute');
+ localTransceiver.stop();
+ await mutePromise;
+}, 'transceiver.stop() on one side (without renegotiation) causes mute events on the other');
+
+promise_test(async t => {
+ const pc1 = createPeerConnectionWithCleanup(t);
+ const pc1Sender = pc1.addTrack(...await createTrackAndStreamWithCleanup(t));
+ const localTransceiver = findTransceiverForSender(pc1, pc1Sender);
+ const pc2 = createPeerConnectionWithCleanup(t);
+ exchangeIceCandidates(pc1, pc2);
+
+ const e = await exchangeOfferAndListenToOntrack(t, pc1, pc2);
+ // Need to wait for the initial unmute event before closing, otherwise
+ // there will be no transition from unmuted->muted.
+ const muteWatcher = new EventWatcher(t, e.track, ['mute', 'unmute']);
+ const unmutePromise = muteWatcher.wait_for('unmute');
+ await exchangeAnswer(pc1, pc2);
+ await unmutePromise;
+
+ const mutePromise = muteWatcher.wait_for('mute');
+ pc1.close();
+ await mutePromise;
+}, 'pc.close() on one side causes mute events on the other');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-removeTrack.https.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-removeTrack.https.html
new file mode 100644
index 0000000000..83095c085a
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-removeTrack.https.html
@@ -0,0 +1,338 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerConnection.prototype.removeTrack</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="RTCPeerConnection-helper.js"></script>
+<script>
+ 'use strict';
+
+ // Test is based on the following editor draft:
+ // https://w3c.github.io/webrtc-pc/archives/20170605/webrtc.html
+
+ // The following helper functions are called from RTCPeerConnection-helper.js:
+ // generateAnswer
+
+ /*
+ 5.1. RTCPeerConnection Interface Extensions
+ partial interface RTCPeerConnection {
+ ...
+ void removeTrack(RTCRtpSender sender);
+ RTCRtpTransceiver addTransceiver((MediaStreamTrack or DOMString) trackOrKind,
+ optional RTCRtpTransceiverInit init);
+ };
+ */
+
+ // Before calling removeTrack can be tested, one needs to add MediaStreamTracks to
+ // a peer connection. There are two ways for adding MediaStreamTrack: addTrack and
+ // addTransceiver. addTransceiver is a newer API while addTrack has been implemented
+ // in current browsers for some time. As a result some of the removeTrack tests have
+ // two versions so that removeTrack can be partially tested without addTransceiver
+ // and the transceiver APIs being implemented.
+
+ /*
+ 5.1. removeTrack
+ 3. If connection's [[isClosed]] slot is true, throw an InvalidStateError.
+ */
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ const stream = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const [track] = stream.getTracks();
+ const transceiver = pc.addTransceiver(track);
+ const { sender } = transceiver;
+
+ pc.close();
+ assert_throws_dom('InvalidStateError', () => pc.removeTrack(sender));
+ }, 'addTransceiver - Calling removeTrack when connection is closed should throw InvalidStateError');
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const stream = await getNoiseStream({ audio: true });
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const [track] = stream.getTracks();
+ const sender = pc.addTrack(track, stream);
+
+ pc.close();
+ assert_throws_dom('InvalidStateError', () => pc.removeTrack(sender));
+ }, 'addTrack - Calling removeTrack when connection is closed should throw InvalidStateError');
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const stream = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const [track] = stream.getTracks();
+ const transceiver = pc.addTransceiver(track);
+ const { sender } = transceiver;
+
+ const pc2 = new RTCPeerConnection();
+ pc2.close();
+ assert_throws_dom('InvalidStateError', () => pc2.removeTrack(sender));
+ }, 'addTransceiver - Calling removeTrack on different connection that is closed should throw InvalidStateError');
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const stream = await getNoiseStream({ audio: true });
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const [track] = stream.getTracks();
+ const sender = pc.addTrack(track, stream);
+
+ const pc2 = new RTCPeerConnection();
+ pc2.close();
+ assert_throws_dom('InvalidStateError', () => pc2.removeTrack(sender));
+ }, 'addTrack - Calling removeTrack on different connection that is closed should throw InvalidStateError');
+
+ /*
+ 5.1. removeTrack
+ 4. If sender was not created by connection, throw an InvalidAccessError.
+ */
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const stream = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const [track] = stream.getTracks();
+ const transceiver = pc.addTransceiver(track);
+ const { sender } = transceiver;
+
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+ assert_throws_dom('InvalidAccessError', () => pc2.removeTrack(sender));
+ }, 'addTransceiver - Calling removeTrack on different connection should throw InvalidAccessError');
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const stream = await getNoiseStream({ audio: true });
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const [track] = stream.getTracks();
+ const sender = pc.addTrack(track, stream);
+
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+ assert_throws_dom('InvalidAccessError', () => pc2.removeTrack(sender));
+ }, 'addTrack - Calling removeTrack on different connection should throw InvalidAccessError')
+
+ /*
+ 5.1. removeTrack
+ 7. Set sender.track to null.
+ */
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const stream = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const [track] = stream.getTracks();
+ const transceiver = pc.addTransceiver(track);
+ const { sender } = transceiver;
+
+ assert_equals(sender.track, track);
+ assert_equals(transceiver.direction, 'sendrecv');
+ assert_equals(transceiver.currentDirection, null);
+
+ pc.removeTrack(sender);
+ assert_equals(sender.track, null);
+ assert_equals(transceiver.direction, 'recvonly');
+ }, 'addTransceiver - Calling removeTrack with valid sender should set sender.track to null');
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const stream = await getNoiseStream({ audio: true });
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const [track] = stream.getTracks();
+ const sender = pc.addTrack(track, stream);
+
+ assert_equals(sender.track, track);
+
+ pc.removeTrack(sender);
+ assert_equals(sender.track, null);
+ }, 'addTrack - Calling removeTrack with valid sender should set sender.track to null');
+
+ /*
+ 5.1. removeTrack
+ 7. Set sender.track to null.
+ 10. If transceiver.currentDirection is sendrecv set transceiver.direction
+ to recvonly.
+ */
+ promise_test(async t => {
+ const caller = new RTCPeerConnection();
+ t.add_cleanup(() => caller.close());
+ const callee = new RTCPeerConnection();
+ t.add_cleanup(() => callee.close());
+ const stream = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const [track] = stream.getTracks();
+ const transceiver = caller.addTransceiver(track);
+ const { sender } = transceiver;
+
+ assert_equals(sender.track, track);
+ assert_equals(transceiver.direction, 'sendrecv');
+ assert_equals(transceiver.currentDirection, null);
+
+ const offer = await caller.createOffer();
+ await caller.setLocalDescription(offer);
+ await callee.setRemoteDescription(offer);
+ callee.addTrack(track, stream);
+ const answer = await callee.createAnswer();
+ await callee.setLocalDescription(answer);
+ await caller.setRemoteDescription(answer);
+ assert_equals(transceiver.currentDirection, 'sendrecv');
+
+ caller.removeTrack(sender);
+ assert_equals(sender.track, null);
+ assert_equals(transceiver.direction, 'recvonly');
+ assert_equals(transceiver.currentDirection, 'sendrecv',
+ 'Expect currentDirection to not change');
+ }, 'Calling removeTrack with currentDirection sendrecv should set direction to recvonly');
+
+ /*
+ 5.1. removeTrack
+ 7. Set sender.track to null.
+ 11. If transceiver.currentDirection is sendonly set transceiver.direction
+ to inactive.
+ */
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const stream = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const [track] = stream.getTracks();
+ const transceiver = pc.addTransceiver(track, { direction: 'sendonly' });
+ const { sender } = transceiver;
+
+ assert_equals(sender.track, track);
+ assert_equals(transceiver.direction, 'sendonly');
+ assert_equals(transceiver.currentDirection, null);
+
+ const offer = await pc.createOffer();
+ await pc.setLocalDescription(offer);
+ const answer = await generateAnswer(offer);
+ await pc.setRemoteDescription(answer);
+ assert_equals(transceiver.currentDirection, 'sendonly');
+
+ pc.removeTrack(sender);
+ assert_equals(sender.track, null);
+ assert_equals(transceiver.direction, 'inactive');
+ assert_equals(transceiver.currentDirection, 'sendonly',
+ 'Expect currentDirection to not change');
+ }, 'Calling removeTrack with currentDirection sendonly should set direction to inactive');
+
+ /*
+ 5.1. removeTrack
+ 7. Set sender.track to null.
+ 9. If transceiver.currentDirection is recvonly or inactive,
+ then abort these steps.
+ */
+ promise_test(async t => {
+ const caller = new RTCPeerConnection();
+ t.add_cleanup(() => caller.close());
+ const callee = new RTCPeerConnection();
+ t.add_cleanup(() => callee.close());
+ const stream = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const [track] = stream.getTracks();
+ const transceiver = caller.addTransceiver(track, { direction: 'recvonly' });
+ const { sender } = transceiver;
+
+ assert_equals(sender.track, track);
+ assert_equals(transceiver.direction, 'recvonly');
+ assert_equals(transceiver.currentDirection, null);
+
+ const offer = await caller.createOffer();
+ await caller.setLocalDescription(offer);
+ await callee.setRemoteDescription(offer);
+ callee.addTrack(track, stream);
+ const answer = await callee.createAnswer();
+ await callee.setLocalDescription(answer);
+ await caller.setRemoteDescription(answer);
+ assert_equals(transceiver.currentDirection, 'recvonly');
+
+ caller.removeTrack(sender);
+ assert_equals(sender.track, null);
+ assert_equals(transceiver.direction, 'recvonly');
+ assert_equals(transceiver.currentDirection, 'recvonly');
+ }, 'Calling removeTrack with currentDirection recvonly should not change direction');
+
+ /*
+ 5.1. removeTrack
+ 7. Set sender.track to null.
+ 9. If transceiver.currentDirection is recvonly or inactive,
+ then abort these steps.
+ */
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const stream = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const [track] = stream.getTracks();
+ const transceiver = pc.addTransceiver(track, { direction: 'inactive' });
+ const { sender } = transceiver;
+
+ assert_equals(sender.track, track);
+ assert_equals(transceiver.direction, 'inactive');
+ assert_equals(transceiver.currentDirection, null);
+
+ const offer = await pc.createOffer();
+ await pc.setLocalDescription(offer);
+ const answer = await generateAnswer(offer);
+ await pc.setRemoteDescription(answer);
+ assert_equals(transceiver.currentDirection, 'inactive');
+
+ pc.removeTrack(sender);
+ assert_equals(sender.track, null);
+ assert_equals(transceiver.direction, 'inactive');
+ assert_equals(transceiver.currentDirection, 'inactive');
+ }, 'Calling removeTrack with currentDirection inactive should not change direction');
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const stream = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const [track] = stream.getTracks();
+ const sender = pc.addTrack(track, stream);
+
+ pc.getTransceivers()[0].stop();
+ pc.removeTrack(sender);
+ assert_equals(sender.track, track);
+ }, "Calling removeTrack on a stopped transceiver should be a no-op");
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const stream = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const [track] = stream.getTracks();
+ const sender = pc.addTrack(track, stream);
+
+ await sender.replaceTrack(null);
+ pc.removeTrack(sender);
+ assert_equals(sender.track, null);
+}, "Calling removeTrack on a null track should have no effect");
+
+
+ /*
+ TODO
+ 5.1. removeTrack
+ Stops sending media from sender. The RTCRtpSender will still appear
+ in getSenders. Doing so will cause future calls to createOffer to
+ mark the media description for the corresponding transceiver as
+ recvonly or inactive, as defined in [JSEP] (section 5.2.2.).
+
+ When the other peer stops sending a track in this manner, an ended
+ event is fired at the MediaStreamTrack object.
+
+ 6. If sender is not in senders (which indicates that it was removed
+ due to setting an RTCSessionDescription of type "rollback"),
+ then abort these steps.
+ 12. Update the negotiation-needed flag for connection.
+ */
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-restartIce-onnegotiationneeded.https.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-restartIce-onnegotiationneeded.https.html
new file mode 100644
index 0000000000..4dcce45199
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-restartIce-onnegotiationneeded.https.html
@@ -0,0 +1,29 @@
+<!doctype html>
+<meta charset=utf-8>
+<title></title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src="RTCPeerConnection-helper.js"></script>
+<script>
+"use strict";
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+
+ pc1.addTransceiver("audio");
+
+ await pc1.setLocalDescription(await pc1.createOffer());
+ pc1.restartIce();
+ await pc2.setRemoteDescription(pc1.localDescription);
+ await pc2.setLocalDescription(await pc2.createAnswer());
+ await pc1.setRemoteDescription(pc2.localDescription);
+ // When the setRemoteDescription() promise above is resolved a task should be
+ // queued to fire the onnegotiationneeded event. Because of this, we should
+ // have time to hook up the event listener *after* awaiting the SRD promise.
+ await new Promise(r => pc1.onnegotiationneeded = r);
+}, "Negotiation needed when returning to stable does not fire too early");
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-restartIce.https.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-restartIce.https.html
new file mode 100644
index 0000000000..45a04d3a7a
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-restartIce.https.html
@@ -0,0 +1,482 @@
+<!doctype html>
+<meta charset=utf-8>
+<title></title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src="RTCPeerConnection-helper.js"></script>
+<script>
+"use strict";
+
+function getLines(sdp, startsWith) {
+ const lines = sdp.split("\r\n").filter(l => l.startsWith(startsWith));
+ assert_true(lines.length > 0, `One or more ${startsWith} in sdp`);
+ return lines;
+}
+
+const getUfrags = ({sdp}) => getLines(sdp, "a=ice-ufrag:");
+const getPwds = ({sdp}) => getLines(sdp, "a=ice-pwd:");
+
+const negotiators = [
+ {
+ tag: "",
+ async setOffer(pc) {
+ await pc.setLocalDescription(await pc.createOffer());
+ },
+ async setAnswer(pc) {
+ await pc.setLocalDescription(await pc.createAnswer());
+ },
+ },
+ {
+ tag: " (perfect negotiation)",
+ async setOffer(pc) {
+ await pc.setLocalDescription();
+ },
+ async setAnswer(pc) {
+ await pc.setLocalDescription();
+ },
+ },
+];
+
+async function exchangeOfferAnswerEndOnFirst(pc1, pc2, negotiator) {
+ await negotiator.setOffer(pc1);
+ await pc2.setRemoteDescription(pc1.localDescription);
+ await negotiator.setAnswer(pc2);
+ await pc1.setRemoteDescription(pc2.localDescription); // End on pc1. No race
+}
+
+async function exchangeOfferAnswerEndOnSecond(pc1, pc2, negotiator) {
+ await negotiator.setOffer(pc1);
+ await pc2.setRemoteDescription(pc1.localDescription);
+ await pc1.setRemoteDescription(await pc2.createAnswer());
+ await pc2.setLocalDescription(pc1.remoteDescription); // End on pc2. No race
+}
+
+async function assertNoNegotiationNeeded(t, pc, state = "stable") {
+ assert_equals(pc.signalingState, state, `In ${state} state`);
+ const event = await Promise.race([
+ new Promise(r => pc.onnegotiationneeded = r),
+ new Promise(r => t.step_timeout(r, 10))
+ ]);
+ assert_equals(event, undefined, "No negotiationneeded event");
+}
+
+// In Chromium, assert_equals() produces test expectations with the values
+// compared. Because ufrags are different on each run, this would make Chromium
+// test expectations different on each run on tests that failed when comparing
+// ufrags. To work around this problem, assert_ufrags_equals() and
+// assert_ufrags_not_equals() should be preferred over assert_equals() and
+// assert_not_equals().
+function assert_ufrags_equals(x, y, description) {
+ assert_true(x === y, description);
+}
+function assert_ufrags_not_equals(x, y, description) {
+ assert_false(x === y, description);
+}
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ pc.close();
+ pc.restartIce();
+ await assertNoNegotiationNeeded(t, pc, "closed");
+}, "restartIce() has no effect on a closed peer connection");
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+
+ pc1.restartIce();
+ await assertNoNegotiationNeeded(t, pc1);
+ pc1.addTransceiver("audio");
+ await new Promise(r => pc1.onnegotiationneeded = r);
+ await assertNoNegotiationNeeded(t, pc1);
+}, "restartIce() does not trigger negotiation ahead of initial negotiation");
+
+// Run remaining tests twice: once for each negotiator
+
+for (const negotiator of negotiators) {
+ const {tag} = negotiator;
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+
+ pc1.addTransceiver("audio");
+ await new Promise(r => pc1.onnegotiationneeded = r);
+ pc1.restartIce();
+ await exchangeOfferAnswerEndOnFirst(pc1, pc2, negotiator);
+ await assertNoNegotiationNeeded(t, pc1);
+ }, `restartIce() has no effect on initial negotiation${tag}`);
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+
+ pc1.addTransceiver("audio");
+ await new Promise(r => pc1.onnegotiationneeded = r);
+ await exchangeOfferAnswerEndOnFirst(pc1, pc2, negotiator);
+ pc1.restartIce();
+ await new Promise(r => pc1.onnegotiationneeded = r);
+ }, `restartIce() fires negotiationneeded after initial negotiation${tag}`);
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+
+ pc1.addTransceiver("audio");
+ await exchangeOfferAnswerEndOnFirst(pc1, pc2, negotiator);
+
+ const [oldUfrag1] = getUfrags(pc1.localDescription);
+ const [oldUfrag2] = getUfrags(pc2.localDescription);
+ await exchangeOfferAnswerEndOnFirst(pc1, pc2, negotiator);
+ assert_ufrags_equals(getUfrags(pc1.localDescription)[0], oldUfrag1, "control 1");
+ assert_ufrags_equals(getUfrags(pc2.localDescription)[0], oldUfrag2, "control 2");
+
+ pc1.restartIce();
+ await new Promise(r => pc1.onnegotiationneeded = r);
+ await exchangeOfferAnswerEndOnFirst(pc1, pc2, negotiator);
+ const [newUfrag1] = getUfrags(pc1.localDescription);
+ const [newUfrag2] = getUfrags(pc2.localDescription);
+ assert_ufrags_not_equals(newUfrag1, oldUfrag1, "ufrag 1 changed");
+ assert_ufrags_not_equals(newUfrag1, oldUfrag2, "ufrag 2 changed");
+ await assertNoNegotiationNeeded(t, pc1);
+
+ await exchangeOfferAnswerEndOnFirst(pc1, pc2, negotiator);
+ assert_ufrags_equals(getUfrags(pc1.localDescription)[0], newUfrag1, "Unchanged 1");
+ assert_ufrags_equals(getUfrags(pc2.localDescription)[0], newUfrag2, "Unchanged 2");
+ }, `restartIce() causes fresh ufrags${tag}`);
+
+ promise_test(async t => {
+ const config = {bundlePolicy: "max-bundle"};
+ const pc1 = new RTCPeerConnection(config);
+ const pc2 = new RTCPeerConnection(config);
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+
+ pc1.onicecandidate = e => pc2.addIceCandidate(e.candidate);
+ pc2.onicecandidate = e => pc1.addIceCandidate(e.candidate);
+
+ // See the explanation below about Chrome's onnegotiationneeded firing
+ // too early.
+ const negotiationNeededPromise1 =
+ new Promise(r => pc1.onnegotiationneeded = r);
+ pc1.addTransceiver("video");
+ pc1.addTransceiver("audio");
+ await negotiationNeededPromise1;
+ await exchangeOfferAnswerEndOnFirst(pc1, pc2, negotiator);
+
+ const [videoTc, audioTc] = pc1.getTransceivers();
+ const [videoTp, audioTp] =
+ pc1.getTransceivers().map(tc => tc.sender.transport);
+ assert_equals(pc1.getTransceivers().length, 2, 'transceiver count');
+
+ // On Chrome, it is possible (likely, even) that videoTc.sender.transport.state
+ // will be 'connected' by the time we get here. We'll race 2 promises here:
+ // 1. Resolve after onstatechange is called with connected state.
+ // 2. If already connected, resolve immediately.
+ await Promise.race([
+ new Promise(r => videoTc.sender.transport.onstatechange =
+ () => videoTc.sender.transport.state == "connected" && r()),
+ new Promise(r => videoTc.sender.transport.state == "connected" && r())
+ ]);
+ assert_equals(videoTc.sender.transport.state, "connected");
+
+ await exchangeOfferAnswerEndOnFirst(pc1, pc2, negotiator);
+ assert_equals(videoTp, pc1.getTransceivers()[0].sender.transport,
+ 'offer/answer retains dtls transport');
+ assert_equals(audioTp, pc1.getTransceivers()[1].sender.transport,
+ 'offer/answer retains dtls transport');
+
+ const negotiationNeededPromise2 =
+ new Promise(r => pc1.onnegotiationneeded = r);
+ pc1.restartIce();
+ await negotiationNeededPromise2;
+ await exchangeOfferAnswerEndOnFirst(pc1, pc2, negotiator);
+
+ const [newVideoTp, newAudioTp] =
+ pc1.getTransceivers().map(tc => tc.sender.transport);
+ assert_equals(videoTp, newVideoTp, 'ice restart retains dtls transport');
+ assert_equals(audioTp, newAudioTp, 'ice restart retains dtls transport');
+ }, `restartIce() retains dtls transports${tag}`);
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+
+ pc1.addTransceiver("audio");
+ await new Promise(r => pc1.onnegotiationneeded = r);
+ await exchangeOfferAnswerEndOnFirst(pc1, pc2, negotiator);
+
+ const [oldUfrag1] = getUfrags(pc1.localDescription);
+ const [oldUfrag2] = getUfrags(pc2.localDescription);
+
+ await negotiator.setOffer(pc1);
+ pc1.restartIce();
+ await pc2.setRemoteDescription(pc1.localDescription);
+ await negotiator.setAnswer(pc2);
+ // Several tests in this file initializes the onnegotiationneeded listener
+ // before the setLocalDescription() or setRemoteDescription() that we expect
+ // to trigger negotiation needed. This allows Chrome to exercise these tests
+ // without timing out due to a bug that causes onnegotiationneeded to fire too
+ // early.
+ // TODO(https://crbug.com/985797): Once Chrome does not fire ONN too early,
+ // simply do "await new Promise(...)" instead of
+ // "await negotiationNeededPromise" here and in other tests in this file.
+ const negotiationNeededPromise =
+ new Promise(r => pc1.onnegotiationneeded = r);
+ await pc1.setRemoteDescription(pc2.localDescription);
+ assert_ufrags_equals(getUfrags(pc1.localDescription)[0], oldUfrag1, "Unchanged 1");
+ assert_ufrags_equals(getUfrags(pc2.localDescription)[0], oldUfrag2, "Unchanged 2");
+ await negotiationNeededPromise;
+ await exchangeOfferAnswerEndOnFirst(pc1, pc2, negotiator);
+ const [newUfrag1] = getUfrags(pc1.localDescription);
+ const [newUfrag2] = getUfrags(pc2.localDescription);
+ assert_ufrags_not_equals(newUfrag1, oldUfrag1, "ufrag 1 changed");
+ assert_ufrags_not_equals(newUfrag1, oldUfrag2, "ufrag 2 changed");
+ await assertNoNegotiationNeeded(t, pc1);
+ }, `restartIce() works in have-local-offer${tag}`);
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+
+ pc1.addTransceiver("audio");
+ await new Promise(r => pc1.onnegotiationneeded = r);
+ await negotiator.setOffer(pc1);
+ pc1.restartIce();
+ await pc2.setRemoteDescription(pc1.localDescription);
+ await negotiator.setAnswer(pc2);
+ const negotiationNeededPromise =
+ new Promise(r => pc1.onnegotiationneeded = r);
+ await pc1.setRemoteDescription(pc2.localDescription);
+ const [oldUfrag1] = getUfrags(pc1.localDescription);
+ const [oldUfrag2] = getUfrags(pc2.localDescription);
+ await negotiationNeededPromise;
+ await exchangeOfferAnswerEndOnFirst(pc1, pc2, negotiator);
+ const [newUfrag1] = getUfrags(pc1.localDescription);
+ const [newUfrag2] = getUfrags(pc2.localDescription);
+ assert_ufrags_not_equals(newUfrag1, oldUfrag1, "ufrag 1 changed");
+ assert_ufrags_not_equals(newUfrag1, oldUfrag2, "ufrag 2 changed");
+ await assertNoNegotiationNeeded(t, pc1);
+ }, `restartIce() works in initial have-local-offer${tag}`);
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+
+ pc1.addTransceiver("audio");
+ await new Promise(r => pc1.onnegotiationneeded = r);
+ await exchangeOfferAnswerEndOnFirst(pc1, pc2, negotiator);
+
+ const [oldUfrag1] = getUfrags(pc1.localDescription);
+ const [oldUfrag2] = getUfrags(pc2.localDescription);
+
+ await negotiator.setOffer(pc2);
+ await pc1.setRemoteDescription(pc2.localDescription);
+ pc1.restartIce();
+ await pc2.setRemoteDescription(await pc1.createAnswer());
+ const negotiationNeededPromise =
+ new Promise(r => pc1.onnegotiationneeded = r);
+ await pc1.setLocalDescription(pc2.remoteDescription); // End on pc1. No race
+ assert_ufrags_equals(getUfrags(pc1.localDescription)[0], oldUfrag1, "Unchanged 1");
+ assert_ufrags_equals(getUfrags(pc2.localDescription)[0], oldUfrag2, "Unchanged 2");
+ await negotiationNeededPromise;
+ await exchangeOfferAnswerEndOnFirst(pc1, pc2, negotiator);
+ const [newUfrag1] = getUfrags(pc1.localDescription);
+ const [newUfrag2] = getUfrags(pc2.localDescription);
+ assert_ufrags_not_equals(newUfrag1, oldUfrag1, "ufrag 1 changed");
+ assert_ufrags_not_equals(newUfrag1, oldUfrag2, "ufrag 2 changed");
+ await assertNoNegotiationNeeded(t, pc1);
+ }, `restartIce() works in have-remote-offer${tag}`);
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+
+ pc2.addTransceiver("audio");
+ await negotiator.setOffer(pc2);
+ await pc1.setRemoteDescription(pc2.localDescription);
+ pc1.restartIce();
+ await pc2.setRemoteDescription(await pc1.createAnswer());
+ await pc1.setLocalDescription(pc2.remoteDescription); // End on pc1. No race
+ await assertNoNegotiationNeeded(t, pc1);
+ }, `restartIce() does nothing in initial have-remote-offer${tag}`);
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+
+ pc1.addTransceiver("audio");
+ await new Promise(r => pc1.onnegotiationneeded = r);
+ await exchangeOfferAnswerEndOnFirst(pc1, pc2, negotiator);
+
+ const [oldUfrag1] = getUfrags(pc1.localDescription);
+ const [oldUfrag2] = getUfrags(pc2.localDescription);
+
+ pc1.restartIce();
+ await new Promise(r => pc1.onnegotiationneeded = r);
+ const negotiationNeededPromise =
+ new Promise(r => pc1.onnegotiationneeded = r);
+ await exchangeOfferAnswerEndOnSecond(pc2, pc1, negotiator);
+ assert_ufrags_equals(getUfrags(pc1.localDescription)[0], oldUfrag1, "nothing yet 1");
+ assert_ufrags_equals(getUfrags(pc2.localDescription)[0], oldUfrag2, "nothing yet 2");
+ await negotiationNeededPromise;
+ await exchangeOfferAnswerEndOnFirst(pc1, pc2, negotiator);
+ const [newUfrag1] = getUfrags(pc1.localDescription);
+ const [newUfrag2] = getUfrags(pc2.localDescription);
+ assert_ufrags_not_equals(newUfrag1, oldUfrag1, "ufrag 1 changed");
+ assert_ufrags_not_equals(newUfrag2, oldUfrag2, "ufrag 2 changed");
+ await assertNoNegotiationNeeded(t, pc1);
+ }, `restartIce() survives remote offer${tag}`);
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+
+ pc1.addTransceiver("audio");
+ await new Promise(r => pc1.onnegotiationneeded = r);
+ await exchangeOfferAnswerEndOnFirst(pc1, pc2, negotiator);
+
+ const [oldUfrag1] = getUfrags(pc1.localDescription);
+ const [oldUfrag2] = getUfrags(pc2.localDescription);
+
+ pc1.restartIce();
+ pc2.restartIce();
+ await new Promise(r => pc1.onnegotiationneeded = r);
+ await exchangeOfferAnswerEndOnSecond(pc2, pc1, negotiator);
+ const [newUfrag1] = getUfrags(pc1.localDescription);
+ const [newUfrag2] = getUfrags(pc2.localDescription);
+ assert_ufrags_not_equals(newUfrag1, oldUfrag1, "ufrag 1 changed");
+ assert_ufrags_not_equals(newUfrag1, oldUfrag2, "ufrag 2 changed");
+ await assertNoNegotiationNeeded(t, pc1);
+
+ await exchangeOfferAnswerEndOnFirst(pc1, pc2, negotiator);
+ assert_ufrags_equals(getUfrags(pc1.localDescription)[0], newUfrag1, "Unchanged 1");
+ assert_ufrags_equals(getUfrags(pc2.localDescription)[0], newUfrag2, "Unchanged 2");
+ await assertNoNegotiationNeeded(t, pc1);
+ }, `restartIce() is satisfied by remote ICE restart${tag}`);
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+
+ pc1.addTransceiver("audio");
+ await new Promise(r => pc1.onnegotiationneeded = r);
+ await exchangeOfferAnswerEndOnFirst(pc1, pc2, negotiator);
+
+ const [oldUfrag1] = getUfrags(pc1.localDescription);
+ const [oldUfrag2] = getUfrags(pc2.localDescription);
+
+ pc1.restartIce();
+ await new Promise(r => pc1.onnegotiationneeded = r);
+ await pc1.setLocalDescription(await pc1.createOffer({iceRestart: false}));
+ await pc2.setRemoteDescription(pc1.localDescription);
+ await negotiator.setAnswer(pc2);
+ await pc1.setRemoteDescription(pc2.localDescription);
+ const [newUfrag1] = getUfrags(pc1.localDescription);
+ const [newUfrag2] = getUfrags(pc2.localDescription);
+ assert_ufrags_not_equals(newUfrag1, oldUfrag1, "ufrag 1 changed");
+ assert_ufrags_not_equals(newUfrag1, oldUfrag2, "ufrag 2 changed");
+ await assertNoNegotiationNeeded(t, pc1);
+ }, `restartIce() trumps {iceRestart: false}${tag}`);
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+
+ pc1.addTransceiver("audio");
+ await new Promise(r => pc1.onnegotiationneeded = r);
+ await exchangeOfferAnswerEndOnFirst(pc1, pc2, negotiator);
+
+ const [oldUfrag1] = getUfrags(pc1.localDescription);
+ const [oldUfrag2] = getUfrags(pc2.localDescription);
+
+ pc1.restartIce();
+ await new Promise(r => pc1.onnegotiationneeded = r);
+ await negotiator.setOffer(pc1);
+ const negotiationNeededPromise =
+ new Promise(r => pc1.onnegotiationneeded = r);
+ await pc1.setLocalDescription({type: "rollback"});
+ await negotiationNeededPromise;
+ await exchangeOfferAnswerEndOnFirst(pc1, pc2, negotiator);
+ const [newUfrag1] = getUfrags(pc1.localDescription);
+ const [newUfrag2] = getUfrags(pc2.localDescription);
+ assert_ufrags_not_equals(newUfrag1, oldUfrag1, "ufrag 1 changed");
+ assert_ufrags_not_equals(newUfrag1, oldUfrag2, "ufrag 2 changed");
+ await assertNoNegotiationNeeded(t, pc1);
+ }, `restartIce() survives rollback${tag}`);
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection({bundlePolicy: "max-compat"});
+ const pc2 = new RTCPeerConnection({bundlePolicy: "max-compat"});
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+
+ pc1.addTransceiver("audio");
+ pc1.addTransceiver("video");
+ await new Promise(r => pc1.onnegotiationneeded = r);
+ await exchangeOfferAnswerEndOnFirst(pc1, pc2, negotiator);
+
+ const oldUfrags1 = getUfrags(pc1.localDescription);
+ const oldUfrags2 = getUfrags(pc2.localDescription);
+ const oldPwds2 = getPwds(pc2.localDescription);
+
+ pc1.restartIce();
+ await new Promise(r => pc1.onnegotiationneeded = r);
+
+ // Engineer a partial ICE restart from pc2
+ pc2.restartIce();
+ await negotiator.setOffer(pc2);
+ {
+ let {type, sdp} = pc2.localDescription;
+ // Restore both old ice-ufrag and old ice-pwd to trigger a partial restart
+ sdp = sdp.replace(getUfrags({sdp})[0], oldUfrags2[0]);
+ sdp = sdp.replace(getPwds({sdp})[0], oldPwds2[0]);
+ const newUfrags2 = getUfrags({sdp});
+ const newPwds2 = getPwds({sdp});
+ assert_ufrags_equals(newUfrags2[0], oldUfrags2[0], "control ufrag match");
+ assert_ufrags_equals(newPwds2[0], oldPwds2[0], "control pwd match");
+ assert_ufrags_not_equals(newUfrags2[1], oldUfrags2[1], "control ufrag non-match");
+ assert_ufrags_not_equals(newPwds2[1], oldPwds2[1], "control pwd non-match");
+ await pc1.setRemoteDescription({type, sdp});
+ }
+ const negotiationNeededPromise =
+ new Promise(r => pc1.onnegotiationneeded = r);
+ await negotiator.setAnswer(pc1);
+ const newUfrags1 = getUfrags(pc1.localDescription);
+ assert_ufrags_equals(newUfrags1[0], oldUfrags1[0], "Unchanged 1");
+ assert_ufrags_not_equals(newUfrags1[1], oldUfrags1[1], "Restarted 2");
+ await negotiationNeededPromise;
+ await negotiator.setOffer(pc1);
+ const newestUfrags1 = getUfrags(pc1.localDescription);
+ assert_ufrags_not_equals(newestUfrags1[0], oldUfrags1[0], "Restarted 1");
+ assert_ufrags_not_equals(newestUfrags1[1], oldUfrags1[1], "Restarted 2");
+ await assertNoNegotiationNeeded(t, pc1);
+ }, `restartIce() survives remote offer containing partial restart${tag}`);
+
+}
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-setDescription-transceiver.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-setDescription-transceiver.html
new file mode 100644
index 0000000000..9bbab30d56
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-setDescription-transceiver.html
@@ -0,0 +1,295 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerConnection Set Session Description - Transceiver Tests</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="RTCPeerConnection-helper.js"></script>
+<script>
+ 'use strict';
+
+ // Test is based on the following editor draft:
+ // https://w3c.github.io/webrtc-pc/archives/20170605/webrtc.html
+
+ // The following helper functions are called from RTCPeerConnection-helper.js:
+ // generateAnswer
+
+ /*
+ 4.3.2. Interface Definition
+
+ [Constructor(optional RTCConfiguration configuration)]
+ interface RTCPeerConnection : EventTarget {
+ Promise<void> setLocalDescription(
+ RTCSessionDescriptionInit description);
+
+ Promise<void> setRemoteDescription(
+ RTCSessionDescriptionInit description);
+ ...
+ };
+
+ 4.6.2. RTCSessionDescription Class
+ dictionary RTCSessionDescriptionInit {
+ required RTCSdpType type;
+ DOMString sdp = "";
+ };
+
+ 4.6.1. RTCSdpType
+ enum RTCSdpType {
+ "offer",
+ "pranswer",
+ "answer",
+ "rollback"
+ };
+
+ 5.4. RTCRtpTransceiver Interface
+
+ interface RTCRtpTransceiver {
+ readonly attribute DOMString? mid;
+ [SameObject]
+ readonly attribute RTCRtpSender sender;
+ [SameObject]
+ readonly attribute RTCRtpReceiver receiver;
+ readonly attribute RTCRtpTransceiverDirection direction;
+ readonly attribute RTCRtpTransceiverDirection? currentDirection;
+ ...
+ };
+ */
+
+ /*
+ 4.3.1.6. Set the RTCSessionSessionDescription
+ 7. If description is set as a local description, then run the following steps for
+ each media description in description that is not yet associated with an
+ RTCRtpTransceiver object:
+ 1. Let transceiver be the RTCRtpTransceiver used to create the media
+ description.
+ 2. Set transceiver's mid value to the mid of the corresponding media
+ description.
+ */
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const transceiver = pc.addTransceiver('audio');
+ assert_equals(transceiver.mid, null);
+
+ return pc.createOffer()
+ .then(offer => {
+ assert_equals(transceiver.mid, null,
+ 'Expect transceiver.mid to still be null after createOffer');
+
+ return pc.setLocalDescription(offer)
+ .then(() => {
+ assert_equals(typeof transceiver.mid, 'string',
+ 'Expect transceiver.mid to set to valid string value');
+
+ assert_equals(offer.sdp.includes(`\r\na=mid:${transceiver.mid}`), true,
+ 'Expect transceiver mid to be found in offer SDP');
+ });
+ });
+ }, 'setLocalDescription(offer) with m= section should assign mid to corresponding transceiver');
+
+ /*
+ 4.3.1.6. Set the RTCSessionSessionDescription
+ 8. If description is set as a remote description, then run the following steps
+ for each media description in description:
+ 2. If no suitable transceiver is found (transceiver is unset), run the following
+ steps:
+ 1. Create an RTCRtpSender, sender, from the media description.
+ 2. Create an RTCRtpReceiver, receiver, from the media description.
+ 3. Create an RTCRtpTransceiver with sender, receiver and direction, and let
+ transceiver be the result.
+ 3. Set transceiver's mid value to the mid of the corresponding media description.
+ */
+ promise_test(t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+
+ t.add_cleanup(() => pc2.close());
+
+ const transceiver1 = pc1.addTransceiver('audio');
+ assert_array_equals(pc1.getTransceivers(), [transceiver1]);
+ assert_array_equals(pc2.getTransceivers(), []);
+
+ return pc1.createOffer()
+ .then(offer => {
+ return Promise.all([
+ pc1.setLocalDescription(offer),
+ pc2.setRemoteDescription(offer)
+ ])
+ .then(() => {
+ const transceivers = pc2.getTransceivers();
+ assert_equals(transceivers.length, 1,
+ 'Expect new transceiver added to pc2 after setRemoteDescription');
+
+ const [ transceiver2 ] = transceivers;
+
+ assert_equals(typeof transceiver2.mid, 'string',
+ 'Expect transceiver2.mid to be set');
+
+ assert_equals(transceiver1.mid, transceiver2.mid,
+ 'Expect transceivers of both side to have the same mid');
+
+ assert_equals(offer.sdp.includes(`\r\na=mid:${transceiver2.mid}`), true,
+ 'Expect transceiver mid to be found in offer SDP');
+ });
+ });
+ }, 'setRemoteDescription(offer) with m= section and no existing transceiver should create corresponding transceiver');
+
+ /*
+ 4.3.1.6. Set the RTCSessionSessionDescription
+ 9. If description is of type "rollback", then run the following steps:
+ 1. If the mid value of an RTCRtpTransceiver was set to a non-null value by
+ the RTCSessionDescription that is being rolled back, set the mid value
+ of that transceiver to null, as described by [JSEP] (section 4.1.8.2.).
+ */
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const transceiver = pc.addTransceiver('audio');
+ assert_equals(transceiver.mid, null);
+
+ return pc.createOffer()
+ .then(offer => {
+ assert_equals(transceiver.mid, null);
+ return pc.setLocalDescription(offer);
+ })
+ .then(() => {
+ assert_not_equals(transceiver.mid, null);
+ return pc.setLocalDescription({ type: 'rollback' });
+ })
+ .then(() => {
+ assert_equals(transceiver.mid, null,
+ 'Expect transceiver.mid to become null again after rollback');
+ });
+ }, 'setLocalDescription(rollback) should unset transceiver.mid');
+
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const transceiver1 = pc.addTransceiver('audio');
+ assert_equals(transceiver1.mid, null);
+
+ return pc.createOffer()
+ .then(offer =>
+ pc.setLocalDescription(offer)
+ .then(() => generateAnswer(offer)))
+ .then(answer => pc.setRemoteDescription(answer))
+ .then(() => {
+ // pc is back to stable state
+ // create another transceiver
+ const transceiver2 = pc.addTransceiver('video');
+
+ assert_not_equals(transceiver1.mid, null);
+ assert_equals(transceiver2.mid, null);
+
+ return pc.createOffer()
+ .then(offer => pc.setLocalDescription(offer))
+ .then(() => {
+ assert_not_equals(transceiver1.mid, null);
+ assert_not_equals(transceiver2.mid, null,
+ 'Expect transceiver2.mid to become set');
+
+ return pc.setLocalDescription({ type: 'rollback' });
+ })
+ .then(() => {
+ assert_not_equals(transceiver1.mid, null,
+ 'Expect transceiver1.mid to stay set');
+
+ assert_equals(transceiver2.mid, null,
+ 'Expect transceiver2.mid to be rolled back to null');
+ });
+ })
+ }, 'setLocalDescription(rollback) should only unset transceiver mids associated with current round');
+
+ /*
+ 4.3.1.6. Set the RTCSessionSessionDescription
+ 9. If description is of type "rollback", then run the following steps:
+ 2. If an RTCRtpTransceiver was created by applying the RTCSessionDescription
+ that is being rolled back, and a track has not been attached to it via
+ addTrack, remove that transceiver from connection's set of transceivers,
+ as described by [JSEP] (section 4.1.8.2.).
+ */
+ promise_test(t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+
+ t.add_cleanup(() => pc2.close());
+
+ pc1.addTransceiver('audio');
+
+ return pc1.createOffer()
+ .then(offer => pc2.setRemoteDescription(offer))
+ .then(() => {
+ const transceivers = pc2.getTransceivers();
+ assert_equals(transceivers.length, 1);
+ const [ transceiver ] = transceivers;
+
+ assert_equals(typeof transceiver.mid, 'string',
+ 'Expect transceiver.mid to be set');
+
+ return pc2.setRemoteDescription({ type: 'rollback' })
+ .then(() => {
+ assert_equals(transceiver.mid, null,
+ 'Expect transceiver.mid to be unset');
+
+ assert_array_equals(pc2.getTransceivers(), [],
+ `Expect transceiver to be removed from pc2's transceiver list`);
+ });
+ });
+ }, 'setRemoteDescription(rollback) should remove newly created transceiver from transceiver list');
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ pc1.addTransceiver('audio');
+ const offer = await pc1.createOffer();
+ await pc1.setLocalDescription(offer);
+
+ await pc2.setRemoteDescription(offer);
+ pc2.getTransceivers()[0].stop();
+ const answer = await pc2.createAnswer();
+
+ await pc1.setRemoteDescription(answer);
+
+ assert_equals(pc1.getTransceivers()[0].currentDirection, 'inactive', 'A stopped m-line should give an inactive transceiver');
+ }, 'setRemoteDescription should set transceiver inactive if its corresponding m section is rejected');
+
+ /*
+ TODO
+ - Steps for transceiver direction is added to tip of tree draft, but not yet
+ published as editor's draft
+
+ 4.3.1.6. Set the RTCSessionSessionDescription
+ 8. If description is set as a remote description, then run the following steps
+ for each media description in description:
+ 1. As described by [JSEP] (section 5.9.), attempt to find an existing
+ RTCRtpTransceiver object, transceiver, to represent the media description.
+ 3. If the media description has no MID, and transceiver's mid is unset, generate
+ a random value as described in [JSEP] (section 5.9.).
+ 4. If the direction of the media description is sendrecv or sendonly, and
+ transceiver.receiver.track has not yet been fired in a track event, process
+ the remote track for the media description, given transceiver.
+ 5. If the media description is rejected, and transceiver is not already stopped,
+ stop the RTCRtpTransceiver transceiver.
+
+ [JSEP]
+ 5.9. Applying a Remote Description
+ - If the m= section is not associated with any RtpTransceiver
+ (possibly because it was dissociated in the previous step),
+ either find an RtpTransceiver or create one according to the
+ following steps:
+
+ - If the m= section is sendrecv or recvonly, and there are
+ RtpTransceivers of the same type that were added to the
+ PeerConnection by addTrack and are not associated with any
+ m= section and are not stopped, find the first (according to
+ the canonical order described in Section 5.2.1) such
+ RtpTransceiver.
+
+ - If no RtpTransceiver was found in the previous step, create
+ one with a recvonly direction.
+ */
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-setLocalDescription-answer.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-setLocalDescription-answer.html
new file mode 100644
index 0000000000..32e2332635
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-setLocalDescription-answer.html
@@ -0,0 +1,230 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerConnection.prototype.setLocalDescription</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="RTCPeerConnection-helper.js"></script>
+<script>
+ 'use strict';
+
+ // Test is based on the following editor draft:
+ // https://w3c.github.io/webrtc-pc/archives/20170605/webrtc.html
+
+ // The following helper functions are called from RTCPeerConnection-helper.js:
+ // generateAnswer
+ // assert_session_desc_similar
+
+ /*
+ 4.3.2. Interface Definition
+ [Constructor(optional RTCConfiguration configuration)]
+ interface RTCPeerConnection : EventTarget {
+ Promise<void> setRemoteDescription(
+ RTCSessionDescriptionInit description);
+
+ readonly attribute RTCSessionDescription? remoteDescription;
+ readonly attribute RTCSessionDescription? currentRemoteDescription;
+ readonly attribute RTCSessionDescription? pendingRemoteDescription;
+ ...
+ };
+
+ 4.6.2. RTCSessionDescription Class
+ dictionary RTCSessionDescriptionInit {
+ required RTCSdpType type;
+ DOMString sdp = "";
+ };
+
+ 4.6.1. RTCSdpType
+ enum RTCSdpType {
+ "offer",
+ "pranswer",
+ "answer",
+ "rollback"
+ };
+ */
+
+ /*
+ 4.3.1.6. Set the RTCSessionSessionDescription
+ 2.2.2. If description is set as a local description, then run one of the following
+ steps:
+
+ - If description is of type "answer", then this completes an offer answer
+ negotiation.
+
+ Set connection's currentLocalDescription to description and
+ currentRemoteDescription to the value of pendingRemoteDescription.
+
+ Set both pendingRemoteDescription and pendingLocalDescription to null.
+
+ Finally set connection's signaling state to stable.
+ */
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const states = [];
+ pc.addEventListener('signalingstatechange', () => states.push(pc.signalingState));
+
+ return generateVideoReceiveOnlyOffer(pc)
+ .then(offer =>
+ pc.setRemoteDescription(offer)
+ .then(() => pc.createAnswer())
+ .then(answer =>
+ pc.setLocalDescription(answer)
+ .then(() => {
+ assert_equals(pc.signalingState, 'stable');
+ assert_session_desc_similar(pc.localDescription, answer);
+ assert_session_desc_similar(pc.remoteDescription, offer);
+
+ assert_session_desc_similar(pc.currentLocalDescription, answer);
+ assert_session_desc_similar(pc.currentRemoteDescription, offer);
+
+ assert_equals(pc.pendingLocalDescription, null);
+ assert_equals(pc.pendingRemoteDescription, null);
+
+ assert_array_equals(states, ['have-remote-offer', 'stable']);
+ })));
+ }, 'setLocalDescription() with valid answer should succeed');
+
+ /*
+ 4.3.2. setLocalDescription
+ 3. Let lastAnswer be the result returned by the last call to createAnswer.
+ 4. If description.sdp is null and description.type is answer, set description.sdp
+ to lastAnswer.
+ */
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+
+ t.add_cleanup(() => pc.close());
+
+ return generateVideoReceiveOnlyOffer(pc)
+ .then(offer =>
+ pc.setRemoteDescription(offer)
+ .then(() => pc.createAnswer())
+ .then(answer =>
+ pc.setLocalDescription({ type: 'answer' })
+ .then(() => {
+ assert_equals(pc.signalingState, 'stable');
+ assert_session_desc_similar(pc.localDescription, answer);
+ assert_session_desc_similar(pc.remoteDescription, offer);
+
+ assert_session_desc_similar(pc.currentLocalDescription, answer);
+ assert_session_desc_similar(pc.currentRemoteDescription, offer);
+
+ assert_equals(pc.pendingLocalDescription, null);
+ assert_equals(pc.pendingRemoteDescription, null);
+ })));
+ }, 'setLocalDescription() with type answer and null sdp should use lastAnswer generated from createAnswer');
+
+ /*
+ 4.3.2. setLocalDescription
+ 3. Let lastAnswer be the result returned by the last call to createAnswer.
+ 7. If description.type is answer and description.sdp does not match lastAnswer,
+ reject the promise with a newly created InvalidModificationError and abort these
+ steps.
+ */
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+
+ t.add_cleanup(() => pc.close());
+
+ return generateVideoReceiveOnlyOffer(pc)
+ .then(offer =>
+ pc.setRemoteDescription(offer)
+ .then(() => generateAnswer(offer))
+ .then(answer => pc.setLocalDescription(answer))
+ .then(() => t.unreached_func("setLocalDescription should have rejected"),
+ (error) => assert_equals(error.name, 'InvalidModificationError')));
+ }, 'setLocalDescription() with answer not created by own createAnswer() should reject with InvalidModificationError');
+
+ /*
+ 4.3.1.6. Set the RTCSessionSessionDescription
+ 2.3. If the description's type is invalid for the current signaling state of
+ connection, then reject p with a newly created InvalidStateError and abort
+ these steps.
+
+ [jsep]
+ 5.5. If the type is "pranswer" or "answer", the PeerConnection
+ state MUST be either "have-remote-offer" or "have-local-pranswer".
+ */
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+
+ t.add_cleanup(() => pc.close());
+
+ return pc.createOffer()
+ .then(offer =>
+ promise_rejects_dom(t, 'InvalidModificationError',
+ pc.setLocalDescription({ type: 'answer', sdp: offer.sdp })));
+ }, 'Calling setLocalDescription(answer) from stable state should reject with InvalidModificationError');
+
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+
+ t.add_cleanup(() => pc.close());
+
+ return pc.createOffer()
+ .then(offer =>
+ pc.setLocalDescription(offer)
+ .then(() => generateAnswer(offer)))
+ .then(answer =>
+ promise_rejects_dom(t, 'InvalidModificationError',
+ pc.setLocalDescription(answer)));
+ }, 'Calling setLocalDescription(answer) from have-local-offer state should reject with InvalidModificationError');
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ pc1.addTransceiver('audio', { direction: 'recvonly' });
+ const offer = await pc1.createOffer();
+ await pc2.setRemoteDescription(offer);
+ const answer = await pc2.createAnswer(); // [[LastAnswer]] slot set
+ await pc2.setRemoteDescription({type: "rollback"});
+ pc2.addTransceiver('video', { direction: 'recvonly' });
+ await pc2.createOffer(); // [[LastOffer]] slot set
+ await pc2.setRemoteDescription(offer);
+ await pc2.setLocalDescription(answer); // Should check against [[LastAnswer]], not [[LastOffer]]
+ }, "Setting previously generated answer after a call to createOffer should work");
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ pc1.addTransceiver('audio', { direction: 'recvonly' });
+ await pc2.setRemoteDescription(await pc1.createOffer());
+ const answer = await pc2.createAnswer();
+ const sldPromise = pc2.setLocalDescription(answer);
+
+ assert_equals(pc2.signalingState, "have-remote-offer", "signalingState should not be set synchronously after a call to sLD");
+
+ assert_equals(pc2.pendingLocalDescription, null, "pendingLocalDescription should never be set due to sLD(answer)");
+ assert_not_equals(pc2.pendingRemoteDescription, null, "pendingRemoteDescription should not be set synchronously after a call to sLD");
+ assert_equals(pc2.pendingRemoteDescription.type, "offer");
+ assert_equals(pc2.remoteDescription.sdp, pc2.pendingRemoteDescription.sdp);
+ assert_equals(pc2.currentLocalDescription, null, "currentLocalDescription should not be set synchronously after a call to sLD");
+ assert_equals(pc2.currentRemoteDescription, null, "currentRemoteDescription should not be set synchronously after a call to sLD");
+
+ const stablePromise = new Promise(resolve => {
+ pc2.onsignalingstatechange = () => {
+ resolve(pc2.signalingState);
+ }
+ });
+ const raceValue = await Promise.race([stablePromise, sldPromise]);
+ assert_equals(raceValue, "stable", "signalingstatechange event should fire before sLD resolves");
+ assert_equals(pc2.pendingLocalDescription, null, "pendingLocalDescription should never be set due to sLD(answer)");
+ assert_equals(pc2.pendingRemoteDescription, null, "pendingRemoteDescription should be updated before the signalingstatechange event");
+ assert_not_equals(pc2.currentLocalDescription, null, "currentLocalDescription should be updated before the signalingstatechange event");
+ assert_equals(pc2.currentLocalDescription.type, "answer");
+ assert_equals(pc2.currentLocalDescription.sdp, pc2.localDescription.sdp);
+ assert_not_equals(pc2.currentRemoteDescription, null, "currentRemoteDescription should be updated before the signalingstatechange event");
+ assert_equals(pc2.currentRemoteDescription.type, "offer");
+ assert_equals(pc2.currentRemoteDescription.sdp, pc2.remoteDescription.sdp);
+
+ await sldPromise;
+ }, "setLocalDescription(answer) should update internal state with a queued task, in the right order");
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-setLocalDescription-offer.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-setLocalDescription-offer.html
new file mode 100644
index 0000000000..88f1de5ed8
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-setLocalDescription-offer.html
@@ -0,0 +1,229 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerConnection.prototype.setLocalDescription</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="RTCPeerConnection-helper.js"></script>
+<script>
+ 'use strict';
+
+ // Test is based on the following editor draft:
+ // https://w3c.github.io/webrtc-pc/archives/20170605/webrtc.html
+
+ // The following helper functions are called from RTCPeerConnection-helper.js:
+ // generateDataChannelOffer
+ // assert_session_desc_not_similar
+ // assert_session_desc_similar
+
+ /*
+ 4.3.2. Interface Definition
+ [Constructor(optional RTCConfiguration configuration)]
+ interface RTCPeerConnection : EventTarget {
+ Promise<void> setRemoteDescription(
+ RTCSessionDescriptionInit description);
+
+ readonly attribute RTCSessionDescription? remoteDescription;
+ readonly attribute RTCSessionDescription? currentRemoteDescription;
+ readonly attribute RTCSessionDescription? pendingRemoteDescription;
+ ...
+ };
+
+ 4.6.2. RTCSessionDescription Class
+ dictionary RTCSessionDescriptionInit {
+ required RTCSdpType type;
+ DOMString sdp = "";
+ };
+
+ 4.6.1. RTCSdpType
+ enum RTCSdpType {
+ "offer",
+ "pranswer",
+ "answer",
+ "rollback"
+ };
+ */
+
+ /*
+ 4.3.2. setLocalDescription
+ 2. Let lastOffer be the result returned by the last call to createOffer.
+ 5. If description.sdp is null and description.type is offer, set description.sdp
+ to lastOffer.
+
+ 4.3.1.6. Set the RTCSessionSessionDescription
+ 2.2.2. If description is set as a local description, then run one of the following
+ steps:
+ - If description is of type "offer", set connection.pendingLocalDescription
+ to description and signaling state to have-local-offer.
+ */
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const states = [];
+ pc.addEventListener('signalingstatechange', () => states.push(pc.signalingState));
+
+ return generateAudioReceiveOnlyOffer(pc)
+ .then(offer =>
+ pc.setLocalDescription(offer)
+ .then(() => {
+ assert_equals(pc.signalingState, 'have-local-offer');
+ assert_session_desc_similar(pc.localDescription, offer);
+ assert_session_desc_similar(pc.pendingLocalDescription, offer);
+ assert_equals(pc.currentLocalDescription, null);
+
+ assert_array_equals(states, ['have-local-offer']);
+ }));
+ }, 'setLocalDescription with valid offer should succeed');
+
+ /*
+ 4.3.2. setLocalDescription
+ 2. Let lastOffer be the result returned by the last call to createOffer.
+ 5. If description.sdp is null and description.type is offer, set description.sdp
+ to lastOffer.
+ */
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ return generateAudioReceiveOnlyOffer(pc)
+ .then(offer =>
+ pc.setLocalDescription({ type: 'offer' })
+ .then(() => {
+ assert_equals(pc.signalingState, 'have-local-offer');
+ assert_session_desc_similar(pc.localDescription, offer);
+ assert_session_desc_similar(pc.pendingLocalDescription, offer);
+ assert_equals(pc.currentLocalDescription, null);
+ }));
+ }, 'setLocalDescription with type offer and null sdp should use lastOffer generated from createOffer');
+
+ /*
+ 4.3.2. setLocalDescription
+ 2. Let lastOffer be the result returned by the last call to createOffer.
+ 6. If description.type is offer and description.sdp does not match lastOffer,
+ reject the promise with a newly created InvalidModificationError and abort
+ these steps.
+ */
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const pc2 = new RTCPeerConnection();
+
+ t.add_cleanup(() => pc2.close());
+
+ return generateDataChannelOffer(pc)
+ .then(offer => pc2.setLocalDescription(offer))
+ .then(() => t.unreached_func("setLocalDescription should have rejected"),
+ (error) => assert_equals(error.name, 'InvalidModificationError'));
+ }, 'setLocalDescription() with offer not created by own createOffer() should reject with InvalidModificationError');
+
+ promise_test(t => {
+ // Create first offer with audio line, then second offer with
+ // both audio and video line. Since the second offer is the
+ // last offer, setLocalDescription would reject when setting
+ // with the first offer
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ return generateAudioReceiveOnlyOffer(pc)
+ .then(offer1 =>
+ generateVideoReceiveOnlyOffer(pc)
+ .then(offer2 => {
+ assert_session_desc_not_similar(offer1, offer2);
+ return promise_rejects_dom(t, 'InvalidModificationError',
+ pc.setLocalDescription(offer1));
+ }));
+ }, 'Set created offer other than last offer should reject with InvalidModificationError');
+
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const states = [];
+ pc.addEventListener('signalingstatechange', () => states.push(pc.signalingState));
+
+ return generateAudioReceiveOnlyOffer(pc)
+ .then(offer1 =>
+ pc.setLocalDescription(offer1)
+ .then(() =>
+ generateVideoReceiveOnlyOffer(pc)
+ .then(offer2 =>
+ pc.setLocalDescription(offer2)
+ .then(() => {
+ assert_session_desc_not_similar(offer1, offer2);
+ assert_equals(pc.signalingState, 'have-local-offer');
+ assert_session_desc_similar(pc.localDescription, offer2);
+ assert_session_desc_similar(pc.pendingLocalDescription, offer2);
+ assert_equals(pc.currentLocalDescription, null);
+
+ assert_array_equals(states, ['have-local-offer']);
+ }))));
+ }, 'Creating and setting offer multiple times should succeed');
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ pc1.addTransceiver('audio', { direction: 'recvonly' });
+ const offer = await pc1.createOffer(); // [[LastOffer]] set
+ pc2.addTransceiver('video', { direction: 'recvonly' });
+ const offer2 = await pc2.createOffer();
+ await pc1.setRemoteDescription(offer2);
+ await pc1.createAnswer(); // [[LastAnswer]] set
+ await pc1.setRemoteDescription({type: "rollback"});
+ await pc1.setLocalDescription(offer);
+ }, "Setting previously generated offer after a call to createAnswer should work");
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ pc1.addTransceiver('audio', { direction: 'recvonly' });
+ await pc1.setLocalDescription(await pc1.createOffer());
+
+ const offer = await pc1.createOffer();
+ await pc1.setLocalDescription(offer);
+ await pc2.setRemoteDescription(offer);
+ const answer = await pc2.createAnswer();
+ await pc2.setLocalDescription(answer);
+ await pc1.setRemoteDescription(answer);
+
+ assert_equals(pc1.getTransceivers().length, 1);
+ assert_equals(pc1.getTransceivers()[0].receiver.track.kind, "audio");
+ assert_equals(pc2.getTransceivers().length, 1);
+ assert_equals(pc2.getTransceivers()[0].receiver.track.kind, "audio");
+ }, "Negotiation works when there has been a repeated setLocalDescription(offer)");
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ pc.addTransceiver('audio', { direction: 'recvonly' });
+ const sldPromise = pc.setLocalDescription(await pc.createOffer());
+
+ assert_equals(pc.signalingState, "stable", "signalingState should not be set synchronously after a call to sLD");
+
+ assert_equals(pc.pendingLocalDescription, null, "pendingRemoteDescription should never be set due to sLD");
+ assert_equals(pc.pendingRemoteDescription, null, "pendingLocalDescription should not be set synchronously after a call to sLD");
+ assert_equals(pc.currentLocalDescription, null, "currentLocalDescription should not be set synchronously after a call to sLD");
+ assert_equals(pc.currentRemoteDescription, null, "currentRemoteDescription should not be set synchronously after a call to sLD");
+
+ const statePromise = new Promise(resolve => {
+ pc.onsignalingstatechange = () => {
+ resolve(pc.signalingState);
+ }
+ });
+ const raceValue = await Promise.race([statePromise, sldPromise]);
+ assert_equals(raceValue, "have-local-offer", "signalingstatechange event should fire before sLD resolves");
+ assert_equals(pc.pendingRemoteDescription, null, "pendingRemoteDescription should never be set due to sLD");
+ assert_not_equals(pc.pendingLocalDescription, null, "pendingLocalDescription should be updated before the signalingstatechange event");
+ assert_equals(pc.pendingLocalDescription.type, "offer");
+ assert_equals(pc.pendingLocalDescription.sdp, pc.localDescription.sdp);
+ assert_equals(pc.currentLocalDescription, null, "currentLocalDescription should never be updated due to sLD(offer)");
+ assert_equals(pc.currentRemoteDescription, null, "currentRemoteDescription should never be updated due to sLD(offer)");
+
+ await sldPromise;
+ }, "setLocalDescription(offer) should update internal state with a queued task, in the right order");
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-setLocalDescription-parameterless.https.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-setLocalDescription-parameterless.https.html
new file mode 100644
index 0000000000..5a7a76319a
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-setLocalDescription-parameterless.https.html
@@ -0,0 +1,170 @@
+<!doctype html>
+<meta charset=utf-8>
+<title></title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src="RTCPeerConnection-helper.js"></script>
+<script>
+"use strict";
+
+const kSmallTimeoutMs = 100;
+
+promise_test(async t => {
+ const offerer = new RTCPeerConnection();
+ t.add_cleanup(() => offerer.close());
+
+ const signalingStateChangeEvent
+ = new EventWatcher(t, offerer, 'signalingstatechange')
+ .wait_for('signalingstatechange');
+ await offerer.setLocalDescription();
+ await signalingStateChangeEvent;
+ assert_equals(offerer.signalingState, 'have-local-offer');
+}, "Parameterless SLD() in 'stable' goes to 'have-local-offer'");
+
+promise_test(async t => {
+ const offerer = new RTCPeerConnection();
+ t.add_cleanup(() => offerer.close());
+
+ await offerer.setLocalDescription();
+ assert_not_equals(offerer.pendingLocalDescription, null);
+}, "Parameterless SLD() in 'stable' sets pendingLocalDescription");
+
+promise_test(async t => {
+ const offerer = new RTCPeerConnection();
+ t.add_cleanup(() => offerer.close());
+
+ const transceiver = offerer.addTransceiver('audio');
+ assert_equals(transceiver.mid, null);
+ await offerer.setLocalDescription();
+ assert_not_equals(transceiver.mid, null);
+}, "Parameterless SLD() in 'stable' assigns transceiver.mid");
+
+promise_test(async t => {
+ const offerer = new RTCPeerConnection();
+ t.add_cleanup(() => offerer.close());
+ const answerer = new RTCPeerConnection();
+ t.add_cleanup(() => answerer.close());
+
+ await answerer.setRemoteDescription(await offerer.createOffer());
+ const signalingStateChangeEvent
+ = new EventWatcher(t, answerer, 'signalingstatechange')
+ .wait_for('signalingstatechange');
+ await answerer.setLocalDescription();
+ await signalingStateChangeEvent;
+ assert_equals(answerer.signalingState, 'stable');
+}, "Parameterless SLD() in 'have-remote-offer' goes to 'stable'");
+
+promise_test(async t => {
+ const offerer = new RTCPeerConnection();
+ t.add_cleanup(() => offerer.close());
+ const answerer = new RTCPeerConnection();
+ t.add_cleanup(() => answerer.close());
+
+ await answerer.setRemoteDescription(await offerer.createOffer());
+ await answerer.setLocalDescription();
+ assert_not_equals(answerer.currentLocalDescription, null);
+}, "Parameterless SLD() in 'have-remote-offer' sets currentLocalDescription");
+
+promise_test(async t => {
+ const offerer = new RTCPeerConnection();
+ t.add_cleanup(() => offerer.close());
+ const answerer = new RTCPeerConnection();
+ t.add_cleanup(() => answerer.close());
+
+ offerer.addTransceiver('audio');
+ const onTransceiverPromise = new Promise(resolve =>
+ answerer.ontrack = e => resolve(e.transceiver));
+ await answerer.setRemoteDescription(await offerer.createOffer());
+ const transceiver = await onTransceiverPromise;
+ await answerer.setLocalDescription();
+ assert_equals(transceiver.currentDirection, 'recvonly');
+}, "Parameterless SLD() in 'have-remote-offer' sets " +
+ "transceiver.currentDirection");
+
+promise_test(async t => {
+ const offerer = new RTCPeerConnection();
+ t.add_cleanup(() => offerer.close());
+
+ const offer = await offerer.createOffer();
+ await offerer.setLocalDescription();
+ // assert_true() is used rather than assert_equals() so that if the assertion
+ // fails, the -expected.txt file is not different on each run.
+ assert_true(offerer.pendingLocalDescription.sdp == offer.sdp,
+ "offerer.pendingLocalDescription.sdp == offer.sdp");
+}, "Parameterless SLD() uses [[LastCreatedOffer]] if it is still valid");
+
+promise_test(async t => {
+ const offerer = new RTCPeerConnection();
+ t.add_cleanup(() => offerer.close());
+ const answerer = new RTCPeerConnection();
+ t.add_cleanup(() => answerer.close());
+
+ await answerer.setRemoteDescription(await offerer.createOffer());
+ const answer = await answerer.createAnswer();
+ await answerer.setLocalDescription();
+ // assert_true() is used rather than assert_equals() so that if the assertion
+ // fails, the -expected.txt file is not different on each run.
+ assert_true(answerer.currentLocalDescription.sdp == answer.sdp,
+ "answerer.currentLocalDescription.sdp == answer.sdp");
+}, "Parameterless SLD() uses [[LastCreatedAnswer]] if it is still valid");
+
+promise_test(async t => {
+ const offerer = new RTCPeerConnection();
+ offerer.close();
+ try {
+ await offerer.setLocalDescription();
+ assert_not_reached();
+ } catch (e) {
+ assert_equals(e.name, "InvalidStateError");
+ }
+}, "Parameterless SLD() rejects with InvalidStateError if already closed");
+
+promise_test(async t => {
+ const offerer = new RTCPeerConnection();
+ t.add_cleanup(() => offerer.close());
+
+ const p = Promise.race([
+ offerer.setLocalDescription(),
+ new Promise(r => t.step_timeout(() => r("timeout"), kSmallTimeoutMs))
+ ]);
+ offerer.close();
+ assert_equals(await p, "timeout");
+}, "Parameterless SLD() never settles if closed while pending");
+
+promise_test(async t => {
+ const offerer = new RTCPeerConnection();
+ t.add_cleanup(() => offerer.close());
+ const answerer = new RTCPeerConnection();
+ t.add_cleanup(() => answerer.close());
+
+ // Implicitly create an offer.
+ await offerer.setLocalDescription();
+ await answerer.setRemoteDescription(offerer.pendingLocalDescription);
+ // Implicitly create an answer.
+ await answerer.setLocalDescription();
+ await offerer.setRemoteDescription(answerer.currentLocalDescription);
+}, "Parameterless SLD() in a full O/A exchange succeeds");
+
+promise_test(async t => {
+ const answerer = new RTCPeerConnection();
+ try {
+ await answerer.setRemoteDescription();
+ assert_not_reached();
+ } catch (e) {
+ assert_equals(e.name, "TypeError");
+ }
+}, "Parameterless SRD() rejects with TypeError.");
+
+promise_test(async t => {
+ const offerer = new RTCPeerConnection();
+ const {sdp} = await offerer.createOffer();
+ new RTCSessionDescription({type: "offer", sdp});
+ try {
+ new RTCSessionDescription({sdp});
+ assert_not_reached();
+ } catch (e) {
+ assert_equals(e.name, "TypeError");
+ }
+}, "RTCSessionDescription constructed without type throws TypeError");
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-setLocalDescription-pranswer.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-setLocalDescription-pranswer.html
new file mode 100644
index 0000000000..01845f09b1
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-setLocalDescription-pranswer.html
@@ -0,0 +1,166 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerConnection.prototype.setLocalDescription pranswer</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="RTCPeerConnection-helper.js"></script>
+<script>
+ 'use strict';
+
+ // Test is based on the following editor draft:
+ // https://w3c.github.io/webrtc-pc/archives/20170605/webrtc.html
+
+ // The following helper functions are called from RTCPeerConnection-helper.js:
+ // assert_session_desc_similar
+
+ /*
+ 4.3.2. Interface Definition
+ [Constructor(optional RTCConfiguration configuration)]
+ interface RTCPeerConnection : EventTarget {
+ Promise<void> setLocalDescription(
+ RTCSessionDescriptionInit description);
+
+ readonly attribute RTCSessionDescription? localDescription;
+ readonly attribute RTCSessionDescription? currentLocalDescription;
+ readonly attribute RTCSessionDescription? pendingLocalDescription;
+
+ Promise<void> setRemoteDescription(
+ RTCSessionDescriptionInit description);
+
+ readonly attribute RTCSessionDescription? remoteDescription;
+ readonly attribute RTCSessionDescription? currentRemoteDescription;
+ readonly attribute RTCSessionDescription? pendingRemoteDescription;
+ ...
+ };
+
+ 4.6.2. RTCSessionDescription Class
+ dictionary RTCSessionDescriptionInit {
+ required RTCSdpType type;
+ DOMString sdp = "";
+ };
+
+ 4.6.1. RTCSdpType
+ enum RTCSdpType {
+ "offer",
+ "pranswer",
+ "answer",
+ "rollback"
+ };
+ */
+
+ /*
+ 4.3.1.6. Set the RTCSessionSessionDescription
+ 2.3. If the description's type is invalid for the current signaling state of
+ connection, then reject p with a newly created InvalidStateError and abort
+ these steps.
+
+ [jsep]
+ 5.5. If the type is "pranswer" or "answer", the PeerConnection
+ state MUST be either "have-remote-offer" or "have-local-pranswer".
+ */
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+
+ t.add_cleanup(() => pc.close());
+
+ return pc.createOffer()
+ .then(offer =>
+ promise_rejects_dom(t, 'InvalidStateError',
+ pc.setLocalDescription({ type: 'pranswer', sdp: offer.sdp })));
+ }, 'setLocalDescription(pranswer) from stable state should reject with InvalidStateError');
+
+ /*
+ 4.3.1.6 Set the RTCSessionSessionDescription
+ 2.2.2. If description is set as a local description, then run one of the
+ following steps:
+ - If description is of type "pranswer", then set
+ connection.pendingLocalDescription to description and signaling state to
+ have-local-pranswer.
+ */
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const states = [];
+ pc.addEventListener('signalingstatechange', () => states.push(pc.signalingState));
+
+ return generateVideoReceiveOnlyOffer(pc)
+ .then(offer =>
+ pc.setRemoteDescription(offer)
+ .then(() => pc.createAnswer())
+ .then(answer => {
+ const pranswer = { type: 'pranswer', sdp: answer.sdp };
+
+ return pc.setLocalDescription(pranswer)
+ .then(() => {
+ assert_equals(pc.signalingState, 'have-local-pranswer');
+
+ assert_session_desc_similar(pc.remoteDescription, offer);
+ assert_session_desc_similar(pc.pendingRemoteDescription, offer);
+ assert_equals(pc.currentRemoteDescription, null);
+
+ assert_session_desc_similar(pc.localDescription, pranswer);
+ assert_session_desc_similar(pc.pendingLocalDescription, pranswer);
+ assert_equals(pc.currentLocalDescription, null);
+
+
+ assert_array_equals(states, ['have-remote-offer', 'have-local-pranswer']);
+ });
+ }));
+ }, 'setLocalDescription(pranswer) should succeed');
+
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const states = [];
+ pc.addEventListener('signalingstatechange', () => states.push(pc.signalingState));
+
+ return generateVideoReceiveOnlyOffer(pc)
+ .then(offer =>
+ pc.setRemoteDescription(offer)
+ .then(() => pc.createAnswer())
+ .then(answer => {
+ const pranswer = { type: 'pranswer', sdp: answer.sdp };
+
+ return pc.setLocalDescription(pranswer)
+ .then(() => pc.setLocalDescription(pranswer))
+ .then(() => {
+ assert_array_equals(states, ['have-remote-offer', 'have-local-pranswer']);
+ });
+ }));
+ }, 'setLocalDescription(pranswer) can be applied multiple times while still in have-local-pranswer');
+
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const states = [];
+ pc.addEventListener('signalingstatechange', () => states.push(pc.signalingState));
+
+ return generateVideoReceiveOnlyOffer(pc)
+ .then(offer =>
+ pc.setRemoteDescription(offer)
+ .then(() => pc.createAnswer())
+ .then(answer => {
+ const pranswer = { type: 'pranswer', sdp: answer.sdp };
+
+ return pc.setLocalDescription(pranswer)
+ .then(() => pc.setLocalDescription(answer))
+ .then(() => {
+ assert_equals(pc.signalingState, 'stable');
+ assert_session_desc_similar(pc.localDescription, answer);
+ assert_session_desc_similar(pc.remoteDescription, offer);
+
+ assert_session_desc_similar(pc.currentLocalDescription, answer);
+ assert_session_desc_similar(pc.currentRemoteDescription, offer);
+
+ assert_equals(pc.pendingLocalDescription, null);
+ assert_equals(pc.pendingRemoteDescription, null);
+
+ assert_array_equals(states, ['have-remote-offer', 'have-local-pranswer', 'stable']);
+ });
+ }));
+ }, 'setLocalDescription(answer) from have-local-pranswer state should succeed');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-setLocalDescription-rollback.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-setLocalDescription-rollback.html
new file mode 100644
index 0000000000..787edc92e7
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-setLocalDescription-rollback.html
@@ -0,0 +1,167 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerConnection.prototype.setLocalDescription rollback</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="RTCPeerConnection-helper.js"></script>
+<script>
+ 'use strict';
+
+ // Test is based on the following editor draft:
+ // https://w3c.github.io/webrtc-pc/archives/20170605/webrtc.html
+
+ // The following helper functions are called from RTCPeerConnection-helper.js:
+ // assert_session_desc_similar
+ // generateAudioReceiveOnlyOffer
+
+ /*
+ 4.3.2. Interface Definition
+ [Constructor(optional RTCConfiguration configuration)]
+ interface RTCPeerConnection : EventTarget {
+ Promise<void> setLocalDescription(
+ RTCSessionDescriptionInit description);
+
+ readonly attribute RTCSessionDescription? localDescription;
+ readonly attribute RTCSessionDescription? currentLocalDescription;
+ readonly attribute RTCSessionDescription? pendingLocalDescription;
+
+ Promise<void> setRemoteDescription(
+ RTCSessionDescriptionInit description);
+
+ readonly attribute RTCSessionDescription? remoteDescription;
+ readonly attribute RTCSessionDescription? currentRemoteDescription;
+ readonly attribute RTCSessionDescription? pendingRemoteDescription;
+ ...
+ };
+
+ 4.6.2. RTCSessionDescription Class
+ dictionary RTCSessionDescriptionInit {
+ required RTCSdpType type;
+ DOMString sdp = "";
+ };
+
+ 4.6.1. RTCSdpType
+ enum RTCSdpType {
+ "offer",
+ "pranswer",
+ "answer",
+ "rollback"
+ };
+ */
+
+ /*
+ 4.3.1.6. Set the RTCSessionSessionDescription
+ 2.2.2. If description is set as a local description, then run one of the
+ following steps:
+ - If description is of type "rollback", then this is a rollback. Set
+ connection.pendingLocalDescription to null and signaling state to stable.
+ */
+ promise_test(t=> {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const states = [];
+ pc.addEventListener('signalingstatechange', () => states.push(pc.signalingState));
+
+ return pc.createOffer()
+ .then(offer => pc.setLocalDescription(offer))
+ .then(() => {
+ assert_equals(pc.signalingState, 'have-local-offer');
+ assert_not_equals(pc.localDescription, null);
+ assert_not_equals(pc.pendingLocalDescription, null);
+ assert_equals(pc.currentLocalDescription, null);
+
+ return pc.setLocalDescription({ type: 'rollback' });
+ })
+ .then(() => {
+ assert_equals(pc.signalingState, 'stable');
+ assert_equals(pc.localDescription, null);
+ assert_equals(pc.pendingLocalDescription, null);
+ assert_equals(pc.currentLocalDescription, null);
+
+ assert_array_equals(states, ['have-local-offer', 'stable']);
+ });
+ }, 'setLocalDescription(rollback) from have-local-offer state should reset back to stable state');
+
+ /*
+ 4.3.1.6. Set the RTCSessionSessionDescription
+ 2.3. If the description's type is invalid for the current signaling state of
+ connection, then reject p with a newly created InvalidStateError and abort
+ these steps. Note that this implies that once the answerer has performed
+ setLocalDescription with his answer, this cannot be rolled back.
+
+ [jsep]
+ 4.1.8.2. Rollback
+ - Rollback can only be used to cancel proposed changes;
+ there is no support for rolling back from a stable state to a
+ previous stable state
+ */
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ return promise_rejects_dom(t, 'InvalidStateError',
+ pc.setLocalDescription({ type: 'rollback' }));
+ }, `setLocalDescription(rollback) from stable state should reject with InvalidStateError`);
+
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ return generateAudioReceiveOnlyOffer(pc)
+ .then(offer =>
+ pc.setRemoteDescription(offer)
+ .then(() => pc.createAnswer()))
+ .then(answer => pc.setLocalDescription(answer))
+ .then(() => {
+ return promise_rejects_dom(t, 'InvalidStateError',
+ pc.setLocalDescription({ type: 'rollback' }));
+ });
+ }, `setLocalDescription(rollback) after setting answer description should reject with InvalidStateError`);
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const offer = await generateAudioReceiveOnlyOffer(pc);
+ await pc.setRemoteDescription(offer);
+ await promise_rejects_dom(t, 'InvalidStateError', pc.setLocalDescription({ type: 'rollback' }));
+ }, `setLocalDescription(rollback) after setting a remote offer should reject with InvalidStateError`);
+
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ return pc.createOffer()
+ .then(offer => pc.setLocalDescription(offer))
+ .then(() => pc.setLocalDescription({
+ type: 'rollback',
+ sdp: '!<Invalid SDP Content>;'
+ }));
+ }, `setLocalDescription(rollback) should ignore invalid sdp content and succeed`);
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ pc.addTransceiver('audio', { direction: 'recvonly' });
+ await pc.setLocalDescription(await pc.createOffer());
+ const sldPromise = pc.setLocalDescription({type: "rollback"});
+
+ assert_equals(pc.signalingState, "have-local-offer", "signalingState should not be set synchronously after a call to sLD");
+
+ assert_not_equals(pc.pendingLocalDescription, null, "pendingLocalDescription should not be set synchronously after a call to sLD");
+ assert_equals(pc.pendingLocalDescription.type, "offer");
+ assert_equals(pc.pendingLocalDescription.sdp, pc.localDescription.sdp);
+ assert_equals(pc.pendingRemoteDescription, null, "pendingRemoteDescription should never be set due to sLD(offer)");
+
+ const stablePromise = new Promise(resolve => {
+ pc.onsignalingstatechange = () => {
+ resolve(pc.signalingState);
+ }
+ });
+ const raceValue = await Promise.race([stablePromise, sldPromise]);
+ assert_equals(raceValue, "stable", "signalingstatechange event should fire before sLD resolves");
+ assert_equals(pc.pendingLocalDescription, null, "pendingLocalDescription should be updated before the signalingstatechange event");
+ assert_equals(pc.pendingRemoteDescription, null, "pendingRemoteDescription should never be set due to sLD(offer)");
+
+ await sldPromise;
+ }, "setLocalDescription(rollback) should update internal state with a queued tassk, in the right order");
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-setLocalDescription.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-setLocalDescription.html
new file mode 100644
index 0000000000..c4671c3008
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-setLocalDescription.html
@@ -0,0 +1,152 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerConnection.prototype.setLocalDescription</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="RTCPeerConnection-helper.js"></script>
+<script>
+ 'use strict';
+
+ // Test is based on the following editor draft:
+ // https://w3c.github.io/webrtc-pc/archives/20170605/webrtc.html
+
+ // The following helper functions are called from RTCPeerConnection-helper.js:
+ // generateDataChannelOffer
+ // assert_session_desc_not_similar
+ // assert_session_desc_similar
+
+ /*
+ 4.3.2. Interface Definition
+ [Constructor(optional RTCConfiguration configuration)]
+ interface RTCPeerConnection : EventTarget {
+ Promise<void> setRemoteDescription(
+ RTCSessionDescriptionInit description);
+
+ readonly attribute RTCSessionDescription? remoteDescription;
+ readonly attribute RTCSessionDescription? currentRemoteDescription;
+ readonly attribute RTCSessionDescription? pendingRemoteDescription;
+ ...
+ };
+
+ 4.6.2. RTCSessionDescription Class
+ dictionary RTCSessionDescriptionInit {
+ required RTCSdpType type;
+ DOMString sdp = "";
+ };
+
+ 4.6.1. RTCSdpType
+ enum RTCSdpType {
+ "offer",
+ "pranswer",
+ "answer",
+ "rollback"
+ };
+ */
+
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const states = [];
+ pc.addEventListener('signalingstatechange', () => states.push(pc.signalingState));
+
+ return generateAudioReceiveOnlyOffer(pc)
+ .then(offer1 =>
+ pc.setLocalDescription(offer1)
+ .then(() => generateAnswer(offer1))
+ .then(answer => pc.setRemoteDescription(answer))
+ .then(() => {
+ pc.createDataChannel('test');
+ return generateVideoReceiveOnlyOffer(pc);
+ })
+ .then(offer2 =>
+ pc.setLocalDescription(offer2)
+ .then(() => {
+ assert_equals(pc.signalingState, 'have-local-offer');
+ assert_session_desc_not_similar(offer1, offer2);
+ assert_session_desc_similar(pc.localDescription, offer2);
+ assert_session_desc_similar(pc.currentLocalDescription, offer1);
+ assert_session_desc_similar(pc.pendingLocalDescription, offer2);
+
+ assert_array_equals(states, ['have-local-offer', 'stable', 'have-local-offer']);
+ })));
+ }, 'Calling createOffer() and setLocalDescription() again after one round of local-offer/remote-answer should succeed');
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const states = [];
+ pc1.addEventListener('signalingstatechange', () => states.push(pc1.signalingState));
+
+ assert_equals(pc1.localDescription, null);
+ assert_equals(pc1.currentLocalDescription, null);
+ assert_equals(pc1.pendingLocalDescription, null);
+
+ pc1.createDataChannel('test');
+ const offer = await pc1.createOffer();
+
+ assert_equals(pc1.localDescription, null);
+ assert_equals(pc1.currentLocalDescription, null);
+ assert_equals(pc1.pendingLocalDescription, null);
+
+ await pc1.setLocalDescription(offer);
+
+ assert_session_desc_similar(pc1.localDescription, offer);
+ assert_equals(pc1.currentLocalDescription, null);
+ assert_session_desc_similar(pc1.pendingLocalDescription, offer);
+
+ await pc2.setRemoteDescription(offer);
+ const answer = await pc2.createAnswer();
+ await pc2.setLocalDescription(answer);
+ await pc1.setRemoteDescription(answer);
+
+ assert_equals(pc1.signalingState, 'stable');
+ assert_session_desc_similar(pc1.localDescription, offer);
+ assert_session_desc_similar(pc1.currentLocalDescription, offer);
+ assert_equals(pc1.pendingLocalDescription, null);
+
+ const stream = await getNoiseStream({audio:true});
+ pc2.addTrack(stream.getTracks()[0], stream);
+
+ const reoffer = await pc2.createOffer();
+ await pc2.setLocalDescription(reoffer);
+ await pc1.setRemoteDescription(reoffer);
+ const reanswer = await pc1.createAnswer();
+ await pc1.setLocalDescription(reanswer);
+
+ assert_session_desc_similar(pc1.localDescription, reanswer);
+ assert_session_desc_similar(pc1.currentLocalDescription, reanswer);
+ assert_equals(pc1.pendingLocalDescription, null);
+ }, 'Switching role from answerer to offerer after going back to stable state should succeed');
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const offer = await pc.createOffer();
+ let eventSequence = '';
+ const signalingstatechangeResolver = new Resolver();
+ pc.onsignalingstatechange = () => {
+ eventSequence += 'onsignalingstatechange;';
+ signalingstatechangeResolver.resolve();
+ };
+ await pc.setLocalDescription(offer);
+ eventSequence += 'setLocalDescription;';
+ await signalingstatechangeResolver;
+ assert_equals(eventSequence, 'onsignalingstatechange;setLocalDescription;');
+ }, 'onsignalingstatechange fires before setLocalDescription resolves');
+
+ /*
+ TODO
+ 4.3.2. setLocalDescription
+ 4. If description.sdp is null and description.type is pranswer, set description.sdp
+ to lastAnswer.
+ 7. If description.type is pranswer and description.sdp does not match lastAnswer,
+ reject the promise with a newly created InvalidModificationError and abort these
+ steps.
+ */
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-setRemoteDescription-answer.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-setRemoteDescription-answer.html
new file mode 100644
index 0000000000..7306311b0a
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-setRemoteDescription-answer.html
@@ -0,0 +1,123 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerConnection.prototype.setRemoteDescription - answer</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="RTCPeerConnection-helper.js"></script>
+<script>
+ 'use strict';
+
+ // Test is based on the following editor draft:
+ // https://w3c.github.io/webrtc-pc/archives/20170605/webrtc.html
+
+ // The following helper functions are called from RTCPeerConnection-helper.js:
+ // generateAnswer()
+ // assert_session_desc_similar()
+
+ /*
+ 4.3.2. Interface Definition
+ [Constructor(optional RTCConfiguration configuration)]
+ interface RTCPeerConnection : EventTarget {
+ Promise<void> setRemoteDescription(
+ RTCSessionDescriptionInit description);
+
+ readonly attribute RTCSessionDescription? remoteDescription;
+ readonly attribute RTCSessionDescription? currentRemoteDescription;
+ readonly attribute RTCSessionDescription? pendingRemoteDescription;
+ ...
+ };
+
+ 4.6.2. RTCSessionDescription Class
+ dictionary RTCSessionDescriptionInit {
+ required RTCSdpType type;
+ DOMString sdp = "";
+ };
+
+ 4.6.1. RTCSdpType
+ enum RTCSdpType {
+ "offer",
+ "pranswer",
+ "answer",
+ "rollback"
+ };
+ */
+
+ /*
+ 4.3.1.6. Set the RTCSessionSessionDescription
+ 2.2.3. Otherwise, if description is set as a remote description, then run one of
+ the following steps:
+ - If description is of type "answer", then this completes an offer answer
+ negotiation.
+
+ Set connection's currentRemoteDescription to description and
+ currentLocalDescription to the value of pendingLocalDescription.
+
+ Set both pendingRemoteDescription and pendingLocalDescription to null.
+
+ Finally setconnection's signaling state to stable.
+ */
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const states = [];
+ pc.addEventListener('signalingstatechange', () => states.push(pc.signalingState));
+
+ return generateVideoReceiveOnlyOffer(pc)
+ .then(offer =>
+ pc.setLocalDescription(offer)
+ .then(() => generateAnswer(offer))
+ .then(answer =>
+ pc.setRemoteDescription(answer)
+ .then(() => {
+ assert_equals(pc.signalingState, 'stable');
+
+ assert_session_desc_similar(pc.localDescription, offer);
+ assert_session_desc_similar(pc.remoteDescription, answer);
+
+ assert_session_desc_similar(pc.currentLocalDescription, offer);
+ assert_session_desc_similar(pc.currentRemoteDescription, answer);
+
+ assert_equals(pc.pendingLocalDescription, null);
+ assert_equals(pc.pendingRemoteDescription, null);
+
+ assert_array_equals(states, ['have-local-offer', 'stable']);
+ })));
+ }, 'setRemoteDescription() with valid state and answer should succeed');
+
+ /*
+ 4.3.1.6. Set the RTCSessionSessionDescription
+ 2.1.3. If the description's type is invalid for the current signaling state of
+ connection, then reject p with a newly created InvalidStateError and abort
+ these steps.
+
+ [JSEP]
+ 5.6. If the type is "answer", the PeerConnection state MUST be either
+ "have-local-offer" or "have-remote-pranswer".
+ */
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+
+ t.add_cleanup(() => pc.close());
+
+ return pc.createOffer()
+ .then(offer =>
+ promise_rejects_dom(t, 'InvalidStateError',
+ pc.setRemoteDescription({ type: 'answer', sdp: offer.sdp })));
+ }, 'Calling setRemoteDescription(answer) from stable state should reject with InvalidStateError');
+
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+
+ t.add_cleanup(() => pc.close());
+
+ return pc.createOffer()
+ .then(offer =>
+ pc.setRemoteDescription(offer)
+ .then(() => generateAnswer(offer)))
+ .then(answer =>
+ promise_rejects_dom(t, 'InvalidStateError',
+ pc.setRemoteDescription(answer)));
+ }, 'Calling setRemoteDescription(answer) from have-remote-offer state should reject with InvalidStateError');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-setRemoteDescription-nomsid.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-setRemoteDescription-nomsid.html
new file mode 100644
index 0000000000..8a86bb0c8e
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-setRemoteDescription-nomsid.html
@@ -0,0 +1,40 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerConnection.prototype.setRemoteDescription - legacy streams without a=msid lines</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+'use strict';
+
+const FINGERPRINT_SHA256 = '00:00:00:00:00:00:00:00:00:00:00:00:00' +
+ ':00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00';
+const ICEUFRAG = 'someufrag';
+const ICEPWD = 'somelongpwdwithenoughrandomness';
+const SDP_BOILERPLATE = 'v=0\r\n' +
+ 'o=- 166855176514521964 2 IN IP4 127.0.0.1\r\n' +
+ 's=-\r\n' +
+ 't=0 0\r\n';
+const MINIMAL_AUDIO_MLINE =
+ 'm=audio 9 UDP/TLS/RTP/SAVPF 111\r\n' +
+ 'c=IN IP4 0.0.0.0\r\n' +
+ 'a=rtcp:9 IN IP4 0.0.0.0\r\n' +
+ 'a=ice-ufrag:' + ICEUFRAG + '\r\n' +
+ 'a=ice-pwd:' + ICEPWD + '\r\n' +
+ 'a=fingerprint:sha-256 ' + FINGERPRINT_SHA256 + '\r\n' +
+ 'a=setup:actpass\r\n' +
+ 'a=mid:0\r\n' +
+ 'a=sendrecv\r\n' +
+ 'a=rtcp-mux\r\n' +
+ 'a=rtcp-rsize\r\n' +
+ 'a=rtpmap:111 opus/48000/2\r\n';
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const haveOntrack = new Promise(r => pc.ontrack = r);
+ await pc.setRemoteDescription({type: 'offer', sdp: SDP_BOILERPLATE + MINIMAL_AUDIO_MLINE});
+ assert_equals((await haveOntrack).streams.length, 1);
+ }, 'setRemoteDescription with an SDP without a=msid lines triggers ontrack with a default stream.');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-setRemoteDescription-offer.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-setRemoteDescription-offer.html
new file mode 100644
index 0000000000..d5acb7e1c9
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-setRemoteDescription-offer.html
@@ -0,0 +1,356 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerConnection.prototype.setRemoteDescription - offer</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="RTCPeerConnection-helper.js"></script>
+<script>
+ 'use strict';
+
+ // The following helper functions are called from RTCPeerConnection-helper.js:
+ // assert_session_desc_similar()
+ // generateAudioReceiveOnlyOffer
+
+ /*
+ 4.3.2. Interface Definition
+ [Constructor(optional RTCConfiguration configuration)]
+ interface RTCPeerConnection : EventTarget {
+ Promise<void> setRemoteDescription(
+ RTCSessionDescriptionInit description);
+
+ readonly attribute RTCSessionDescription? remoteDescription;
+ readonly attribute RTCSessionDescription? currentRemoteDescription;
+ readonly attribute RTCSessionDescription? pendingRemoteDescription;
+ ...
+ };
+
+ 4.6.2. RTCSessionDescription Class
+ dictionary RTCSessionDescriptionInit {
+ required RTCSdpType type;
+ DOMString sdp = "";
+ };
+
+ 4.6.1. RTCSdpType
+ enum RTCSdpType {
+ "offer",
+ "pranswer",
+ "answer",
+ "rollback"
+ };
+ */
+
+ /*
+ 4.3.1.6. Set the RTCSessionSessionDescription
+ 2.2.3. Otherwise, if description is set as a remote description, then run one of
+ the following steps:
+ - If description is of type "offer", set connection.pendingRemoteDescription
+ attribute to description and signaling state to have-remote-offer.
+ */
+
+ promise_test(t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ pc1.createDataChannel('datachannel');
+
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const states = [];
+ pc2.addEventListener('signalingstatechange', () => states.push(pc2.signalingState));
+
+ return pc1.createOffer()
+ .then(offer => {
+ return pc2.setRemoteDescription(offer)
+ .then(() => {
+ assert_equals(pc2.signalingState, 'have-remote-offer');
+ assert_session_desc_similar(pc2.remoteDescription, offer);
+ assert_session_desc_similar(pc2.pendingRemoteDescription, offer);
+ assert_equals(pc2.currentRemoteDescription, null);
+
+ assert_array_equals(states, ['have-remote-offer']);
+ });
+ });
+ }, 'setRemoteDescription with valid offer should succeed');
+
+ promise_test(t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ pc1.createDataChannel('datachannel');
+
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const states = [];
+ pc2.addEventListener('signalingstatechange', () => states.push(pc2.signalingState));
+
+ return pc1.createOffer()
+ .then(offer => {
+ return pc2.setRemoteDescription(offer)
+ .then(() => pc2.setRemoteDescription(offer))
+ .then(() => {
+ assert_equals(pc2.signalingState, 'have-remote-offer');
+ assert_session_desc_similar(pc2.remoteDescription, offer);
+ assert_session_desc_similar(pc2.pendingRemoteDescription, offer);
+ assert_equals(pc2.currentRemoteDescription, null);
+
+ assert_array_equals(states, ['have-remote-offer']);
+ });
+ });
+ }, 'setRemoteDescription multiple times should succeed');
+
+ promise_test(t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ pc1.createDataChannel('datachannel');
+
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const states = [];
+ pc2.addEventListener('signalingstatechange', () => states.push(pc2.signalingState));
+
+ return pc1.createOffer()
+ .then(offer1 => {
+ return pc1.setLocalDescription(offer1)
+ .then(()=> {
+ return generateAudioReceiveOnlyOffer(pc1)
+ .then(offer2 => {
+ assert_session_desc_not_similar(offer1, offer2);
+
+ return pc2.setRemoteDescription(offer1)
+ .then(() => pc2.setRemoteDescription(offer2))
+ .then(() => {
+ assert_equals(pc2.signalingState, 'have-remote-offer');
+ assert_session_desc_similar(pc2.remoteDescription, offer2);
+ assert_session_desc_similar(pc2.pendingRemoteDescription, offer2);
+ assert_equals(pc2.currentRemoteDescription, null);
+
+ assert_array_equals(states, ['have-remote-offer']);
+ });
+ });
+ });
+ });
+ }, 'setRemoteDescription multiple times with different offer should succeed');
+
+ /*
+ 4.3.1.6. Set the RTCSessionSessionDescription
+ 2.1.4. If the content of description is not valid SDP syntax, then reject p with
+ an RTCError (with errorDetail set to "sdp-syntax-error" and the
+ sdpLineNumber attribute set to the line number in the SDP where the syntax
+ error was detected) and abort these steps.
+ */
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+
+ t.add_cleanup(() => pc.close());
+
+ return pc.setRemoteDescription({
+ type: 'offer',
+ sdp: 'Invalid SDP'
+ })
+ .then(() => {
+ assert_unreached('Expect promise to be rejected');
+ }, err => {
+ assert_equals(err.errorDetail, 'sdp-syntax-error',
+ 'Expect error detail field to set to sdp-syntax-error');
+
+ assert_true(err instanceof RTCError,
+ 'Expect err to be instance of RTCError');
+ });
+ }, 'setRemoteDescription(offer) with invalid SDP should reject with RTCError');
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+ await pc1.setLocalDescription(await pc1.createOffer());
+ await pc1.setRemoteDescription(await pc2.createOffer());
+ assert_equals(pc1.signalingState, 'have-remote-offer');
+ }, 'setRemoteDescription(offer) from have-local-offer should roll back and succeed');
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+ await pc1.setLocalDescription(await pc1.createOffer());
+ const p = pc1.setRemoteDescription(await pc2.createOffer());
+ await new Promise(r => pc1.onsignalingstatechange = r);
+ assert_equals(pc1.signalingState, 'stable');
+ assert_equals(pc1.pendingLocalDescription, null);
+ assert_equals(pc1.pendingRemoteDescription, null);
+ await new Promise(r => pc1.onsignalingstatechange = r);
+ assert_equals(pc1.signalingState, 'have-remote-offer');
+ assert_equals(pc1.pendingLocalDescription, null);
+ assert_equals(pc1.pendingRemoteDescription.type, 'offer');
+ await p;
+ }, 'setRemoteDescription(offer) from have-local-offer fires signalingstatechange twice');
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ pc1.addTransceiver('audio', { direction: 'recvonly' });
+ const srdPromise = pc2.setRemoteDescription(await pc1.createOffer());
+
+ assert_equals(pc2.signalingState, "stable", "signalingState should not be set synchronously after a call to sRD");
+
+ assert_equals(pc2.pendingRemoteDescription, null, "pendingRemoteDescription should not be set synchronously after a call to sRD");
+ assert_equals(pc2.currentRemoteDescription, null, "currentRemoteDescription should not be set synchronously after a call to sRD");
+
+ const statePromise = new Promise(resolve => {
+ pc2.onsignalingstatechange = () => {
+ resolve(pc2.signalingState);
+ }
+ });
+
+ const raceValue = await Promise.race([statePromise, srdPromise]);
+ assert_equals(raceValue, "have-remote-offer", "signalingstatechange event should fire before sRD resolves");
+ assert_not_equals(pc2.pendingRemoteDescription, null, "pendingRemoteDescription should be updated before the signalingstatechange event");
+ assert_equals(pc2.pendingRemoteDescription.type, "offer");
+ assert_equals(pc2.pendingRemoteDescription.sdp, pc2.remoteDescription.sdp);
+ assert_equals(pc2.currentRemoteDescription, null, "currentRemoteDescription should not be set after a call to sRD(offer)");
+
+ await srdPromise;
+ }, "setRemoteDescription(offer) in stable should update internal state with a queued task, in the right order");
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ pc2.addTransceiver('audio', { direction: 'recvonly' });
+ await pc2.setLocalDescription(await pc2.createOffer());
+
+ // Implicit rollback!
+ pc1.addTransceiver('audio', { direction: 'recvonly' });
+ const srdPromise = pc2.setRemoteDescription(await pc1.createOffer());
+
+ assert_equals(pc2.signalingState, "have-local-offer", "signalingState should not be set synchronously after a call to sRD");
+
+ assert_equals(pc2.pendingRemoteDescription, null, "pendingRemoteDescription should not be set synchronously after a call to sRD");
+ assert_not_equals(pc2.pendingLocalDescription, null, "pendingLocalDescription should not be set synchronously after a call to sRD");
+ assert_equals(pc2.pendingLocalDescription.type, "offer");
+ assert_equals(pc2.pendingLocalDescription.sdp, pc2.localDescription.sdp);
+
+ // First, we should go through stable (the implicit rollback part)
+ const stablePromise = new Promise(resolve => {
+ pc2.onsignalingstatechange = () => {
+ resolve(pc2.signalingState);
+ }
+ });
+
+ let raceValue = await Promise.race([stablePromise, srdPromise]);
+ assert_equals(raceValue, "stable", "signalingstatechange event should fire before sRD resolves");
+ assert_equals(pc2.pendingLocalDescription, null, "pendingLocalDescription should be updated before the signalingstatechange event");
+ assert_equals(pc2.pendingRemoteDescription, null, "pendingRemoteDescription should be updated before the signalingstatechange event");
+
+ const haveRemoteOfferPromise = new Promise(resolve => {
+ pc2.onsignalingstatechange = () => {
+ resolve(pc2.signalingState);
+ }
+ });
+
+ raceValue = await Promise.race([haveRemoteOfferPromise, srdPromise]);
+ assert_equals(raceValue, "have-remote-offer", "signalingstatechange event should fire before sRD resolves");
+ assert_not_equals(pc2.pendingRemoteDescription, null, "pendingRemoteDescription should be updated before the signalingstatechange event");
+ assert_equals(pc2.pendingRemoteDescription.type, "offer");
+ assert_equals(pc2.pendingRemoteDescription.sdp, pc2.remoteDescription.sdp);
+ assert_equals(pc2.pendingLocalDescription, null, "pendingLocalDescription should be updated before the signalingstatechange event");
+
+ await srdPromise;
+ }, "setRemoteDescription(offer) in have-local-offer should update internal state with a queued task, in the right order");
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+ await pc1.setLocalDescription(await pc1.createOffer());
+ const offer = await pc2.createOffer();
+ const p1 = pc1.setLocalDescription({type: 'rollback'});
+ await new Promise(r => pc1.onsignalingstatechange = r);
+ assert_equals(pc1.signalingState, 'stable');
+ const p2 = pc1.addIceCandidate();
+ const p3 = pc1.setRemoteDescription(offer);
+ await promise_rejects_dom(t, 'InvalidStateError', p2);
+ await p1;
+ await p3;
+ assert_equals(pc1.signalingState, 'have-remote-offer');
+ }, 'Naive rollback approach is not glare-proof (control)');
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+ await pc1.setLocalDescription(await pc1.createOffer());
+ const p = pc1.setRemoteDescription(await pc2.createOffer());
+ await new Promise(r => pc1.onsignalingstatechange = r);
+ assert_equals(pc1.signalingState, 'stable');
+ await pc1.addIceCandidate();
+ await p;
+ assert_equals(pc1.signalingState, 'have-remote-offer');
+ }, 'setRemoteDescription(offer) from have-local-offer is glare-proof');
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+ await pc1.setLocalDescription(await pc1.createOffer());
+ const p = pc1.setRemoteDescription({type: 'offer', sdp: 'Invalid SDP'});
+ await new Promise(r => pc1.onsignalingstatechange = r);
+ assert_equals(pc1.signalingState, 'stable');
+ assert_equals(pc1.pendingLocalDescription, null);
+ assert_equals(pc1.pendingRemoteDescription, null);
+ await promise_rejects_dom(t, 'RTCError', p);
+ }, 'setRemoteDescription(invalidOffer) from have-local-offer does not undo rollback');
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+ pc1.addTransceiver('video');
+ const offer = await pc1.createOffer();
+ await pc2.setRemoteDescription(offer);
+ assert_equals(pc2.getTransceivers().length, 1);
+ await pc2.setRemoteDescription(offer);
+ assert_equals(pc2.getTransceivers().length, 1);
+ await pc1.setLocalDescription(offer);
+ const answer = await pc2.createAnswer();
+ await pc2.setLocalDescription(answer);
+ await pc1.setRemoteDescription(answer);
+ }, 'repeated sRD(offer) works');
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+ pc1.addTransceiver('video');
+ await exchangeOfferAnswer(pc1, pc2);
+ await waitForIceGatheringState(pc1, ['complete']);
+ await exchangeOfferAnswer(pc1, pc2);
+ await waitForIceStateChange(pc2, ['connected', 'completed']);
+ }, 'sRD(reoffer) with candidates and without trickle works');
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+ pc1.addTransceiver('video');
+ const offer = await pc1.createOffer();
+ const srdPromise = pc2.setRemoteDescription(offer);
+ assert_equals(pc2.getTransceivers().length, 0);
+ await srdPromise;
+ assert_equals(pc2.getTransceivers().length, 1);
+ }, 'Transceivers added by sRD(offer) should not show up until sRD resolves');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-setRemoteDescription-pranswer.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-setRemoteDescription-pranswer.html
new file mode 100644
index 0000000000..1f8bde0f29
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-setRemoteDescription-pranswer.html
@@ -0,0 +1,166 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerConnection.prototype.setRemoteDescription pranswer</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="RTCPeerConnection-helper.js"></script>
+<script>
+ 'use strict';
+
+ // Test is based on the following editor draft:
+ // https://w3c.github.io/webrtc-pc/archives/20170605/webrtc.html
+
+ // The following helper functions are called from RTCPeerConnection-helper.js:
+ // generateAnswer
+ // assert_session_desc_similar
+
+ /*
+ 4.3.2. Interface Definition
+ [Constructor(optional RTCConfiguration configuration)]
+ interface RTCPeerConnection : EventTarget {
+ Promise<void> setLocalDescription(
+ RTCSessionDescriptionInit description);
+
+ readonly attribute RTCSessionDescription? localDescription;
+ readonly attribute RTCSessionDescription? currentLocalDescription;
+ readonly attribute RTCSessionDescription? pendingLocalDescription;
+
+ Promise<void> setRemoteDescription(
+ RTCSessionDescriptionInit description);
+
+ readonly attribute RTCSessionDescription? remoteDescription;
+ readonly attribute RTCSessionDescription? currentRemoteDescription;
+ readonly attribute RTCSessionDescription? pendingRemoteDescription;
+ ...
+ };
+
+ 4.6.2. RTCSessionDescription Class
+ dictionary RTCSessionDescriptionInit {
+ required RTCSdpType type;
+ DOMString sdp = "";
+ };
+
+ 4.6.1. RTCSdpType
+ enum RTCSdpType {
+ "offer",
+ "pranswer",
+ "answer",
+ "rollback"
+ };
+ */
+
+ /*
+ 4.3.1.6. Set the RTCSessionSessionDescription
+ 2.1.3. If the description's type is invalid for the current signaling state of
+ connection, then reject p with a newly created InvalidStateError and abort
+ these steps.
+
+ [JSEP]
+ 5.6. If the type is "pranswer" or "answer", the PeerConnection state MUST be either
+ "have-local-offer" or "have-remote-pranswer".
+ */
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+
+ t.add_cleanup(() => pc.close());
+
+ return pc.createOffer()
+ .then(offer =>
+ promise_rejects_dom(t, 'InvalidStateError',
+ pc.setRemoteDescription({ type: 'pranswer', sdp: offer.sdp })));
+ }, 'setRemoteDescription(pranswer) from stable state should reject with InvalidStateError');
+
+ /*
+ 4.3.1.6. Set the RTCSessionSessionDescription
+ 2.2.3. Otherwise, if description is set as a remote description, then run one
+ of the following steps:
+ - If description is of type "pranswer", then set
+ connection.pendingRemoteDescription to description and signaling state
+ to have-remote-pranswer.
+ */
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const states = [];
+ pc.addEventListener('signalingstatechange', () => states.push(pc.signalingState));
+
+ return generateVideoReceiveOnlyOffer(pc)
+ .then(offer =>
+ pc.setLocalDescription(offer)
+ .then(() => generateAnswer(offer))
+ .then(answer => {
+ const pranswer = { type: 'pranswer', sdp: answer.sdp };
+
+ return pc.setRemoteDescription(pranswer)
+ .then(() => {
+ assert_equals(pc.signalingState, 'have-remote-pranswer');
+
+ assert_session_desc_similar(pc.localDescription, offer);
+ assert_session_desc_similar(pc.pendingLocalDescription, offer);
+ assert_equals(pc.currentLocalDescription, null);
+
+ assert_session_desc_similar(pc.remoteDescription, pranswer);
+ assert_session_desc_similar(pc.pendingRemoteDescription, pranswer);
+ assert_equals(pc.currentRemoteDescription, null);
+
+ assert_array_equals(states, ['have-local-offer', 'have-remote-pranswer']);
+ });
+ }));
+ }, 'setRemoteDescription(pranswer) from have-local-offer state should succeed');
+
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const states = [];
+ pc.addEventListener('signalingstatechange', () => states.push(pc.signalingState));
+
+ return generateVideoReceiveOnlyOffer(pc)
+ .then(offer =>
+ pc.setLocalDescription(offer)
+ .then(() => generateAnswer(offer))
+ .then(answer => {
+ const pranswer = { type: 'pranswer', sdp: answer.sdp };
+
+ return pc.setRemoteDescription(pranswer)
+ .then(() => pc.setRemoteDescription(pranswer))
+ .then(() => {
+ assert_array_equals(states, ['have-local-offer', 'have-remote-pranswer']);
+ });
+ }));
+ }, 'setRemoteDescription(pranswer) multiple times should succeed');
+
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const states = [];
+ pc.addEventListener('signalingstatechange', () => states.push(pc.signalingState));
+
+ return generateVideoReceiveOnlyOffer(pc)
+ .then(offer =>
+ pc.setLocalDescription(offer)
+ .then(() => generateAnswer(offer))
+ .then(answer => {
+ const pranswer = { type: 'pranswer', sdp: answer.sdp };
+
+ return pc.setRemoteDescription(pranswer)
+ .then(() => pc.setRemoteDescription(answer))
+ .then(() => {
+ assert_equals(pc.signalingState, 'stable');
+
+ assert_session_desc_similar(pc.localDescription, offer);
+ assert_session_desc_similar(pc.currentLocalDescription, offer);
+ assert_equals(pc.pendingLocalDescription, null);
+
+ assert_session_desc_similar(pc.remoteDescription, answer);
+ assert_session_desc_similar(pc.currentRemoteDescription, answer);
+ assert_equals(pc.pendingRemoteDescription, null);
+
+ assert_array_equals(states, ['have-local-offer', 'have-remote-pranswer', 'stable']);
+ });
+ }));
+ }, 'setRemoteDescription(answer) from have-remote-pranswer state should succeed');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-setRemoteDescription-replaceTrack.https.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-setRemoteDescription-replaceTrack.https.html
new file mode 100644
index 0000000000..217326bfae
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-setRemoteDescription-replaceTrack.https.html
@@ -0,0 +1,115 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerConnection.prototype.setRemoteDescription - replaceTrack</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="RTCPeerConnection-helper.js"></script>
+<script>
+ 'use strict';
+
+ // The following helper functions are called from RTCPeerConnection-helper.js:
+ // getUserMediaTracksAndStreams
+
+ async_test(t => {
+ const caller = new RTCPeerConnection();
+ t.add_cleanup(() => caller.close());
+ getUserMediaTracksAndStreams(2)
+ .then(t.step_func(([tracks, streams]) => {
+ const sender = caller.addTrack(tracks[0], streams[0]);
+ return sender.replaceTrack(tracks[1])
+ .then(t.step_func(() => {
+ assert_equals(sender.track, tracks[1]);
+ t.done();
+ }));
+ }))
+ .catch(t.step_func(reason => {
+ assert_unreached(reason);
+ }));
+ }, 'replaceTrack() sets the track attribute to a new track.');
+
+ async_test(t => {
+ const caller = new RTCPeerConnection();
+ t.add_cleanup(() => caller.close());
+ getUserMediaTracksAndStreams(1)
+ .then(t.step_func(([tracks, streams]) => {
+ const sender = caller.addTrack(tracks[0], streams[0]);
+ return sender.replaceTrack(null)
+ .then(t.step_func(() => {
+ assert_equals(sender.track, null);
+ t.done();
+ }));
+ }))
+ .catch(t.step_func(reason => {
+ assert_unreached(reason);
+ }));
+ }, 'replaceTrack() sets the track attribute to null.');
+
+ async_test(t => {
+ const caller = new RTCPeerConnection();
+ t.add_cleanup(() => caller.close());
+ getUserMediaTracksAndStreams(2)
+ .then(t.step_func(([tracks, streams]) => {
+ const sender = caller.addTrack(tracks[0], streams[0]);
+ assert_equals(sender.track, tracks[0]);
+ sender.replaceTrack(tracks[1]);
+ // replaceTrack() is asynchronous, there should be no synchronously
+ // observable effects.
+ assert_equals(sender.track, tracks[0]);
+ t.done();
+ }))
+ .catch(t.step_func(reason => {
+ assert_unreached(reason);
+ }));
+ }, 'replaceTrack() does not set the track synchronously.');
+
+ async_test(t => {
+ const expectedException = 'InvalidStateError';
+ const caller = new RTCPeerConnection();
+ t.add_cleanup(() => caller.close());
+ getUserMediaTracksAndStreams(2)
+ .then(t.step_func(([tracks, streams]) => {
+ const sender = caller.addTrack(tracks[0], streams[0]);
+ caller.close();
+ return sender.replaceTrack(tracks[1])
+ .then(t.step_func(() => {
+ assert_unreached('Expected replaceTrack() to be rejected with ' +
+ expectedException + ' but the promise was resolved.');
+ }),
+ t.step_func(e => {
+ assert_equals(e.name, expectedException);
+ t.done();
+ }));
+ }))
+ .catch(t.step_func(reason => {
+ assert_unreached(reason);
+ }));
+ }, 'replaceTrack() rejects when the peer connection is closed.');
+
+ promise_test(async t => {
+ const caller = new RTCPeerConnection();
+ t.add_cleanup(() => caller.close());
+ const [tracks, streams] = await getUserMediaTracksAndStreams(2);
+ const sender = caller.addTrack(tracks[0], streams[0]);
+ caller.removeTrack(sender);
+ await sender.replaceTrack(tracks[1]);
+ assert_equals(sender.track, tracks[1], "Make sure track gets updated");
+ }, 'replaceTrack() does not reject when invoked after removeTrack().');
+
+
+ promise_test(async t => {
+ const caller = new RTCPeerConnection();
+ t.add_cleanup(() => caller.close());
+ const [tracks, streams] = await getUserMediaTracksAndStreams(2);
+ const sender = caller.addTrack(tracks[0], streams[0]);
+ let p = sender.replaceTrack(tracks[1])
+ caller.removeTrack(sender);
+ await p;
+ assert_equals(sender.track, tracks[1], "Make sure track gets updated");
+ }, 'replaceTrack() does not reject after a subsequent removeTrack().');
+
+ // TODO(hbos): Verify that replaceTrack() changes what media is received on
+ // the remote end of two connected peer connections. For video tracks, this
+ // requires Chromium's video tag to update on receiving frames when running
+ // content_shell. https://crbug.com/793808
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-setRemoteDescription-rollback.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-setRemoteDescription-rollback.html
new file mode 100644
index 0000000000..0e6213d708
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-setRemoteDescription-rollback.html
@@ -0,0 +1,602 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerConnection.prototype.setRemoteDescription rollback</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="RTCPeerConnection-helper.js"></script>
+<script>
+ 'use strict';
+
+ // Test is based on the following editor draft:
+ // https://w3c.github.io/webrtc-pc/archives/20170605/webrtc.html
+
+ // The following helper functions are called from RTCPeerConnection-helper.js:
+ // assert_session_desc_similar
+ // generateAudioReceiveOnlyOffer
+ // generateDataChannelOffer
+
+ /*
+ 4.3.2. Interface Definition
+ [Constructor(optional RTCConfiguration configuration)]
+ interface RTCPeerConnection : EventTarget {
+ Promise<void> setLocalDescription(
+ RTCSessionDescriptionInit description);
+
+ readonly attribute RTCSessionDescription? localDescription;
+ readonly attribute RTCSessionDescription? currentLocalDescription;
+ readonly attribute RTCSessionDescription? pendingLocalDescription;
+
+ Promise<void> setRemoteDescription(
+ RTCSessionDescriptionInit description);
+
+ readonly attribute RTCSessionDescription? remoteDescription;
+ readonly attribute RTCSessionDescription? currentRemoteDescription;
+ readonly attribute RTCSessionDescription? pendingRemoteDescription;
+ ...
+ };
+
+ 4.6.2. RTCSessionDescription Class
+ dictionary RTCSessionDescriptionInit {
+ required RTCSdpType type;
+ DOMString sdp = "";
+ };
+
+ 4.6.1. RTCSdpType
+ enum RTCSdpType {
+ "offer",
+ "pranswer",
+ "answer",
+ "rollback"
+ };
+ */
+
+ /*
+ 4.3.1.6. Set the RTCSessionSessionDescription
+ 2.2.3. Otherwise, if description is set as a remote description, then run one
+ of the following steps:
+ - If description is of type "rollback", then this is a rollback.
+ Set connection.pendingRemoteDescription to null and signaling state to stable.
+ */
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const states = [];
+ pc.addEventListener('signalingstatechange', () => states.push(pc.signalingState));
+
+ return generateDataChannelOffer(pc)
+ .then(offer => pc.setRemoteDescription(offer))
+ .then(() => {
+ assert_equals(pc.signalingState, 'have-remote-offer');
+ assert_not_equals(pc.remoteDescription, null);
+ assert_not_equals(pc.pendingRemoteDescription, null);
+ assert_equals(pc.currentRemoteDescription, null);
+
+ return pc.setRemoteDescription({type: 'rollback'});
+ })
+ .then(() => {
+ assert_equals(pc.signalingState, 'stable');
+ assert_equals(pc.remoteDescription, null);
+ assert_equals(pc.pendingRemoteDescription, null);
+ assert_equals(pc.currentRemoteDescription, null);
+
+ assert_array_equals(states, ['have-remote-offer', 'stable']);
+ });
+ }, 'setRemoteDescription(rollback) in have-remote-offer state should revert to stable state');
+
+ /*
+ 4.3.1.6. Set the RTCSessionSessionDescription
+ 2.3. If the description's type is invalid for the current signaling state of
+ connection, then reject p with a newly created InvalidStateError and abort
+ these steps.
+
+ [jsep]
+ 4.1.8.2. Rollback
+ - Rollback can only be used to cancel proposed changes;
+ there is no support for rolling back from a stable state to a
+ previous stable state
+ */
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ return promise_rejects_dom(t, 'InvalidStateError',
+ pc.setRemoteDescription({type: 'rollback'}));
+ }, `setRemoteDescription(rollback) from stable state should reject with InvalidStateError`);
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ await pc.setLocalDescription();
+ await promise_rejects_dom(t, 'InvalidStateError', pc.setRemoteDescription({ type: 'rollback' }));
+ }, `setRemoteDescription(rollback) after setting a local offer should reject with InvalidStateError`);
+
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ return generateAudioReceiveOnlyOffer(pc)
+ .then(offer => pc.setRemoteDescription(offer))
+ .then(() => pc.setRemoteDescription({
+ type: 'rollback',
+ sdp: '!<Invalid SDP Content>;'
+ }));
+ }, `setRemoteDescription(rollback) should ignore invalid sdp content and succeed`);
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+
+ // We don't use this right away
+ pc1.addTransceiver('audio', { direction: 'recvonly' });
+ const offer1 = await pc1.createOffer();
+
+ // Create offer from pc2, apply and rollback on pc1
+ pc2.addTransceiver('audio', { direction: 'recvonly' });
+ const offer2 = await pc2.createOffer();
+ await pc1.setRemoteDescription(offer2);
+ await pc1.setRemoteDescription({type: "rollback"});
+
+ // Then try applying pc1's old offer
+ await pc1.setLocalDescription(offer1);
+ }, `local offer created before setRemoteDescription(remote offer) then rollback should still be usable`);
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+
+ const stream = await getNoiseStream({video: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ pc1.addTrack(stream.getTracks()[0], stream);
+
+ // We don't use this right away. pc1 has provisionally decided that the
+ // (only) transceiver is bound to level 0.
+ const offer1 = await pc1.createOffer();
+
+ // Create offer from pc2, apply and rollback on pc1
+ pc2.addTransceiver('audio', { direction: 'recvonly' });
+ pc2.addTransceiver('video', { direction: 'recvonly' });
+ const offer2 = await pc2.createOffer();
+ // pc1 now should change its mind about what level its video transceiver is
+ // bound to. It was 0, now it is 1.
+ await pc1.setRemoteDescription(offer2);
+
+ // Rolling back should put things back the way they were.
+ await pc1.setRemoteDescription({type: "rollback"});
+
+ // Then try applying pc1's old offer
+ await pc1.setLocalDescription(offer1);
+ }, "local offer created before setRemoteDescription(remote offer) with different transceiver level assignments then rollback should still be usable");
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const stream = await getNoiseStream({video: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ pc1.addTrack(stream.getTracks()[0], stream);
+
+ await pc2.setRemoteDescription(await pc1.createOffer());
+ assert_equals(pc2.getTransceivers().length, 1);
+ await pc2.setRemoteDescription({type: "rollback"});
+ assert_equals(pc2.getTransceivers().length, 0);
+ }, "rollback of a remote offer should remove a transceiver");
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const stream = await getNoiseStream({video: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ pc1.addTrack(stream.getTracks()[0], stream);
+
+ await pc2.setRemoteDescription(await pc1.createOffer());
+ assert_equals(pc2.getTransceivers().length, 1);
+
+ const stream2 = await getNoiseStream({video: true});
+ t.add_cleanup(() => stream2.getTracks().forEach(track => track.stop()));
+ const track = stream2.getVideoTracks()[0];
+ await pc2.getTransceivers()[0].sender.replaceTrack(track);
+
+ await pc2.setRemoteDescription({type: "rollback"});
+ assert_equals(pc2.getTransceivers().length, 0);
+ }, "rollback of a remote offer should remove touched transceiver");
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const stream = await getNoiseStream({video: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ pc1.addTrack(stream.getTracks()[0], stream);
+
+ await pc2.setRemoteDescription(await pc1.createOffer());
+ assert_equals(pc2.getTransceivers().length, 1);
+
+ const stream2 = await getNoiseStream({video: true});
+ t.add_cleanup(() => stream2.getTracks().forEach(track => track.stop()));
+ pc2.addTrack(stream2.getTracks()[0], stream2);
+
+ await pc2.setRemoteDescription({type: "rollback"});
+ assert_equals(pc2.getTransceivers().length, 1);
+ assert_equals(pc2.getTransceivers()[0].mid, null);
+ assert_equals(pc2.getTransceivers()[0].receiver.transport, null);
+ }, "rollback of a remote offer should keep a transceiver");
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const stream = await getNoiseStream({video: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ pc1.addTrack(stream.getTracks()[0], stream);
+
+ const stream2 = await getNoiseStream({video: true});
+ t.add_cleanup(() => stream2.getTracks().forEach(track => track.stop()));
+ pc2.addTrack(stream2.getTracks()[0], stream2);
+
+ await pc2.setRemoteDescription(await pc1.createOffer());
+ assert_equals(pc2.getTransceivers().length, 1);
+
+ await pc2.setRemoteDescription({type: "rollback"});
+ assert_equals(pc2.getTransceivers().length, 1);
+ assert_equals(pc2.getTransceivers()[0].mid, null);
+ assert_equals(pc2.getTransceivers()[0].receiver.transport, null);
+ }, "rollback of a remote offer should keep a transceiver created by addtrack");
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const stream = await getNoiseStream({video: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ pc1.addTrack(stream.getTracks()[0], stream);
+
+ await pc2.setRemoteDescription(await pc1.createOffer());
+ assert_equals(pc2.getTransceivers().length, 1);
+
+ const stream2 = await getNoiseStream({video: true});
+ t.add_cleanup(() => stream2.getTracks().forEach(track => track.stop()));
+ pc2.addTrack(stream2.getTracks()[0], stream2);
+ await pc2.getTransceivers()[0].sender.replaceTrack(null);
+ await pc2.setRemoteDescription({type: "rollback"});
+ assert_equals(pc2.getTransceivers().length, 1);
+ }, "rollback of a remote offer should keep a transceiver without tracks");
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const stream = await getNoiseStream({video: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ pc.addTrack(stream.getTracks()[0], stream);
+
+ const states = [];
+ const signalingstatechangeResolver = new Resolver();
+ pc.onsignalingstatechange = () => {
+ states.push(pc.signalingState);
+ signalingstatechangeResolver.resolve();
+ };
+
+ const offer = await pc.createOffer();
+ await pc.setLocalDescription(offer);
+ assert_not_equals(pc.getTransceivers()[0].sender.transport, null);
+ await pc.setLocalDescription({type: "rollback"});
+ assert_equals(pc.getTransceivers().length, 1);
+ assert_equals(pc.getTransceivers()[0].mid, null)
+ assert_equals(pc.getTransceivers()[0].sender.transport, null);
+ await pc.setLocalDescription(offer);
+ assert_equals(pc.getTransceivers().length, 1);
+ await signalingstatechangeResolver.promise;
+ assert_array_equals(states, ['have-local-offer', 'stable', 'have-local-offer']);
+ }, "explicit rollback of local offer should remove transceivers and transport");
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const states = [];
+ const signalingstatechangeResolver = new Resolver();
+ pc1.onsignalingstatechange = () => {
+ states.push(pc1.signalingState);
+ signalingstatechangeResolver.resolve();
+ };
+ const stream1 = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stream1.getTracks().forEach(track => track.stop()));
+ pc1.addTransceiver(stream1.getTracks()[0], stream1);
+
+ const stream2 = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stream2.getTracks().forEach(track => track.stop()));
+ pc2.addTransceiver(stream2.getTracks()[0], stream2);
+
+ await pc1.setLocalDescription(await pc1.createOffer());
+ pc1.onnegotiationneeded = t.step_func(() => assert_true(false, "There should be no negotiationneeded event right now"));
+ await pc1.setRemoteDescription(await pc2.createOffer());
+ await pc1.setLocalDescription(await pc1.createAnswer());
+ await signalingstatechangeResolver.promise;
+ assert_array_equals(states, ['have-local-offer', 'stable', 'have-remote-offer', 'stable']);
+ await new Promise(r => pc1.onnegotiationneeded = r);
+ }, "when using addTransceiver, implicit rollback of a local offer should visit stable state, but not fire negotiationneeded until we settle in stable");
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const states = [];
+ const signalingstatechangeResolver = new Resolver();
+ pc1.onsignalingstatechange = () => {
+ states.push(pc1.signalingState);
+ signalingstatechangeResolver.resolve();
+ };
+ const stream1 = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stream1.getTracks().forEach(track => track.stop()));
+ pc1.addTrack(stream1.getTracks()[0], stream1);
+
+ const stream2 = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stream2.getTracks().forEach(track => track.stop()));
+ pc2.addTrack(stream2.getTracks()[0], stream2);
+
+ await pc1.setLocalDescription(await pc1.createOffer());
+ pc1.onnegotiationneeded = t.step_func(() => assert_true(false, "There should be no negotiationneeded event in this test"));
+ await pc1.setRemoteDescription(await pc2.createOffer());
+ await pc1.setLocalDescription(await pc1.createAnswer());
+ assert_array_equals(states, ['have-local-offer', 'stable', 'have-remote-offer', 'stable']);
+ await new Promise(r => t.step_timeout(r, 0));
+ }, "when using addTrack, implicit rollback of a local offer should visit stable state, but not fire negotiationneeded");
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+
+ const stream = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ pc1.addTrack(stream.getTracks()[0], stream);
+
+ await pc1.setLocalDescription(await pc1.createOffer());
+ await pc2.setRemoteDescription(pc1.pendingLocalDescription);
+
+ await pc2.setLocalDescription(await pc2.createAnswer());
+ await pc1.setRemoteDescription(pc2.localDescription);
+
+ // In stable state add video on both end and make sure video transceiver is not killed.
+
+ const stream1 = await getNoiseStream({video: true});
+ t.add_cleanup(() => stream1.getTracks().forEach(track => track.stop()));
+ pc1.addTrack(stream1.getTracks()[0], stream1);
+ await pc1.setLocalDescription(await pc1.createOffer());
+
+ const stream2 = await getNoiseStream({video: true});
+ t.add_cleanup(() => stream2.getTracks().forEach(track => track.stop()));
+ pc2.addTrack(stream2.getTracks()[0], stream2);
+ const offer2 = await pc2.createOffer();
+ await pc2.setRemoteDescription(pc1.pendingLocalDescription);
+ await pc2.setRemoteDescription({type: "rollback"});
+ assert_equals(pc2.getTransceivers().length, 2);
+ await pc2.setLocalDescription(offer2);
+ }, "rollback of a remote offer to negotiated stable state should enable " +
+ "applying of a local offer");
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+
+ const stream = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ pc1.addTrack(stream.getTracks()[0], stream);
+
+ await pc1.setLocalDescription(await pc1.createOffer());
+ await pc2.setRemoteDescription(pc1.pendingLocalDescription);
+
+ await pc2.setLocalDescription(await pc2.createAnswer());
+ await pc1.setRemoteDescription(pc2.localDescription);
+
+ // Both ends want to add video at the same time. pc2 rolls back.
+
+ const stream2 = await getNoiseStream({video: true});
+ t.add_cleanup(() => stream2.getTracks().forEach(track => track.stop()));
+ pc2.addTrack(stream2.getTracks()[0], stream2);
+ await pc2.setLocalDescription(await pc2.createOffer());
+ assert_equals(pc2.getTransceivers().length, 2);
+ assert_not_equals(pc2.getTransceivers()[1].sender.transport, null);
+ await pc2.setLocalDescription({type: "rollback"});
+ assert_equals(pc2.getTransceivers().length, 2);
+ // Rollback didn't touch audio transceiver and transport is intact.
+ assert_not_equals(pc2.getTransceivers()[0].sender.transport, null);
+ // Video transport got killed.
+ assert_equals(pc2.getTransceivers()[1].sender.transport, null);
+ const stream1 = await getNoiseStream({video: true});
+ t.add_cleanup(() => stream1.getTracks().forEach(track => track.stop()));
+ pc1.addTrack(stream1.getTracks()[0], stream1);
+ await pc1.setLocalDescription(await pc1.createOffer());
+ await pc2.setRemoteDescription(pc1.pendingLocalDescription);
+ }, "rollback of a local offer to negotiated stable state should enable " +
+ "applying of a remote offer");
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+
+ const stream = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ pc1.addTrack(stream.getTracks()[0], stream);
+
+ await pc1.setLocalDescription(await pc1.createOffer());
+ await pc2.setRemoteDescription(pc1.pendingLocalDescription);
+
+ await pc2.setLocalDescription(await pc2.createAnswer());
+ await pc1.setRemoteDescription(pc2.localDescription);
+
+ // pc1 adds video and pc2 adds audio. pc2 rolls back.
+ assert_equals(pc2.getTransceivers()[0].direction, "recvonly");
+ const stream2 = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stream2.getTracks().forEach(track => track.stop()));
+ pc2.addTrack(stream2.getTracks()[0], stream2);
+ assert_equals(pc2.getTransceivers()[0].direction, "sendrecv");
+ await pc2.setLocalDescription(await pc2.createOffer());
+ assert_equals(pc2.getTransceivers()[0].direction, "sendrecv");
+ await pc2.setLocalDescription({type: "rollback"});
+ assert_equals(pc2.getTransceivers().length, 1);
+ // setLocalDescription didn't change direction. So direction remains "sendrecv"
+ assert_equals(pc2.getTransceivers()[0].direction, "sendrecv");
+ // Rollback didn't touch audio transceiver and transport is intact. Still can receive audio.
+ assert_not_equals(pc2.getTransceivers()[0].receiver.transport, null);
+ const stream1 = await getNoiseStream({video: true});
+ t.add_cleanup(() => stream1.getTracks().forEach(track => track.stop()));
+ pc1.addTrack(stream1.getTracks()[0], stream1);
+ await pc1.setLocalDescription(await pc1.createOffer());
+ await pc2.setRemoteDescription(pc1.pendingLocalDescription);
+ }, "rollback a local offer with audio direction change to negotiated " +
+ "stable state and then add video receiver");
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+
+ pc1.addTransceiver('video', {direction: 'sendonly'});
+ pc2.addTransceiver('video', {direction: 'sendonly'});
+ await pc1.setLocalDescription(await pc1.createOffer());
+ const pc1FirstMid = pc1.getTransceivers()[0].mid;
+ await pc2.setLocalDescription(await pc2.createOffer());
+ const pc2FirstMid = pc2.getTransceivers()[0].mid;
+ // I don't think it is mandated that this has to be true, but any implementation I know of would
+ // have predictable mids (e.g. 0, 1, 2...) so pc1 and pc2 should offer with the same mids.
+ assert_equals(pc1FirstMid, pc2FirstMid);
+ await pc1.setRemoteDescription(pc2.pendingLocalDescription);
+ // We've implicitly rolled back and the SRD caused a second transceiver to be created.
+ // As such, the first transceiver's mid will now be null, and the second transceiver's mid will
+ // match the remote offer.
+ assert_equals(pc1.getTransceivers().length, 2);
+ assert_equals(pc1.getTransceivers()[0].mid, null);
+ assert_equals(pc1.getTransceivers()[1].mid, pc2FirstMid);
+ // If we now do an offer the first transceiver will get a different mid than in the first
+ // pc1.createOffer()!
+ pc1.setLocalDescription(await pc1.createAnswer());
+ await pc1.setLocalDescription(await pc1.createOffer());
+ assert_not_equals(pc1.getTransceivers()[0].mid, pc1FirstMid);
+ }, "two transceivers with same mids");
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+ const stream = await getNoiseStream({audio: true, video: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const audio = stream.getAudioTracks()[0];
+ pc1.addTrack(audio, stream);
+ const video = stream.getVideoTracks()[0];
+ pc1.addTrack(video, stream);
+
+ let remoteStream = null;
+ pc2.ontrack = e => { remoteStream = e.streams[0]; }
+ await pc2.setRemoteDescription(await pc1.createOffer());
+ assert_true(remoteStream != null);
+ let remoteTracks = remoteStream.getTracks();
+ const removedTracks = [];
+ remoteStream.onremovetrack = e => { removedTracks.push(e.track.id); }
+ await pc2.setRemoteDescription({type: "rollback"});
+ assert_equals(removedTracks.length, 2,
+ "Rollback should have removed two tracks");
+ assert_true(removedTracks.includes(remoteTracks[0].id),
+ "First track should be removed");
+ assert_true(removedTracks.includes(remoteTracks[1].id),
+ "Second track should be removed");
+
+ }, "onremovetrack fires during remote rollback");
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const stream1 = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stream1.getTracks().forEach(track => track.stop()));
+ pc1.addTrack(stream1.getTracks()[0], stream1);
+
+ const offer1 = await pc1.createOffer();
+
+ const remoteStreams = [];
+ pc2.ontrack = e => { remoteStreams.push(e.streams[0]); }
+
+ await pc1.setLocalDescription(offer1);
+ await pc2.setRemoteDescription(pc1.pendingLocalDescription);
+ await pc2.setLocalDescription(await pc2.createAnswer());
+ await pc1.setRemoteDescription(pc2.localDescription);
+
+ assert_equals(remoteStreams.length, 1, "Number of remote streams");
+ assert_equals(remoteStreams[0].getTracks().length, 1, "Number of remote tracks");
+ const track = remoteStreams[0].getTracks()[0];
+
+ const stream2 = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stream2.getTracks().forEach(track => track.stop()));
+ pc1.getTransceivers()[0].sender.setStreams(stream2);
+
+ const offer2 = await pc1.createOffer();
+ await pc2.setRemoteDescription(offer2);
+
+ assert_equals(remoteStreams.length, 2);
+ assert_equals(remoteStreams[0].getTracks().length, 0);
+ assert_equals(remoteStreams[1].getTracks()[0].id, track.id);
+ await pc2.setRemoteDescription({type: "rollback"});
+ assert_equals(remoteStreams.length, 3);
+ assert_equals(remoteStreams[0].id, remoteStreams[2].id);
+ assert_equals(remoteStreams[1].getTracks().length, 0);
+ assert_equals(remoteStreams[2].getTracks().length, 1);
+ assert_equals(remoteStreams[2].getTracks()[0].id, track.id);
+
+ }, "rollback of a remote offer with stream changes");
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+ pc2.addTransceiver('audio');
+ const offer = await pc2.createOffer();
+ await pc1.setRemoteDescription(offer);
+ const [transceiver] = pc1.getTransceivers();
+ pc1.setRemoteDescription({type:'rollback'});
+ pc1.removeTrack(transceiver.sender);
+ }, 'removeTrack() with a sender being rolled back does not crash or throw');
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+ pc1.addTransceiver('video');
+ const channel = pc2.createDataChannel('dummy');
+ await pc2.setLocalDescription(await pc2.createOffer());
+ await pc2.setRemoteDescription(await pc1.createOffer());
+ assert_equals(pc2.signalingState, 'have-remote-offer');
+ await pc2.setLocalDescription(await pc2.createAnswer());
+ await pc2.setLocalDescription(await pc2.createOffer());
+ assert_equals(channel.readyState, 'connecting');
+ }, 'Implicit rollback with only a datachannel works');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-setRemoteDescription-simulcast.https.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-setRemoteDescription-simulcast.https.html
new file mode 100644
index 0000000000..98b5d2bab7
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-setRemoteDescription-simulcast.https.html
@@ -0,0 +1,50 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerConnection.prototype.setRemoteDescription rollback</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="RTCPeerConnection-helper.js"></script>
+<script>
+'use strict';
+// Test for https://github.com/w3c/webrtc-pc/pull/2155
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const [track, stream] = await getTrackFromUserMedia('video');
+ t.add_cleanup(() => track.stop());
+
+ pc.addTrack(track, stream);
+
+ const offer_sdp = `v=0
+o=- 3840232462471583827 2 IN IP4 127.0.0.1
+s=-
+t=0 0
+a=group:BUNDLE 0
+a=msid-semantic: WMS
+m=video 9 UDP/TLS/RTP/SAVPF 96
+c=IN IP4 0.0.0.0
+a=rtcp:9 IN IP4 0.0.0.0
+a=ice-ufrag:Li6+
+a=ice-pwd:3C05CTZBRQVmGCAq7hVasHlT
+a=ice-options:trickle
+a=fingerprint:sha-256 5B:D3:8E:66:0E:7D:D3:F3:8E:E6:80:28:19:FC:55:AD:58:5D:B9:3D:A8:DE:45:4A:E7:87:02:F8:3C:0B:3B:B3
+a=setup:actpass
+a=mid:0
+a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id
+a=recvonly
+a=rtcp-mux
+a=rtpmap:96 VP8/90000
+a=rtcp-fb:96 goog-remb
+a=rtcp-fb:96 transport-cc
+a=rtcp-fb:96 ccm fir
+a=rid:foo recv
+a=rid:bar recv
+a=rid:baz recv
+a=simulcast:recv foo;bar;baz
+`;
+
+ await pc.setRemoteDescription({type: 'offer', sdp: offer_sdp});
+ const transceivers = pc.getTransceivers();
+ assert_equals(transceivers.length, 1, 'Expected exactly one transceiver');
+}, 'createAnswer() attaches to an existing transceiver with a remote simulcast offer');
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-setRemoteDescription-tracks.https.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-setRemoteDescription-tracks.https.html
new file mode 100644
index 0000000000..d2ee646e2c
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-setRemoteDescription-tracks.https.html
@@ -0,0 +1,385 @@
+<!doctype html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<title>RTCPeerConnection.prototype.setRemoteDescription - add/remove remote tracks</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="RTCPeerConnection-helper.js"></script>
+<script>
+ 'use strict';
+
+ // The following helper functions are called from RTCPeerConnection-helper.js:
+ // addEventListenerPromise
+ // exchangeOffer
+ // exchangeOfferAnswer
+ // Resolver
+
+ // These tests are concerned with the observable consequences of processing
+ // the addition or removal of remote tracks, including events firing and the
+ // states of RTCPeerConnection, MediaStream and MediaStreamTrack.
+
+ promise_test(async t => {
+ const caller = new RTCPeerConnection();
+ t.add_cleanup(() => caller.close());
+ const callee = new RTCPeerConnection();
+ t.add_cleanup(() => callee.close());
+ const localStream =
+ await getNoiseStream({audio: true});
+ t.add_cleanup(() => localStream.getTracks().forEach(track => track.stop()));
+ caller.addTrack(localStream.getTracks()[0]);
+ const ontrackPromise = addEventListenerPromise(t, callee, 'track', e => {
+ assert_equals(e.streams.length, 0, 'No remote stream created.');
+ });
+ await exchangeOffer(caller, callee);
+ await ontrackPromise;
+ }, 'addTrack() with a track and no stream makes ontrack fire with a track and no stream.');
+
+ promise_test(async t => {
+ const caller = new RTCPeerConnection();
+ t.add_cleanup(() => caller.close());
+ const callee = new RTCPeerConnection();
+ t.add_cleanup(() => callee.close());
+ const localStream =
+ await getNoiseStream({audio: true});
+ t.add_cleanup(() => localStream.getTracks().forEach(track => track.stop()));
+ caller.addTrack(localStream.getTracks()[0], localStream);
+ const ontrackPromise = addEventListenerPromise(t, callee, 'track', e => {
+ assert_equals(e.streams.length, 1, 'Created a single remote stream.');
+ assert_equals(e.streams[0].id, localStream.id,
+ 'Local and remote stream IDs match.');
+ assert_array_equals(e.streams[0].getTracks(), [e.track],
+ 'The remote stream contains the remote track.');
+ });
+ await exchangeOffer(caller, callee);
+ await ontrackPromise;
+ }, 'addTrack() with a track and a stream makes ontrack fire with a track and a stream.');
+
+ promise_test(async t => {
+ const caller = new RTCPeerConnection();
+ t.add_cleanup(() => caller.close());
+ const callee = new RTCPeerConnection();
+ t.add_cleanup(() => callee.close());
+ let eventSequence = '';
+ const localStream =
+ await getNoiseStream({audio: true});
+ t.add_cleanup(() => localStream.getTracks().forEach(track => track.stop()));
+ caller.addTrack(localStream.getTracks()[0], localStream);
+ const ontrackPromise = addEventListenerPromise(t, callee, 'track', e => {
+ eventSequence += 'ontrack;';
+ });
+ await exchangeOffer(caller, callee);
+ eventSequence += 'setRemoteDescription;';
+ await ontrackPromise;
+ assert_equals(eventSequence, 'ontrack;setRemoteDescription;');
+ }, 'ontrack fires before setRemoteDescription resolves.');
+
+ promise_test(async t => {
+ const caller = new RTCPeerConnection();
+ t.add_cleanup(() => caller.close());
+ const callee = new RTCPeerConnection();
+ t.add_cleanup(() => callee.close());
+ const localStreams = await Promise.all([
+ getNoiseStream({audio: true}),
+ getNoiseStream({audio: true}),
+ ]);
+ t.add_cleanup(() => localStreams.forEach((stream) =>
+ stream.getTracks().forEach((track) => track.stop())));
+ caller.addTrack(localStreams[0].getTracks()[0], localStreams[0]);
+ caller.addTrack(localStreams[1].getTracks()[0], localStreams[0]);
+ let ontrackEventsFired = 0;
+ const ontrackEventResolvers = [ new Resolver(), new Resolver() ];
+ callee.ontrack = t.step_func(e => {
+ ontrackEventResolvers[ontrackEventsFired++].resolve(e);
+ });
+ await exchangeOffer(caller, callee);
+ let firstTrackEvent = await ontrackEventResolvers[0];
+ assert_equals(firstTrackEvent.streams.length, 1,
+ 'First ontrack fires with a single stream.');
+ assert_equals(firstTrackEvent.streams[0].id,
+ localStreams[0].id,
+ 'First ontrack\'s stream ID matches local stream.');
+ let secondTrackEvent = await ontrackEventResolvers[1];
+ assert_equals(secondTrackEvent.streams.length, 1,
+ 'Second ontrack fires with a single stream.');
+ assert_equals(secondTrackEvent.streams[0].id,
+ localStreams[0].id,
+ 'Second ontrack\'s stream ID matches local stream.');
+ assert_array_equals(firstTrackEvent.streams, secondTrackEvent.streams,
+ 'ontrack was fired with the same streams both times.');
+
+ assert_equals(firstTrackEvent.streams[0].getTracks().length, 2, "stream should have two tracks");
+ assert_true(firstTrackEvent.streams[0].getTracks().includes(firstTrackEvent.track), "remoteStream should have the first track");
+ assert_true(firstTrackEvent.streams[0].getTracks().includes(secondTrackEvent.track), "remoteStream should have the second track");
+ assert_equals(ontrackEventsFired, 2, 'Unexpected number of track events.');
+
+ assert_equals(ontrackEventsFired, 2, 'Unexpected number of track events.');
+ }, 'addTrack() with two tracks and one stream makes ontrack fire twice with the tracks and shared stream.');
+
+ promise_test(async t => {
+ const caller = new RTCPeerConnection();
+ t.add_cleanup(() => caller.close());
+ const callee = new RTCPeerConnection();
+ t.add_cleanup(() => callee.close());
+ let eventSequence = '';
+ const localStreams = await Promise.all([
+ getNoiseStream({audio: true}),
+ getNoiseStream({audio: true}),
+ ]);
+ t.add_cleanup(() => localStreams.forEach((stream) =>
+ stream.getTracks().forEach((track) => track.stop())));
+ caller.addTrack(localStreams[0].getTracks()[0], localStreams[0]);
+ const remoteStreams = [];
+ callee.ontrack = e => {
+ if (!remoteStreams.includes(e.streams[0]))
+ remoteStreams.push(e.streams[0]);
+ };
+ exchangeIceCandidates(caller, callee);
+ await exchangeOfferAnswer(caller, callee);
+ assert_equals(remoteStreams.length, 1, 'One remote stream created.');
+ assert_equals(remoteStreams[0].id, localStreams[0].id,
+ 'First local and remote streams have the same ID.');
+ const firstRemoteTrack = remoteStreams[0].getTracks()[0];
+ const onaddtrackPromise = addEventListenerPromise(t, remoteStreams[0], 'addtrack');
+ caller.addTrack(localStreams[1].getTracks()[0], localStreams[0]);
+ await exchangeOffer(caller, callee);
+ const e = await onaddtrackPromise;
+ assert_equals(remoteStreams[0].getTracks().length, 2, 'stream has two tracks');
+ assert_not_equals(e.track.id, firstRemoteTrack.id,
+ 'addtrack event has a new track');
+ assert_equals(remoteStreams.length, 1, 'Still a single remote stream.');
+ }, 'addTrack() for an existing stream makes stream.onaddtrack fire.');
+
+ promise_test(async t => {
+ const caller = new RTCPeerConnection();
+ t.add_cleanup(() => caller.close());
+ const callee = new RTCPeerConnection();
+ t.add_cleanup(() => callee.close());
+ let eventSequence = '';
+ const localStreams = await Promise.all([
+ getNoiseStream({audio: true}),
+ getNoiseStream({audio: true}),
+ ]);
+ t.add_cleanup(() => localStreams.forEach((stream) =>
+ stream.getTracks().forEach((track) => track.stop())));
+ caller.addTrack(localStreams[0].getTracks()[0], localStreams[0]);
+ const remoteStreams = [];
+ callee.ontrack = e => {
+ if (!remoteStreams.includes(e.streams[0]))
+ remoteStreams.push(e.streams[0]);
+ };
+ exchangeIceCandidates(caller, callee);
+ await exchangeOfferAnswer(caller, callee);
+ assert_equals(remoteStreams.length, 1, 'One remote stream created.');
+ const onaddtrackPromise =
+ addEventListenerPromise(t, remoteStreams[0], 'addtrack', e => {
+ eventSequence += 'stream.onaddtrack;';
+ });
+ caller.addTrack(localStreams[1].getTracks()[0], localStreams[0]);
+ await exchangeOffer(caller, callee);
+ eventSequence += 'setRemoteDescription;';
+ await onaddtrackPromise;
+ assert_equals(remoteStreams.length, 1, 'Still a single remote stream.');
+ assert_equals(eventSequence, 'stream.onaddtrack;setRemoteDescription;');
+ }, 'stream.onaddtrack fires before setRemoteDescription resolves.');
+
+ promise_test(async t => {
+ const caller = new RTCPeerConnection();
+ t.add_cleanup(() => caller.close());
+ const callee = new RTCPeerConnection();
+ t.add_cleanup(() => callee.close());
+ const localStreams = await Promise.all([
+ getNoiseStream({audio: true}),
+ getNoiseStream({audio: true}),
+ ]);
+ t.add_cleanup(() => localStreams.forEach((stream) =>
+ stream.getTracks().forEach((track) => track.stop())));
+ caller.addTrack(localStreams[0].getTracks()[0],
+ localStreams[0], localStreams[1]);
+ const ontrackPromise = addEventListenerPromise(t, callee, 'track', e => {
+ assert_equals(e.streams.length, 2, 'Two remote stream created.');
+ assert_array_equals(e.streams[0].getTracks(), [e.track],
+ 'First remote stream == [remote track].');
+ assert_array_equals(e.streams[1].getTracks(), [e.track],
+ 'Second remote stream == [remote track].');
+ assert_equals(e.streams[0].id, localStreams[0].id,
+ 'First local and remote stream IDs match.');
+ assert_equals(e.streams[1].id, localStreams[1].id,
+ 'Second local and remote stream IDs match.');
+ });
+ await exchangeOffer(caller, callee);
+ await ontrackPromise;
+ }, 'addTrack() with a track and two streams makes ontrack fire with a track and two streams.');
+
+ promise_test(async t => {
+ const caller = new RTCPeerConnection();
+ t.add_cleanup(() => caller.close());
+ const callee = new RTCPeerConnection();
+ t.add_cleanup(() => callee.close());
+ const localStream =
+ await getNoiseStream({audio: true});
+ t.add_cleanup(() => localStream.getTracks().forEach(track => track.stop()));
+ caller.addTrack(localStream.getTracks()[0], localStream);
+ const ontrackPromise = addEventListenerPromise(t, callee, 'track', e => {
+ assert_array_equals(callee.getReceivers(), [e.receiver],
+ 'getReceivers() == [e.receiver].');
+ });
+ await exchangeOffer(caller, callee);
+ await ontrackPromise;
+ }, 'ontrack\'s receiver matches getReceivers().');
+
+ promise_test(async t => {
+ const caller = new RTCPeerConnection();
+ t.add_cleanup(() => caller.close());
+ const callee = new RTCPeerConnection();
+ t.add_cleanup(() => callee.close());
+ const localStream =
+ await getNoiseStream({audio: true});
+ t.add_cleanup(() => localStream.getTracks().forEach(track => track.stop()));
+ const sender = caller.addTrack(localStream.getTracks()[0], localStream);
+ const ontrackPromise = addEventListenerPromise(t, callee, 'track');
+ exchangeIceCandidates(caller, callee);
+ await exchangeOfferAnswer(caller, callee);
+ await ontrackPromise;
+ assert_equals(callee.getReceivers().length, 1, 'One receiver created.');
+ caller.removeTrack(sender);
+ await exchangeOffer(caller, callee);
+ assert_equals(callee.getReceivers().length, 1, 'Receiver not removed.');
+ }, 'removeTrack() does not remove the receiver.');
+
+ promise_test(async t => {
+ const caller = new RTCPeerConnection();
+ t.add_cleanup(() => caller.close());
+ const callee = new RTCPeerConnection();
+ t.add_cleanup(() => callee.close());
+ const localStream =
+ await getNoiseStream({audio: true});
+ t.add_cleanup(() => localStream.getTracks().forEach(track => track.stop()));
+ const [track] = localStream.getTracks();
+ const sender = caller.addTrack(track, localStream);
+ const ontrackPromise = addEventListenerPromise(t, callee, 'track', e => {
+ assert_equals(e.streams.length, 1);
+ return e.streams[0];
+ });
+ exchangeIceCandidates(caller, callee);
+ await exchangeOfferAnswer(caller, callee);
+ const remoteStream = await ontrackPromise;
+ const remoteTrack = remoteStream.getTracks()[0];
+ const onremovetrackPromise =
+ addEventListenerPromise(t, remoteStream, 'removetrack', e => {
+ assert_equals(e.track, remoteTrack);
+ assert_equals(remoteStream.getTracks().length, 0,
+ 'Remote stream emptied of tracks.');
+ });
+ caller.removeTrack(sender);
+ await exchangeOffer(caller, callee);
+ await onremovetrackPromise;
+ }, 'removeTrack() makes stream.onremovetrack fire and the track to be removed from the stream.');
+
+ promise_test(async t => {
+ const caller = new RTCPeerConnection();
+ t.add_cleanup(() => caller.close());
+ const callee = new RTCPeerConnection();
+ t.add_cleanup(() => callee.close());
+ let eventSequence = '';
+ const localStream =
+ await getNoiseStream({audio: true});
+ t.add_cleanup(() => localStream.getTracks().forEach(track => track.stop()));
+ const sender = caller.addTrack(localStream.getTracks()[0], localStream);
+ const ontrackPromise = addEventListenerPromise(t, callee, 'track', e => {
+ assert_equals(e.streams.length, 1);
+ return e.streams[0];
+ });
+ exchangeIceCandidates(caller, callee);
+ await exchangeOfferAnswer(caller, callee);
+ const remoteStream = await ontrackPromise;
+ const remoteTrack = remoteStream.getTracks()[0];
+ const onremovetrackPromise =
+ addEventListenerPromise(t, remoteStream, 'removetrack', e => {
+ eventSequence += 'stream.onremovetrack;';
+ });
+ caller.removeTrack(sender);
+ await exchangeOffer(caller, callee);
+ eventSequence += 'setRemoteDescription;';
+ await onremovetrackPromise;
+ assert_equals(eventSequence, 'stream.onremovetrack;setRemoteDescription;');
+ }, 'stream.onremovetrack fires before setRemoteDescription resolves.');
+
+ promise_test(async t => {
+ const caller = new RTCPeerConnection();
+ t.add_cleanup(() => caller.close());
+ const callee = new RTCPeerConnection();
+ t.add_cleanup(() => callee.close());
+ const localStream =
+ await getNoiseStream({audio: true});
+ t.add_cleanup(() => localStream.getTracks().forEach(track => track.stop()));
+ const sender = caller.addTrack(localStream.getTracks()[0], localStream);
+ exchangeIceCandidates(caller, callee);
+ const e = await exchangeOfferAndListenToOntrack(t, caller, callee);
+ const remoteTrack = e.track;
+
+ // Need to wait for unmute, otherwise there's no event for the transition
+ // back to muted.
+ const onunmutePromise =
+ addEventListenerPromise(t, remoteTrack, 'unmute', () => {
+ assert_false(remoteTrack.muted);
+ });
+ await exchangeAnswer(caller, callee);
+ await onunmutePromise;
+
+ const onmutePromise =
+ addEventListenerPromise(t, remoteTrack, 'mute', () => {
+ assert_true(remoteTrack.muted);
+ });
+ caller.removeTrack(sender);
+ await exchangeOffer(caller, callee);
+ await onmutePromise;
+ }, 'removeTrack() makes track.onmute fire and the track to be muted.');
+
+ promise_test(async t => {
+ const caller = new RTCPeerConnection();
+ t.add_cleanup(() => caller.close());
+ const callee = new RTCPeerConnection();
+ t.add_cleanup(() => callee.close());
+ let eventSequence = '';
+ const localStream =
+ await getNoiseStream({audio: true});
+ t.add_cleanup(() => localStream.getTracks().forEach(track => track.stop()));
+ const sender = caller.addTrack(localStream.getTracks()[0], localStream);
+ const ontrackPromise = addEventListenerPromise(t, callee, 'track', e => {
+ assert_equals(e.streams.length, 1);
+ return e.streams[0];
+ });
+ exchangeIceCandidates(caller, callee);
+ const e = await exchangeOfferAndListenToOntrack(t, caller, callee);
+ const remoteTrack = e.track;
+
+ // Need to wait for unmute, otherwise there's no event for the transition
+ // back to muted.
+ const onunmutePromise =
+ addEventListenerPromise(t, remoteTrack, 'unmute', () => {
+ assert_false(remoteTrack.muted);
+ });
+ await exchangeAnswer(caller, callee);
+ await onunmutePromise;
+
+ const onmutePromise =
+ addEventListenerPromise(t, remoteTrack, 'mute', () => {
+ eventSequence += 'track.onmute;';
+ });
+ caller.removeTrack(sender);
+ await exchangeOffer(caller, callee);
+ eventSequence += 'setRemoteDescription;';
+ await onmutePromise;
+ assert_equals(eventSequence, 'track.onmute;setRemoteDescription;');
+ }, 'track.onmute fires before setRemoteDescription resolves.');
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const stream = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const sender = pc.addTrack(stream.getTracks()[0]);
+ pc.removeTrack(sender);
+ pc.removeTrack(sender);
+ }, 'removeTrack() twice is safe.');
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-setRemoteDescription.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-setRemoteDescription.html
new file mode 100644
index 0000000000..c170f766bd
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-setRemoteDescription.html
@@ -0,0 +1,171 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerConnection.prototype.setRemoteDescription</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="RTCPeerConnection-helper.js"></script>
+<script>
+ 'use strict';
+
+ // Test is based on the following editor draft:
+ // https://w3c.github.io/webrtc-pc/archives/20170605/webrtc.html
+
+ // The following helper functions are called from RTCPeerConnection-helper.js:
+ // assert_session_desc_not_similar()
+ // assert_session_desc_similar()
+
+ /*
+ 4.3.2. Interface Definition
+ [Constructor(optional RTCConfiguration configuration)]
+ interface RTCPeerConnection : EventTarget {
+ Promise<void> setRemoteDescription(
+ RTCSessionDescriptionInit description);
+
+ readonly attribute RTCSessionDescription? remoteDescription;
+ readonly attribute RTCSessionDescription? currentRemoteDescription;
+ readonly attribute RTCSessionDescription? pendingRemoteDescription;
+ ...
+ };
+
+ 4.6.2. RTCSessionDescription Class
+ dictionary RTCSessionDescriptionInit {
+ required RTCSdpType type;
+ DOMString sdp = "";
+ };
+
+ 4.6.1. RTCSdpType
+ enum RTCSdpType {
+ "offer",
+ "pranswer",
+ "answer",
+ "rollback"
+ };
+ */
+
+ /*
+ 4.6.1. enum RTCSdpType
+ */
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ // SDP is validated after WebIDL validation
+ try {
+ await pc.setRemoteDescription({ type: 'bogus', sdp: 'bogus' });
+ t.unreached_func("Should have rejected.");
+ } catch (e) {
+ assert_throws_js(TypeError, () => { throw e });
+ }
+ }, 'setRemoteDescription with invalid type and invalid SDP should reject with TypeError');
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ // SDP is validated after validating type
+ try {
+ await pc.setRemoteDescription({ type: 'answer', sdp: 'invalid' });
+ t.unreached_func("Should have rejected.");
+ } catch (e) {
+ assert_throws_dom('InvalidStateError', () => { throw e });
+ }
+ }, 'setRemoteDescription() with invalid SDP and stable state should reject with InvalidStateError');
+
+ /* Dedicated signalingstate events test. */
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ t.add_cleanup(() => pc2.close());
+
+ let eventCount = 0;
+ const states = [
+ 'stable', 'have-local-offer', 'stable', 'have-remote-offer',
+ ];
+ pc.onsignalingstatechange = t.step_func(() =>
+ assert_equals(pc.signalingState, states[++eventCount]));
+
+ const assert_state = state => {
+ assert_equals(state, pc.signalingState);
+ assert_equals(state, states[eventCount]);
+ };
+
+ const offer = await generateAudioReceiveOnlyOffer(pc);
+ assert_state('stable');
+ await pc.setLocalDescription(offer);
+ assert_state('have-local-offer');
+ await pc2.setRemoteDescription(offer);
+ await exchangeAnswer(pc, pc2);
+ assert_state('stable');
+ await exchangeOffer(pc2, pc);
+ assert_state('have-remote-offer');
+ }, 'Negotiation should fire signalingsstate events');
+
+ /* Operations after returning to stable state */
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ t.add_cleanup(() => pc2.close());
+
+ const offer1 = await generateAudioReceiveOnlyOffer(pc2);
+ await pc2.setLocalDescription(offer1);
+ await pc.setRemoteDescription(offer1);
+ await exchangeAnswer(pc2, pc);
+ const offer2 = await generateVideoReceiveOnlyOffer(pc2);
+ await pc2.setLocalDescription(offer2);
+ await pc.setRemoteDescription(offer2);
+ assert_session_desc_not_similar(offer1, offer2);
+ assert_session_desc_similar(pc.remoteDescription, offer2);
+ assert_session_desc_similar(pc.currentRemoteDescription, offer1);
+ assert_session_desc_similar(pc.pendingRemoteDescription, offer2);
+ }, 'Calling setRemoteDescription() again after one round of remote-offer/local-answer should succeed');
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ t.add_cleanup(() => pc2.close());
+
+ const offer = await generateAudioReceiveOnlyOffer(pc);
+ await pc.setLocalDescription(offer);
+ await pc2.setRemoteDescription(offer);
+ const answer = await pc2.createAnswer();
+ await pc2.setLocalDescription(answer);
+ await pc.setRemoteDescription(answer);
+ await exchangeOffer(pc2, pc);
+ assert_equals(pc.remoteDescription.sdp, pc.pendingRemoteDescription.sdp);
+ assert_session_desc_similar(pc.remoteDescription, offer);
+ assert_session_desc_similar(pc.currentRemoteDescription, answer);
+ }, 'Switching role from offerer to answerer after going back to stable state should succeed');
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const offer = await pc.createOffer();
+ const p = Promise.race([
+ pc.setRemoteDescription(offer),
+ new Promise(r => t.step_timeout(() => r("timeout"), 200))
+ ]);
+ pc.close();
+ assert_equals(await p, "timeout");
+ assert_equals(pc.signalingState, "closed", "In closed state");
+ }, 'Closing on setRemoteDescription() neither resolves nor rejects');
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const offer = await pc.createOffer();
+ await pc.setLocalDescription(offer);
+ const p = Promise.race([
+ pc.setRemoteDescription(offer),
+ new Promise(r => t.step_timeout(() => r("timeout"), 200))
+ ]);
+ pc.close();
+ assert_equals(await p, "timeout");
+ assert_equals(pc.signalingState, "closed", "In closed state");
+ }, 'Closing on rollback neither resolves nor rejects');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-transceivers.https.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-transceivers.https.html
new file mode 100644
index 0000000000..bb8ec2fe2b
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-transceivers.https.html
@@ -0,0 +1,509 @@
+<!doctype html>
+<meta name="timeout" content="long"/>
+<meta charset=utf-8>
+<title>RTCPeerConnection-transceivers.https.html</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="RTCPeerConnection-helper.js"></script>
+<script>
+'use strict';
+
+// The following helper functions are called from RTCPeerConnection-helper.js:
+// exchangeOffer
+// exchangeOfferAndListenToOntrack
+// exchangeAnswer
+// exchangeAnswerAndListenToOntrack
+// addEventListenerPromise
+// createPeerConnectionWithCleanup
+// createTrackAndStreamWithCleanup
+// findTransceiverForSender
+
+promise_test(async t => {
+ const pc = createPeerConnectionWithCleanup(t);
+ const [track, stream] = await createTrackAndStreamWithCleanup(t);
+ const sender = pc.addTrack(track, stream);
+ const transceiver = findTransceiverForSender(pc, sender);
+ assert_true(transceiver instanceof RTCRtpTransceiver);
+ assert_true(transceiver.sender instanceof RTCRtpSender);
+ assert_equals(transceiver.sender, sender);
+}, 'addTrack: creates a transceiver for the sender');
+
+promise_test(async t => {
+ const pc = createPeerConnectionWithCleanup(t);
+ const [track, stream] = await createTrackAndStreamWithCleanup(t);
+ const transceiver = findTransceiverForSender(pc, pc.addTrack(track, stream));
+ assert_array_equals(pc.getTransceivers(), [transceiver],
+ 'pc.getTransceivers() equals [transceiver]');
+ assert_array_equals(pc.getSenders(), [transceiver.sender],
+ 'pc.getSenders() equals [transceiver.sender]');
+ assert_array_equals(pc.getReceivers(), [transceiver.receiver],
+ 'pc.getReceivers() equals [transceiver.receiver]');
+}, 'addTrack: "transceiver == {sender,receiver}"');
+
+promise_test(async t => {
+ const pc = createPeerConnectionWithCleanup(t);
+ const [track, stream] = await createTrackAndStreamWithCleanup(t);
+ const transceiver = findTransceiverForSender(pc, pc.addTrack(track, stream));
+ assert_true(transceiver.sender.track instanceof MediaStreamTrack,
+ 'transceiver.sender.track instanceof MediaStreamTrack');
+ assert_equals(transceiver.sender.track, track,
+ 'transceiver.sender.track == track');
+}, 'addTrack: transceiver.sender is associated with the track');
+
+promise_test(async t => {
+ const pc = createPeerConnectionWithCleanup(t);
+ const [track, stream] = await createTrackAndStreamWithCleanup(t);
+ const transceiver = findTransceiverForSender(pc, pc.addTrack(track, stream));
+ assert_true(transceiver.receiver instanceof RTCRtpReceiver,
+ 'transceiver.receiver instanceof RTCRtpReceiver');
+ assert_true(transceiver.receiver.track instanceof MediaStreamTrack,
+ 'transceiver.receiver.track instanceof MediaStreamTrack');
+ assert_not_equals(transceiver.receiver.track, track,
+ 'transceiver.receiver.track != track');
+}, 'addTrack: transceiver.receiver has its own track');
+
+promise_test(async t => {
+ const pc = createPeerConnectionWithCleanup(t);
+ const [track, stream] = await createTrackAndStreamWithCleanup(t);
+ const transceiver = findTransceiverForSender(pc, pc.addTrack(track, stream));
+ assert_true(transceiver.receiver.track.muted);
+}, 'addTrack: transceiver.receiver\'s track is muted');
+
+promise_test(async t => {
+ const pc = createPeerConnectionWithCleanup(t);
+ const [track, stream] = await createTrackAndStreamWithCleanup(t);
+ const transceiver = findTransceiverForSender(pc, pc.addTrack(track, stream));
+ assert_equals(transceiver.mid, null);
+}, 'addTrack: transceiver is not associated with an m-section');
+
+promise_test(async t => {
+ const pc = createPeerConnectionWithCleanup(t);
+ const [track, stream] = await createTrackAndStreamWithCleanup(t);
+ const transceiver = findTransceiverForSender(pc, pc.addTrack(track, stream));
+ // `stopped` is non-standard. Move to external/wpt/webrtc/legacy/?
+ assert_false(transceiver.stopped);
+}, 'addTrack: transceiver is not stopped');
+
+promise_test(async t => {
+ const pc = createPeerConnectionWithCleanup(t);
+ const [track, stream] = await createTrackAndStreamWithCleanup(t);
+ const transceiver = findTransceiverForSender(pc, pc.addTrack(track, stream));
+ assert_equals(transceiver.direction, 'sendrecv');
+}, 'addTrack: transceiver\'s direction is sendrecv');
+
+promise_test(async t => {
+ const pc = createPeerConnectionWithCleanup(t);
+ const [track, stream] = await createTrackAndStreamWithCleanup(t);
+ const transceiver = findTransceiverForSender(pc, pc.addTrack(track, stream));
+ assert_equals(transceiver.currentDirection, null);
+}, 'addTrack: transceiver\'s currentDirection is null');
+
+promise_test(async t => {
+ const pc = createPeerConnectionWithCleanup(t);
+ const [track, stream] = await createTrackAndStreamWithCleanup(t);
+ const transceiver = findTransceiverForSender(pc, pc.addTrack(track, stream));
+ await pc.setLocalDescription(await pc.createOffer());
+ assert_not_equals(transceiver.mid, null, 'transceiver.mid != null');
+}, 'setLocalDescription(offer): transceiver gets associated with an m-section');
+
+promise_test(async t => {
+ const pc = createPeerConnectionWithCleanup(t);
+ const [track, stream] = await createTrackAndStreamWithCleanup(t);
+ const transceiver = findTransceiverForSender(pc, pc.addTrack(track, stream));
+ const offer = await pc.createOffer();
+ await pc.setLocalDescription(offer);
+ let sdp = offer.sdp;
+ let sdpMidLineStart = sdp.indexOf('a=mid:');
+ let sdpMidLineEnd = sdp.indexOf('\r\n', sdpMidLineStart);
+ assert_true(sdpMidLineStart != -1 && sdpMidLineEnd != -1,
+ 'Failed to parse offer SDP for a=mid');
+ let parsedMid = sdp.substring(sdpMidLineStart + 6, sdpMidLineEnd);
+ assert_equals(transceiver.mid, parsedMid, 'transceiver.mid == parsedMid');
+}, 'setLocalDescription(offer): transceiver.mid matches the offer SDP');
+
+promise_test(async t => {
+ const pc1 = createPeerConnectionWithCleanup(t);
+ pc1.addTrack(... await createTrackAndStreamWithCleanup(t));
+ const pc2 = createPeerConnectionWithCleanup(t);
+ const trackEvent = await exchangeOfferAndListenToOntrack(t, pc1, pc2);
+ assert_true(trackEvent instanceof RTCTrackEvent,
+ 'trackEvent instanceof RTCTrackEvent');
+ assert_true(trackEvent.track instanceof MediaStreamTrack,
+ 'trackEvent.track instanceof MediaStreamTrack');
+}, 'setRemoteDescription(offer): ontrack fires with a track');
+
+promise_test(async t => {
+ const pc1 = createPeerConnectionWithCleanup(t);
+ const [track, stream] = await createTrackAndStreamWithCleanup(t);
+ pc1.addTrack(track, stream);
+ const pc2 = createPeerConnectionWithCleanup(t);
+ const trackEvent = await exchangeOfferAndListenToOntrack(t, pc1, pc2);
+ assert_true(trackEvent.track instanceof MediaStreamTrack,
+ 'trackEvent.track instanceof MediaStreamTrack');
+ assert_equals(trackEvent.streams.length, 1,
+ 'trackEvent contains a single stream');
+ assert_true(trackEvent.streams[0] instanceof MediaStream,
+ 'trackEvent has a MediaStream');
+ assert_equals(trackEvent.streams[0].id, stream.id,
+ 'trackEvent.streams[0].id == stream.id');
+}, 'setRemoteDescription(offer): ontrack\'s stream.id is the same as stream.id');
+
+promise_test(async t => {
+ const pc1 = createPeerConnectionWithCleanup(t);
+ pc1.addTrack(... await createTrackAndStreamWithCleanup(t));
+ const pc2 = createPeerConnectionWithCleanup(t);
+ const trackEvent = await exchangeOfferAndListenToOntrack(t, pc1, pc2);
+ assert_true(trackEvent.transceiver instanceof RTCRtpTransceiver,
+ 'trackEvent.transceiver instanceof RTCRtpTransceiver');
+}, 'setRemoteDescription(offer): ontrack fires with a transceiver.');
+
+promise_test(async t => {
+ const pc1 = createPeerConnectionWithCleanup(t);
+ const [track, stream] = await createTrackAndStreamWithCleanup(t);
+ const transceiver = findTransceiverForSender(pc1, pc1.addTrack(track, stream));
+ const pc2 = createPeerConnectionWithCleanup(t);
+ const trackEvent = await exchangeOfferAndListenToOntrack(t, pc1, pc2);
+ assert_equals(transceiver.mid, trackEvent.transceiver.mid);
+}, 'setRemoteDescription(offer): transceiver.mid is the same on both ends');
+
+promise_test(async t => {
+ const pc1 = createPeerConnectionWithCleanup(t);
+ pc1.addTrack(... await createTrackAndStreamWithCleanup(t));
+ const pc2 = createPeerConnectionWithCleanup(t);
+ const trackEvent = await exchangeOfferAndListenToOntrack(t, pc1, pc2);
+ const transceiver = trackEvent.transceiver;
+ assert_array_equals(pc2.getTransceivers(), [transceiver],
+ 'pc2.getTransceivers() equals [transceiver]');
+ assert_array_equals(pc2.getSenders(), [transceiver.sender],
+ 'pc2.getSenders() equals [transceiver.sender]');
+ assert_array_equals(pc2.getReceivers(), [transceiver.receiver],
+ 'pc2.getReceivers() equals [transceiver.receiver]');
+}, 'setRemoteDescription(offer): "transceiver == {sender,receiver}"');
+
+promise_test(async t => {
+ const pc1 = createPeerConnectionWithCleanup(t);
+ pc1.addTrack(... await createTrackAndStreamWithCleanup(t));
+ const pc2 = createPeerConnectionWithCleanup(t);
+ const trackEvent = await exchangeOfferAndListenToOntrack(t, pc1, pc2);
+ assert_equals(trackEvent.transceiver.direction, 'recvonly');
+}, 'setRemoteDescription(offer): transceiver.direction is recvonly');
+
+promise_test(async t => {
+ const pc1 = createPeerConnectionWithCleanup(t);
+ pc1.addTrack(... await createTrackAndStreamWithCleanup(t));
+ const pc2 = createPeerConnectionWithCleanup(t);
+ const trackEvent = await exchangeOfferAndListenToOntrack(t, pc1, pc2);
+ assert_equals(trackEvent.transceiver.currentDirection, null);
+}, 'setRemoteDescription(offer): transceiver.currentDirection is null');
+
+promise_test(async t => {
+ const pc1 = createPeerConnectionWithCleanup(t);
+ pc1.addTrack(... await createTrackAndStreamWithCleanup(t));
+ const pc2 = createPeerConnectionWithCleanup(t);
+ const trackEvent = await exchangeOfferAndListenToOntrack(t, pc1, pc2);
+ // `stopped` is non-standard. Move to external/wpt/webrtc/legacy/?
+ assert_false(trackEvent.transceiver.stopped);
+}, 'setRemoteDescription(offer): transceiver.stopped is false');
+
+promise_test(async t => {
+ const pc1 = createPeerConnectionWithCleanup(t);
+ pc1.addTrack(... await createTrackAndStreamWithCleanup(t));
+ const pc2 = createPeerConnectionWithCleanup(t);
+ const trackEvent = await exchangeOfferAndListenToOntrack(t, pc1, pc2);
+ const transceiver = trackEvent.transceiver;
+ assert_equals(transceiver.currentDirection, null,
+ 'SRD(offer): transceiver.currentDirection is null');
+ await pc2.setLocalDescription(await pc2.createAnswer());
+ assert_equals(transceiver.currentDirection, 'recvonly',
+ 'SLD(answer): transceiver.currentDirection is recvonly');
+}, 'setLocalDescription(answer): transceiver.currentDirection is recvonly');
+
+promise_test(async t => {
+ const pc1 = createPeerConnectionWithCleanup(t);
+ const [track, stream] = await createTrackAndStreamWithCleanup(t);
+ const transceiver = findTransceiverForSender(pc1, pc1.addTrack(track, stream));
+ const pc2 = createPeerConnectionWithCleanup(t);
+ await exchangeOffer(pc1, pc2);
+ assert_equals(transceiver.currentDirection, null,
+ 'SLD(offer): transceiver.currentDirection is null');
+ await exchangeAnswer(pc1, pc2);
+ assert_equals(transceiver.currentDirection, 'sendonly',
+ 'SRD(answer): transceiver.currentDirection is sendonly');
+}, 'setLocalDescription(answer): transceiver.currentDirection is sendonly');
+
+promise_test(async t => {
+ const pc = createPeerConnectionWithCleanup(t);
+ const [track, stream] = await createTrackAndStreamWithCleanup(t);
+ const transceiver = pc.addTransceiver(track);
+ assert_true(transceiver instanceof RTCRtpTransceiver);
+ assert_true(transceiver.sender instanceof RTCRtpSender);
+ assert_true(transceiver.receiver instanceof RTCRtpReceiver);
+ assert_equals(transceiver.sender.track, track);
+}, 'addTransceiver(track): creates a transceiver for the track');
+
+promise_test(async t => {
+ const pc = createPeerConnectionWithCleanup(t);
+ const [track, stream] = await createTrackAndStreamWithCleanup(t);
+ const transceiver = pc.addTransceiver(track);
+ assert_array_equals(pc.getTransceivers(), [transceiver],
+ 'pc.getTransceivers() equals [transceiver]');
+ assert_array_equals(pc.getSenders(), [transceiver.sender],
+ 'pc.getSenders() equals [transceiver.sender]');
+ assert_array_equals(pc.getReceivers(), [transceiver.receiver],
+ 'pc.getReceivers() equals [transceiver.receiver]');
+}, 'addTransceiver(track): "transceiver == {sender,receiver}"');
+
+promise_test(async t => {
+ const pc = createPeerConnectionWithCleanup(t);
+ const [track, stream] = await createTrackAndStreamWithCleanup(t);
+ const transceiver = pc.addTransceiver(track, {direction:'inactive'});
+ assert_equals(transceiver.direction, 'inactive');
+}, 'addTransceiver(track, init): initialize direction to inactive');
+
+promise_test(async t => {
+ const pc = createPeerConnectionWithCleanup(t);
+ const otherPc = createPeerConnectionWithCleanup(t);
+ const [track, stream] = await createTrackAndStreamWithCleanup(t);
+ const transceiver = pc.addTransceiver(track, {
+ sendEncodings: [{active:false}]
+ });
+
+ // Negotiate parameters.
+ const offer = await pc.createOffer();
+ await pc.setLocalDescription(offer);
+ await otherPc.setRemoteDescription(offer);
+ const answer = await otherPc.createAnswer();
+ await otherPc.setLocalDescription(answer);
+ await pc.setRemoteDescription(answer);
+
+ const params = transceiver.sender.getParameters();
+ assert_false(params.encodings[0].active);
+}, 'addTransceiver(track, init): initialize sendEncodings[0].active to false');
+
+promise_test(async t => {
+ const pc1 = createPeerConnectionWithCleanup(t);
+ const pc2 = createPeerConnectionWithCleanup(t);
+ const [track] = await createTrackAndStreamWithCleanup(t);
+ pc1.addTransceiver(track, {streams:[]});
+ const trackEvent = await exchangeOfferAndListenToOntrack(t, pc1, pc2);
+ assert_equals(trackEvent.streams.length, 0, 'trackEvent.streams.length == 0');
+}, 'addTransceiver(0 streams): ontrack fires with no stream');
+
+promise_test(async t => {
+ const pc1 = createPeerConnectionWithCleanup(t);
+ const pc2 = createPeerConnectionWithCleanup(t);
+ const [track] = await createTrackAndStreamWithCleanup(t);
+ const stream = new MediaStream();
+ pc1.addTransceiver(track, {streams:[stream]});
+ const trackEvent = await exchangeOfferAndListenToOntrack(t, pc1, pc2);
+ assert_equals(trackEvent.streams.length, 1, 'trackEvent.streams.length == 1');
+ assert_equals(trackEvent.streams[0].id, stream.id,
+ 'trackEvent.streams[0].id == stream.id');
+}, 'addTransceiver(1 stream): ontrack fires with corresponding stream');
+
+promise_test(async t => {
+ const pc1 = createPeerConnectionWithCleanup(t);
+ const pc2 = createPeerConnectionWithCleanup(t);
+ const [track] = await createTrackAndStreamWithCleanup(t);
+ const stream0 = new MediaStream();
+ const stream1 = new MediaStream();
+ pc1.addTransceiver(track, {streams:[stream0, stream1]});
+ const trackEvent = await exchangeOfferAndListenToOntrack(t, pc1, pc2);
+ assert_equals(trackEvent.streams.length, 2, 'trackEvent.streams.length == 2');
+ assert_equals(trackEvent.streams[0].id, stream0.id,
+ 'trackEvent.streams[0].id == stream0.id');
+ assert_equals(trackEvent.streams[1].id, stream1.id,
+ 'trackEvent.streams[1].id == stream1.id');
+}, 'addTransceiver(2 streams): ontrack fires with corresponding two streams');
+
+promise_test(async t => {
+ const pc1 = createPeerConnectionWithCleanup(t);
+ const pc2 = createPeerConnectionWithCleanup(t);
+ const [track] = await createTrackAndStreamWithCleanup(t);
+ pc1.addTrack(track);
+ const trackEvent = await exchangeOfferAndListenToOntrack(t, pc1, pc2);
+ assert_equals(trackEvent.streams.length, 0, 'trackEvent.streams.length == 0');
+}, 'addTrack(0 streams): ontrack fires with no stream');
+
+promise_test(async t => {
+ const pc1 = createPeerConnectionWithCleanup(t);
+ const pc2 = createPeerConnectionWithCleanup(t);
+ const [track] = await createTrackAndStreamWithCleanup(t);
+ const stream = new MediaStream();
+ pc1.addTrack(track, stream);
+ const trackEvent = await exchangeOfferAndListenToOntrack(t, pc1, pc2);
+ assert_equals(trackEvent.streams.length, 1, 'trackEvent.streams.length == 1');
+ assert_equals(trackEvent.streams[0].id, stream.id,
+ 'trackEvent.streams[0].id == stream.id');
+}, 'addTrack(1 stream): ontrack fires with corresponding stream');
+
+promise_test(async t => {
+ const pc1 = createPeerConnectionWithCleanup(t);
+ const pc2 = createPeerConnectionWithCleanup(t);
+ const [track] = await createTrackAndStreamWithCleanup(t);
+ const stream0 = new MediaStream();
+ const stream1 = new MediaStream();
+ pc1.addTrack(track, stream0, stream1);
+ const trackEvent = await exchangeOfferAndListenToOntrack(t, pc1, pc2);
+ assert_equals(trackEvent.streams.length, 2, 'trackEvent.streams.length == 2');
+ assert_equals(trackEvent.streams[0].id, stream0.id,
+ 'trackEvent.streams[0].id == stream0.id');
+ assert_equals(trackEvent.streams[1].id, stream1.id,
+ 'trackEvent.streams[1].id == stream1.id');
+}, 'addTrack(2 streams): ontrack fires with corresponding two streams');
+
+promise_test(async t => {
+ const pc = createPeerConnectionWithCleanup(t);
+ const [track, stream] = await createTrackAndStreamWithCleanup(t);
+ const transceiver = pc.addTransceiver('audio');
+ assert_equals(transceiver.direction, 'sendrecv');
+}, 'addTransceiver(\'audio\'): creates a transceiver with direction sendrecv');
+
+promise_test(async t => {
+ const pc = createPeerConnectionWithCleanup(t);
+ const [track, stream] = await createTrackAndStreamWithCleanup(t);
+ const transceiver = pc.addTransceiver('audio');
+ assert_equals(transceiver.receiver.track.kind, 'audio');
+}, 'addTransceiver(\'audio\'): transceiver.receiver.track.kind == \'audio\'');
+
+promise_test(async t => {
+ const pc = createPeerConnectionWithCleanup(t);
+ const [track, stream] = await createTrackAndStreamWithCleanup(t);
+ const transceiver = pc.addTransceiver('video');
+ assert_equals(transceiver.receiver.track.kind, 'video');
+}, 'addTransceiver(\'video\'): transceiver.receiver.track.kind == \'video\'');
+
+promise_test(async t => {
+ const pc = createPeerConnectionWithCleanup(t);
+ const [track, stream] = await createTrackAndStreamWithCleanup(t);
+ const transceiver = pc.addTransceiver('audio');
+ assert_equals(transceiver.sender.track, null);
+}, 'addTransceiver(\'audio\'): transceiver.sender.track == null');
+
+promise_test(async t => {
+ const pc = createPeerConnectionWithCleanup(t);
+ const [track, stream] = await createTrackAndStreamWithCleanup(t);
+ const transceiver = pc.addTransceiver('audio');
+ assert_equals(transceiver.currentDirection, null);
+}, 'addTransceiver(\'audio\'): transceiver.currentDirection is null');
+
+promise_test(async t => {
+ const pc = createPeerConnectionWithCleanup(t);
+ const [track, stream] = await createTrackAndStreamWithCleanup(t);
+ const transceiver = pc.addTransceiver('audio');
+ // `stopped` is non-standard. Move to external/wpt/webrtc/legacy/?
+ assert_false(transceiver.stopped);
+}, 'addTransceiver(\'audio\'): transceiver.stopped is false');
+
+promise_test(async t => {
+ const pc = createPeerConnectionWithCleanup(t);
+ const [track, stream] = await createTrackAndStreamWithCleanup(t, 'audio');
+ const transceiver = pc.addTransceiver('audio');
+ const sender = pc.addTrack(track, stream);
+ assert_equals(sender, transceiver.sender, 'sender == transceiver.sender');
+ assert_equals(sender.track, track, 'sender.track == track');
+}, 'addTrack reuses reusable transceivers');
+
+promise_test(async t => {
+ const pc = createPeerConnectionWithCleanup(t);
+ const [track, stream] = await createTrackAndStreamWithCleanup(t, 'audio');
+ const t1 = pc.addTransceiver('audio');
+ const t2 = pc.addTransceiver(track);
+ assert_not_equals(t2, t1, 't2 != t1');
+ assert_equals(t2.sender.track, track, 't2.sender.track == track');
+}, 'addTransceiver does not reuse reusable transceivers');
+
+promise_test(async t => {
+ const pc1 = createPeerConnectionWithCleanup(t);
+ const pc2 = createPeerConnectionWithCleanup(t);
+ const [track, stream] = await createTrackAndStreamWithCleanup(t);
+ const pc1Transceiver = findTransceiverForSender(pc1, pc1.addTrack(track, stream));
+ const pc2TrackEvent = await exchangeOfferAndListenToOntrack(t, pc1, pc2);
+ const pc2Transceiver = pc2TrackEvent.transceiver;
+ assert_equals(pc2Transceiver.direction, 'recvonly',
+ 'pc2Transceiver.direction is recvonly after SRD(offer)');
+ const pc2Sender = pc2.addTrack(track, stream);
+ assert_equals(pc2Transceiver.sender, pc2Sender,
+ 'pc2Transceiver.sender == sender');
+ assert_equals(pc2Transceiver.direction, 'sendrecv',
+ 'pc2Transceiver.direction is sendrecv after addTrack()');
+ assert_equals(pc2Transceiver.currentDirection, null,
+ 'pc2Transceiver.currentDirection is null before answer');
+ const pc1TrackEvent = await exchangeAnswerAndListenToOntrack(t, pc1, pc2);
+ assert_equals(pc2Transceiver.currentDirection, 'sendrecv',
+ 'pc2Transceiver.currentDirection is sendrecv after SLD(answer)');
+ assert_equals(pc1TrackEvent.transceiver, pc1Transceiver,
+ 'Answer: pc1.ontrack fires with the existing transceiver.');
+ assert_equals(pc1Transceiver.currentDirection, 'sendrecv',
+ 'pc1Transceiver.currentDirection is sendrecv');
+ assert_equals(pc2.getTransceivers().length, 1,
+ 'pc2.getTransceivers().length == 1');
+ assert_equals(pc1.getTransceivers().length, 1,
+ 'pc1.getTransceivers().length == 1');
+}, 'Can setup two-way call using a single transceiver');
+
+promise_test(async t => {
+ const pc1 = createPeerConnectionWithCleanup(t);
+ const pc2 = createPeerConnectionWithCleanup(t);
+ const [track, stream] = await createTrackAndStreamWithCleanup(t, 'audio');
+ const transceiver = pc1.addTransceiver(track);
+ await exchangeOffer(pc1, pc2);
+ await exchangeAnswer(pc1, pc2);
+ assert_equals(transceiver.currentDirection, 'sendonly');
+ // `stopped` is non-standard. Move to external/wpt/webrtc/legacy/?
+ assert_false(transceiver.stopped);
+ pc1.close();
+ assert_equals(transceiver.currentDirection, 'stopped');
+ assert_true(transceiver.stopped);
+}, 'Closing the PC stops the transceivers');
+
+promise_test(async t => {
+ const pc1 = createPeerConnectionWithCleanup(t);
+ const pc1Sender = pc1.addTrack(... await createTrackAndStreamWithCleanup(t));
+ const localTransceiver = findTransceiverForSender(pc1, pc1Sender);
+ const pc2 = createPeerConnectionWithCleanup(t);
+ exchangeIceCandidates(pc1, pc2);
+
+ const e = await exchangeOfferAndListenToOntrack(t, pc1, pc2);
+ await exchangeAnswer(pc1, pc2);
+ localTransceiver.direction = 'inactive';
+ await exchangeOfferAnswer(pc1, pc2);
+
+ localTransceiver.direction = 'sendrecv';
+ await exchangeOfferAndListenToOntrack(t, pc1, pc2);
+}, 'Changing transceiver direction to \'sendrecv\' makes ontrack fire');
+
+// Regression test coverage for https://crbug.com/950280.
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+ const pc2Promise = pc2.createOffer()
+ .then((offer) => { return pc1.setRemoteDescription(offer); })
+ .then(() => { return pc1.createAnswer(); })
+ .then((answer) => { return pc1.setLocalDescription(answer); });
+ pc1.addTransceiver('audio', { direction: 'recvonly' });
+ const pc1Promise = pc1.createOffer()
+ .then(() => { pc1.addTrack(pc1.getReceivers()[0].track); });
+ await Promise.all([pc1Promise, pc2Promise]);
+ assert_equals(pc1.getSenders()[0].track, pc1.getReceivers()[0].track);
+}, 'transceiver.sender.track does not revert to an old state');
+
+// Regression test coverage for https://crbug.com/950280.
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+ const pc2Promise = pc2.createOffer()
+ .then((offer) => { return pc1.setRemoteDescription(offer); })
+ .then(() => { return pc1.createAnswer(); });
+ pc1.addTransceiver('audio', { direction: 'recvonly' });
+ const pc1Promise = pc1.createOffer()
+ .then(() => { pc1.getTransceivers()[0].direction = 'inactive'; });
+ await Promise.all([pc1Promise, pc2Promise]);
+ assert_equals(pc1.getTransceivers()[0].direction, 'inactive');
+}, 'transceiver.direction does not revert to an old state');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-transport-stats.https.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-transport-stats.https.html
new file mode 100644
index 0000000000..3dfba16c56
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-transport-stats.https.html
@@ -0,0 +1,46 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerConnection a=setup SDP parameter test</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./third_party/sdp/sdp.js"></script>
+<script>
+'use strict';
+
+// Tests for correct behavior of the transport-stats.
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ pc1.createDataChannel('wpt');
+ await pc1.setLocalDescription();
+ const stats = await pc1.getStats();
+ let transportStats;
+ stats.forEach(report => {
+ if (report.type === 'transport') {
+ transportStats = report;
+ }
+ });
+ assert_equals(transportStats.dtlsState, 'new');
+ assert_equals(transportStats.dtlsRole, 'unknown');
+}, 'DTLS statistics on transport-stats after setLocalDescription');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ pc1.createDataChannel('wpt');
+ await pc1.setLocalDescription();
+ const sections = SDPUtils.splitSections(pc1.localDescription.sdp);
+ const iceParameters = SDPUtils.getIceParameters(sections[1], sections[0]);
+ const stats = await pc1.getStats();
+ let transportStats;
+ stats.forEach(report => {
+ if (report.type === 'transport') {
+ transportStats = report;
+ }
+ });
+ assert_equals(transportStats.iceRole, 'controlling');
+ assert_equals(transportStats.iceLocalUsernameFragment, iceParameters.usernameFragment);
+ assert_equals(transportStats.iceState, 'new');
+ assert_equals(transportStats.selectedCandidatePairChanges, 0);
+}, 'ICE statistics on transport-stats after setLocalDescription');
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-videoDetectorTest.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-videoDetectorTest.html
new file mode 100644
index 0000000000..6786bd49ed
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-videoDetectorTest.html
@@ -0,0 +1,84 @@
+<!doctype html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<title>RTCPeerConnection Video detector test</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="RTCPeerConnection-helper.js"></script>
+<script>
+'use strict';
+
+// This test verifies that the helper function "detectSignal" from
+// RTCPeerConnectionHelper, which is used to detect changes in a video
+// signal, performs properly for a range of "signal" values.
+
+// If it fails, it indicates that the video codec used in this particular
+// browser at this time doesn't reproduce the luma signal reliably enough
+// for this particular application, which may lead to other tests that
+// use the "detectSignal" helper failing without an obvious cause.
+
+// The most likely failure is timeout - which will happen if the
+// luma value detected doesn't settle within the margin of error before
+// the test times out.
+
+async function signalSettlementTime(t, v, sender, signal, backgroundTrack) {
+ const detectionStream = await getNoiseStream({video: {signal}});
+ const [detectionTrack] = detectionStream.getTracks();
+ try {
+ await sender.replaceTrack(detectionTrack);
+ const framesBefore = v.getVideoPlaybackQuality().totalVideoFrames;
+ await detectSignal(t, v, signal);
+ const framesAfter = v.getVideoPlaybackQuality().totalVideoFrames;
+ await sender.replaceTrack(backgroundTrack);
+ await detectSignal(t, v, 100);
+ return framesAfter - framesBefore;
+ } finally {
+ detectionTrack.stop();
+ }
+}
+
+promise_test(async t => {
+ const v = document.createElement('video');
+ v.autoplay = true;
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+ const stream1 = await getNoiseStream({video: {signal: 100}});
+ const [track1] = stream1.getTracks();
+ t.add_cleanup(() => track1.stop());
+
+ const sender = pc1.addTrack(track1);
+ const haveTrackEvent = new Promise(r => pc2.ontrack = r);
+ exchangeIceCandidates(pc1, pc2);
+ await exchangeOfferAnswer(pc1, pc2);
+ v.srcObject = new MediaStream([(await haveTrackEvent).track]);
+ await new Promise(r => v.onloadedmetadata = r);
+ // The basic signal is a track with signal 100. We replace this
+ // with tracks with signal from 0 to 255 and see if they are all
+ // reliably detected.
+ await detectSignal(t, v, 100);
+ // A few buffered frames are received with the old content, and a few
+ // frames may not have settled on exactly the right value. In testing,
+ // this test passes with maxFrames = 3; give a little more margin.
+ const maxFrames = 7;
+ // Test values 0 and 255
+ let maxCount = await signalSettlementTime(t, v, sender, 0, track1);
+ assert_less_than(maxCount, maxFrames,
+ 'Should get the black value within ' + maxFrames + ' frames');
+ maxCount = Math.max(
+ await signalSettlementTime(t, v, sender, 255, track1), maxCount);
+ assert_less_than(maxCount, maxFrames,
+ 'Should get the white value within ' + maxFrames + ' frames');
+ // Test a set of other values - far enough apart to make the test fast.
+ for (let signal = 2; signal <= 255; signal += 47) {
+ if (Math.abs(signal - 100) > 10) {
+ const count = await signalSettlementTime(t, v, sender, signal, track1);
+ maxCount = Math.max(count, maxCount);
+ assert_less_than(maxCount, 10,
+ 'Should get value ' + signal + ' within ' + maxFrames + ' frames');
+ }
+ }
+ assert_less_than(maxCount, 10, 'Should get the right value within 10 frames');
+}, 'Signal detector detects track change within reasonable time');
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnectionIceErrorEvent.html b/testing/web-platform/tests/webrtc/RTCPeerConnectionIceErrorEvent.html
new file mode 100644
index 0000000000..4434cfd28b
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnectionIceErrorEvent.html
@@ -0,0 +1,26 @@
+<!doctype html>
+<meta charset="utf-8">
+<html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+
+test(() => {
+ init = {
+ address: "168.3.4.5",
+ port: 4711,
+ url: "turn:turn.example.org",
+ errorCode: 703,
+ errorText: "Test error"
+ };
+ event = new RTCPeerConnectionIceErrorEvent('type', init);
+ assert_equals(event.type, 'type');
+ assert_equals(event.address, '168.3.4.5');
+ assert_equals(event.port, 4711);
+ assert_equals(event.url, "turn:turn.example.org");
+ assert_equals(event.errorCode, 703);
+ assert_equals(event.errorText, "Test error");
+}, 'RTCPeerConnectionIceErrorEvent constructed from init parameters');
+
+</script>
+</html>
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnectionIceEvent-constructor.html b/testing/web-platform/tests/webrtc/RTCPeerConnectionIceEvent-constructor.html
new file mode 100644
index 0000000000..447002dca1
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnectionIceEvent-constructor.html
@@ -0,0 +1,126 @@
+<!doctype html>
+<meta charset="utf-8">
+<!--
+4.8.2 RTCPeerConnectionIceEvent
+
+ The icecandidate event of the RTCPeerConnection uses the RTCPeerConnectionIceEvent interface.
+
+-->
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+/*
+RTCPeerConnectionIceEvent
+
+[Constructor(DOMString type, optional RTCPeerConnectionIceEventInit eventInitDict)]
+
+interface RTCPeerConnectionIceEvent : Event {
+ readonly attribute RTCIceCandidate? candidate;
+ readonly attribute DOMString? url;
+};
+ */
+test(() => {
+ assert_throws_js(TypeError, () => {
+ new RTCPeerConnectionIceEvent();
+ });
+}, "RTCPeerConnectionIceEvent with no arguments throws TypeError");
+
+test(() => {
+ const event = new RTCPeerConnectionIceEvent("type");
+ /*
+ candidate of type RTCIceCandidate, readonly, nullable
+ url of type DOMString, readonly, nullable
+ */
+ assert_equals(event.candidate, null);
+ assert_equals(event.url, null);
+
+ /*
+ Firing an RTCPeerConnectionIceEvent event named e with an RTCIceCandidate
+ candidate means that an event with the name e, which does not bubble
+ (except where otherwise stated) and is not cancelable
+ (except where otherwise stated),
+ */
+ assert_false(event.bubbles);
+ assert_false(event.cancelable);
+
+}, "RTCPeerConnectionIceEvent with no eventInitDict (default)");
+
+test(() => {
+ const event = new RTCPeerConnectionIceEvent("type", {});
+
+ /*
+ candidate of type RTCIceCandidate, readonly, nullable
+ url of type DOMString, readonly, nullable
+ */
+ assert_equals(event.candidate, null);
+ assert_equals(event.url, null);
+
+ /*
+ Firing an RTCPeerConnectionIceEvent event named e with an RTCIceCandidate
+ candidate means that an event with the name e, which does not bubble
+ (except where otherwise stated) and is not cancelable
+ (except where otherwise stated),
+ */
+ assert_false(event.bubbles);
+ assert_false(event.cancelable);
+
+}, "RTCPeerConnectionIceEvent with empty object as eventInitDict (default)");
+
+test(() => {
+ const event = new RTCPeerConnectionIceEvent("type", {
+ candidate: null
+ });
+ assert_equals(event.candidate, null);
+}, "RTCPeerConnectionIceEvent.candidate is null when constructed with { candidate: null }");
+
+test(() => {
+ const event = new RTCPeerConnectionIceEvent("type", {
+ candidate: undefined
+ });
+ assert_equals(event.candidate, null);
+}, "RTCPeerConnectionIceEvent.candidate is null when constructed with { candidate: undefined }");
+
+
+/*
+
+4.8.1 RTCIceCandidate Interface
+
+The RTCIceCandidate() constructor takes a dictionary argument, candidateInitDict,
+whose content is used to initialize the new RTCIceCandidate object. When run, if
+both the sdpMid and sdpMLineIndex dictionary members are null, throw a TypeError.
+*/
+const candidate = "";
+const sdpMid = "sdpMid";
+const sdpMLineIndex = 1;
+const usernameFragment = "";
+const url = "foo.bar";
+
+test(() => {
+ const iceCandidate = new RTCIceCandidate({ candidate, sdpMid, sdpMLineIndex, usernameFragment });
+ const event = new RTCPeerConnectionIceEvent("type", {
+ candidate: iceCandidate,
+ url,
+ });
+
+ assert_equals(event.candidate, iceCandidate);
+ assert_false(event.bubbles);
+ assert_false(event.cancelable);
+}, "RTCPeerConnectionIceEvent with RTCIceCandidate");
+
+test(() => {
+ const plain = { candidate, sdpMid, sdpMLineIndex, usernameFragment };
+ assert_throws_js(TypeError, () => new RTCPeerConnectionIceEvent("type", { candidate: plain }));
+}, "RTCPeerConnectionIceEvent with non RTCIceCandidate object throws");
+
+test(() => {
+ const event = new RTCPeerConnectionIceEvent("type", {
+ candidate: null,
+ bubbles: true,
+ cancelable: true,
+ });
+
+ assert_true(event.bubbles);
+ assert_true(event.cancelable);
+}, "RTCPeerConnectionIceEvent bubbles and cancelable");
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCRtpCapabilities-helper.js b/testing/web-platform/tests/webrtc/RTCRtpCapabilities-helper.js
new file mode 100644
index 0000000000..fb297c35fb
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCRtpCapabilities-helper.js
@@ -0,0 +1,52 @@
+'use strict'
+
+// Test is based on the following editor draft:
+// https://w3c.github.io/webrtc-pc/archives/20170605/webrtc.html
+
+// This file depends on dictionary-helper.js which should
+// be loaded from the main HTML file.
+
+/*
+ 5.2. RTCRtpSender Interface
+ dictionary RTCRtpCapabilities {
+ sequence<RTCRtpCodecCapability> codecs;
+ sequence<RTCRtpHeaderExtensionCapability> headerExtensions;
+ };
+
+ dictionary RTCRtpCodecCapability {
+ DOMString mimeType;
+ unsigned long clockRate;
+ unsigned short channels;
+ DOMString sdpFmtpLine;
+ };
+
+ dictionary RTCRtpHeaderExtensionCapability {
+ DOMString uri;
+ };
+ */
+
+function validateRtpCapabilities(capabilities) {
+ assert_array_field(capabilities, 'codecs');
+ for(const codec of capabilities.codecs) {
+ validateCodecCapability(codec);
+ }
+
+ assert_greater_than(capabilities.codecs.length, 0,
+ 'Expect at least one codec capability available');
+
+ assert_array_field(capabilities, 'headerExtensions');
+ for(const headerExt of capabilities.headerExtensions) {
+ validateHeaderExtensionCapability(headerExt);
+ }
+}
+
+function validateCodecCapability(codec) {
+ assert_optional_string_field(codec, 'mimeType');
+ assert_optional_unsigned_int_field(codec, 'clockRate');
+ assert_optional_unsigned_int_field(codec, 'channels');
+ assert_optional_string_field(codec, 'sdpFmtpLine');
+}
+
+function validateHeaderExtensionCapability(headerExt) {
+ assert_optional_string_field(headerExt, 'uri');
+}
diff --git a/testing/web-platform/tests/webrtc/RTCRtpParameters-codecs.html b/testing/web-platform/tests/webrtc/RTCRtpParameters-codecs.html
new file mode 100644
index 0000000000..f5fa65e2ac
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCRtpParameters-codecs.html
@@ -0,0 +1,206 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCRtpParameters codecs</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="dictionary-helper.js"></script>
+<script src="RTCRtpParameters-helper.js"></script>
+<script>
+ 'use strict';
+
+ // Test is based on the following editor draft:
+ // https://w3c.github.io/webrtc-pc/archives/20170605/webrtc.html
+
+ // The following helper functions are called from RTCRtpParameters-helper.js:
+ // doOfferAnswerExchange
+ // validateSenderRtpParameters
+
+ /*
+ 5.2. RTCRtpSender Interface
+ interface RTCRtpSender {
+ Promise<void> setParameters(optional RTCRtpParameters parameters);
+ RTCRtpParameters getParameters();
+ };
+
+ dictionary RTCRtpParameters {
+ DOMString transactionId;
+ sequence<RTCRtpEncodingParameters> encodings;
+ sequence<RTCRtpHeaderExtensionParameters> headerExtensions;
+ RTCRtcpParameters rtcp;
+ sequence<RTCRtpCodecParameters> codecs;
+ };
+
+ dictionary RTCRtpCodecParameters {
+ [readonly]
+ unsigned short payloadType;
+
+ [readonly]
+ DOMString mimeType;
+
+ [readonly]
+ unsigned long clockRate;
+
+ [readonly]
+ unsigned short channels;
+
+ [readonly]
+ DOMString sdpFmtpLine;
+ };
+
+ getParameters
+ - The codecs sequence is populated based on the codecs that have been negotiated
+ for sending, and which the user agent is currently capable of sending.
+
+ If setParameters has removed or reordered codecs, getParameters MUST return
+ the shortened/reordered list. However, every time codecs are renegotiated by
+ a new offer/answer exchange, the list of codecs MUST be restored to the full
+ negotiated set, in the priority order indicated by the remote description,
+ in effect discarding the effects of setParameters.
+
+ codecs
+ - When using the setParameters method, the codecs sequence from the corresponding
+ call to getParameters can be reordered and entries can be removed, but entries
+ cannot be added, and the RTCRtpCodecParameters dictionary members cannot be modified.
+ */
+
+ // Get the first codec from param.codecs.
+ // Assert that param.codecs has at least one element
+ function getFirstCodec(param) {
+ const { codecs } = param;
+ assert_greater_than(codecs.length, 0);
+ return codecs[0];
+ }
+
+ /*
+ 5.2. setParameters
+ 7. If parameters.encodings.length is different from N, or if any parameter
+ in the parameters argument, marked as a Read-only parameter, has a value
+ that is different from the corresponding parameter value returned from
+ sender.getParameters(), abort these steps and return a promise rejected
+ with a newly created InvalidModificationError. Note that this also applies
+ to transactionId.
+ */
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const { sender } = pc.addTransceiver('audio');
+ await doOfferAnswerExchange(t, pc);
+
+ const param = sender.getParameters();
+ validateSenderRtpParameters(param);
+
+ const codec = getFirstCodec(param);
+
+ if(codec.payloadType === undefined) {
+ codec.payloadType = 8;
+ } else {
+ codec.payloadType += 1;
+ }
+
+ return promise_rejects_dom(t, 'InvalidModificationError',
+ sender.setParameters(param));
+ }, 'setParameters() with codec.payloadType modified should reject with InvalidModificationError');
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const { sender } = pc.addTransceiver('audio');
+ await doOfferAnswerExchange(t, pc);
+ const param = sender.getParameters();
+ validateSenderRtpParameters(param);
+
+ const codec = getFirstCodec(param);
+
+ if(codec.mimeType === undefined) {
+ codec.mimeType = 'audio/piedpiper';
+ } else {
+ codec.mimeType = `${codec.mimeType}-modified`;
+ }
+
+ return promise_rejects_dom(t, 'InvalidModificationError',
+ sender.setParameters(param));
+ }, 'setParameters() with codec.mimeType modified should reject with InvalidModificationError');
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const { sender } = pc.addTransceiver('audio');
+ await doOfferAnswerExchange(t, pc);
+ const param = sender.getParameters();
+ validateSenderRtpParameters(param);
+
+ const codec = getFirstCodec(param);
+
+ if(codec.clockRate === undefined) {
+ codec.clockRate = 8000;
+ } else {
+ codec.clockRate += 1;
+ }
+
+ return promise_rejects_dom(t, 'InvalidModificationError',
+ sender.setParameters(param));
+ }, 'setParameters() with codec.clockRate modified should reject with InvalidModificationError');
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const { sender } = pc.addTransceiver('audio');
+ await doOfferAnswerExchange(t, pc);
+ const param = sender.getParameters();
+ validateSenderRtpParameters(param);
+
+ const codec = getFirstCodec(param);
+
+ if(codec.channels === undefined) {
+ codec.channels = 6;
+ } else {
+ codec.channels += 1;
+ }
+
+ return promise_rejects_dom(t, 'InvalidModificationError',
+ sender.setParameters(param));
+ }, 'setParameters() with codec.channels modified should reject with InvalidModificationError');
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const { sender } = pc.addTransceiver('audio');
+ await doOfferAnswerExchange(t, pc);
+ const param = sender.getParameters();
+ validateSenderRtpParameters(param);
+
+ const codec = getFirstCodec(param);
+
+ if(codec.sdpFmtpLine === undefined) {
+ codec.sdpFmtpLine = 'a=fmtp:98 0-15';
+ } else {
+ codec.sdpFmtpLine = `${codec.sdpFmtpLine};foo=1`;
+ }
+
+ return promise_rejects_dom(t, 'InvalidModificationError',
+ sender.setParameters(param));
+ }, 'setParameters() with codec.sdpFmtpLine modified should reject with InvalidModificationError');
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const { sender } = pc.addTransceiver('audio');
+ await doOfferAnswerExchange(t, pc);
+ const param = sender.getParameters();
+ validateSenderRtpParameters(param);
+
+ const { codecs } = param;
+
+ codecs.push({
+ payloadType: 2,
+ mimeType: 'audio/piedpiper',
+ clockRate: 1000,
+ channels: 2
+ });
+
+ return promise_rejects_dom(t, 'InvalidModificationError',
+ sender.setParameters(param));
+ }, 'setParameters() with new codecs inserted should reject with InvalidModificationError');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCRtpParameters-encodings.html b/testing/web-platform/tests/webrtc/RTCRtpParameters-encodings.html
new file mode 100644
index 0000000000..22abbb3718
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCRtpParameters-encodings.html
@@ -0,0 +1,543 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCRtpParameters encodings</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="dictionary-helper.js"></script>
+<script src="RTCRtpParameters-helper.js"></script>
+<script>
+ 'use strict';
+
+ // Test is based on the following editor draft:
+ // https://w3c.github.io/webrtc-pc/archives/20170605/webrtc.html
+
+ // The following helper functions are called from RTCRtpParameters-helper.js:
+ // validateSenderRtpParameters
+
+ /*
+ 5.1. RTCPeerConnection Interface Extensions
+ partial interface RTCPeerConnection {
+ RTCRtpTransceiver addTransceiver((MediaStreamTrack or DOMString) trackOrKind,
+ optional RTCRtpTransceiverInit init);
+ ...
+ };
+
+ dictionary RTCRtpTransceiverInit {
+ RTCRtpTransceiverDirection direction = "sendrecv";
+ sequence<MediaStream> streams;
+ sequence<RTCRtpEncodingParameters> sendEncodings;
+ };
+
+ 5.2. RTCRtpSender Interface
+ interface RTCRtpSender {
+ Promise<void> setParameters(optional RTCRtpParameters parameters);
+ RTCRtpParameters getParameters();
+ };
+
+ dictionary RTCRtpParameters {
+ DOMString transactionId;
+ sequence<RTCRtpEncodingParameters> encodings;
+ sequence<RTCRtpHeaderExtensionParameters> headerExtensions;
+ RTCRtcpParameters rtcp;
+ sequence<RTCRtpCodecParameters> codecs;
+ };
+
+ dictionary RTCRtpEncodingParameters {
+ boolean active;
+ unsigned long maxBitrate;
+
+ [readonly]
+ DOMString rid;
+
+ double scaleResolutionDownBy;
+ };
+
+ getParameters
+ - encodings is set to the value of the [[send encodings]] internal slot.
+ */
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const transceiver = pc.addTransceiver('video');
+
+ const param = transceiver.sender.getParameters();
+ assert_equals(param.encodings.length, 1);
+ // Do not call this in every test; it does not make sense to disable all of
+ // the tests below for an implementation that is missing support for
+ // fields that are not related to the test.
+ validateSenderRtpParameters(param);
+ }, `getParameters should return RTCRtpEncodingParameters with all required fields`);
+
+ /*
+ 5.1. addTransceiver
+ 7. Create an RTCRtpSender with track, streams and sendEncodings and let sender
+ be the result.
+
+ 5.2. create an RTCRtpSender
+ 5. Let sender have a [[send encodings]] internal slot, representing a list
+ of RTCRtpEncodingParameters dictionaries.
+ 6. If sendEncodings is given as input to this algorithm, and is non-empty,
+ set the [[send encodings]] slot to sendEncodings.
+
+ Otherwise, set it to a list containing a single RTCRtpEncodingParameters
+ with active set to true.
+ */
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const transceiver = pc.addTransceiver('audio');
+
+ const param = transceiver.sender.getParameters();
+ const { encodings } = param;
+ assert_equals(encodings.length, 1);
+ const encoding = param.encodings[0];
+
+ assert_equals(encoding.active, true);
+ assert_not_own_property(encoding, "maxBitrate");
+ assert_not_own_property(encoding, "rid");
+ assert_not_own_property(encoding, "scaleResolutionDownBy");
+ // We do not check props from extension specifications here; those checks
+ // need to go in a test-case for that extension specification.
+ }, 'addTransceiver(audio) with undefined sendEncodings should have default encoding parameter with active set to true');
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const transceiver = pc.addTransceiver('video');
+
+ const param = transceiver.sender.getParameters();
+ const { encodings } = param;
+ assert_equals(encodings.length, 1);
+ const encoding = param.encodings[0];
+
+ assert_equals(encoding.active, true);
+ // spec says to return an encoding without a scaleResolutionDownBy value
+ // when addTransceiver does not pass any encodings, however spec also says
+ // to throw if setParameters is missing a scaleResolutionDownBy. One of
+ // these two requirements needs to be removed, but it is unclear right now
+ // which will be removed. For now, allow scaleResolutionDownBy, but don't
+ // require it.
+ // https://github.com/w3c/webrtc-pc/issues/2730
+ assert_not_own_property(encoding, "maxBitrate");
+ assert_not_own_property(encoding, "rid");
+ assert_equals(encoding.scaleResolutionDownBy, 1.0);
+ // We do not check props from extension specifications here; those checks
+ // need to go in a test-case for that extension specification.
+ }, 'addTransceiver(video) with undefined sendEncodings should have default encoding parameter with active set to true and scaleResolutionDownBy set to 1');
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const transceiver = pc.addTransceiver('audio', { sendEncodings: [] });
+
+ const param = transceiver.sender.getParameters();
+ const { encodings } = param;
+ assert_equals(encodings.length, 1);
+ const encoding = param.encodings[0];
+
+ assert_equals(encoding.active, true);
+ assert_not_own_property(encoding, "maxBitrate");
+ assert_not_own_property(encoding, "rid");
+ assert_not_own_property(encoding, "scaleResolutionDownBy");
+ // We do not check props from extension specifications here; those checks
+ // need to go in a test-case for that extension specification.
+ }, 'addTransceiver(audio) with empty list sendEncodings should have default encoding parameter with active set to true');
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const transceiver = pc.addTransceiver('video', { sendEncodings: [] });
+
+ const param = transceiver.sender.getParameters();
+ const { encodings } = param;
+ assert_equals(encodings.length, 1);
+ const encoding = param.encodings[0];
+
+ assert_equals(encoding.active, true);
+ assert_not_own_property(encoding, "maxBitrate");
+ assert_not_own_property(encoding, "rid");
+ assert_equals(encoding.scaleResolutionDownBy, 1.0);
+ // We do not check props from extension specifications here; those checks
+ // need to go in a test-case for that extension specification.
+ }, 'addTransceiver(video) with empty list sendEncodings should have default encoding parameter with active set to true and scaleResolutionDownBy set to 1');
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const transceiver = pc.addTransceiver('video', {sendEncodings: [{rid: "foo"}, {rid: "bar", scaleResolutionDownBy: 3.0}]});
+
+ const param = transceiver.sender.getParameters();
+ const { encodings } = param;
+ assert_equals(encodings.length, 2);
+ assert_equals(encodings[0].scaleResolutionDownBy, 1.0);
+ assert_equals(encodings[1].scaleResolutionDownBy, 3.0);
+ }, `addTransceiver(video) should auto-set scaleResolutionDownBy to 1 when some encodings have it, but not all`);
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const transceiver = pc.addTransceiver('video', {sendEncodings: [{rid: "foo"}, {rid: "bar"}]});
+
+ const param = transceiver.sender.getParameters();
+ const { encodings } = param;
+ assert_equals(encodings.length, 2);
+ assert_equals(encodings[0].scaleResolutionDownBy, 2.0);
+ assert_equals(encodings[1].scaleResolutionDownBy, 1.0);
+ }, `addTransceiver should auto-set scaleResolutionDownBy to powers of 2 (descending) when absent`);
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const sendEncodings = [];
+ for (let i = 0; i < 1000; i++) {
+ sendEncodings.push({rid: i});
+ }
+ const transceiver = pc.addTransceiver('video', {sendEncodings});
+
+ const param = transceiver.sender.getParameters();
+ const { encodings } = param;
+ assert_less_than(encodings.length, 1000, `1000 encodings is clearly too many`);
+ }, `addTransceiver with a ridiculous number of encodings should truncate the list`);
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const transceiver = pc.addTransceiver('audio', {sendEncodings: [{rid: "foo"}, {rid: "bar"}]});
+
+ const param = transceiver.sender.getParameters();
+ const { encodings } = param;
+ assert_equals(encodings.length, 1);
+ assert_not_own_property(encodings[0], "maxBitrate");
+ assert_not_own_property(encodings[0], "rid");
+ assert_not_own_property(encodings[0], "scaleResolutionDownBy");
+ // We do not check props from extension specifications here; those checks
+ // need to go in a test-case for that extension specification.
+ }, `addTransceiver(audio) with multiple encodings should result in one encoding with no properties other than active`);
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const {sender} = pc.addTransceiver('audio', {sendEncodings: [{rid: "foo", scaleResolutionDownBy: 2.0}]});
+ const {encodings} = sender.getParameters();
+ assert_equals(encodings.length, 1);
+ assert_not_own_property(encodings[0], "scaleResolutionDownBy");
+ }, `addTransceiver(audio) should remove valid scaleResolutionDownBy`);
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const {sender} = pc.addTransceiver('audio', {sendEncodings: [{rid: "foo", scaleResolutionDownBy: -1.0}]});
+ const {encodings} = sender.getParameters();
+ assert_equals(encodings.length, 1);
+ assert_not_own_property(encodings[0], "scaleResolutionDownBy");
+ }, `addTransceiver(audio) should remove invalid scaleResolutionDownBy`);
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const {sender} = pc.addTransceiver('audio');
+ let params = sender.getParameters();
+ assert_equals(params.encodings.length, 1);
+ params.encodings[0].scaleResolutionDownBy = 2;
+ await sender.setParameters(params);
+ const {encodings} = sender.getParameters();
+ assert_equals(encodings.length, 1);
+ assert_not_own_property(encodings[0], "scaleResolutionDownBy");
+ }, `setParameters with scaleResolutionDownBy on an audio sender should succeed, but remove the scaleResolutionDownBy`);
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const {sender} = pc.addTransceiver('audio');
+ let params = sender.getParameters();
+ assert_equals(params.encodings.length, 1);
+ params.encodings[0].scaleResolutionDownBy = -1;
+ await sender.setParameters(params);
+ const {encodings} = sender.getParameters();
+ assert_equals(encodings.length, 1);
+ assert_not_own_property(encodings[0], "scaleResolutionDownBy");
+ }, `setParameters with an invalid scaleResolutionDownBy on an audio sender should succeed, but remove the scaleResolutionDownBy`);
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ assert_throws_js(TypeError, () => pc.addTransceiver('video', { sendEncodings: [{rid: "foo"}, {rid: "foo"}] }));
+ }, 'addTransceiver with duplicate rid and multiple encodings throws TypeError');
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ assert_throws_js(TypeError, () => pc.addTransceiver('video', { sendEncodings: [{rid: "foo"}, {}] }));
+ }, 'addTransceiver with missing rid and multiple encodings throws TypeError');
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ assert_throws_js(TypeError, () => pc.addTransceiver('video', { sendEncodings: [{rid: ""}] }));
+ }, 'addTransceiver with empty rid throws TypeError');
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ assert_throws_js(TypeError, () => pc.addTransceiver('video', { sendEncodings: [{rid: "!?"}] }));
+ assert_throws_js(TypeError, () => pc.addTransceiver('video', { sendEncodings: [{rid: "(â•Ŋ°□°)â•Ŋïļĩ â”ŧ━â”ŧ"}] }));
+ // RFC 8851 says '-' and '_' are allowed, but RFC 8852 says they are not.
+ // RFC 8852 needs to be adhered to, otherwise we can't put the rid in RTP
+ // https://github.com/w3c/webrtc-pc/issues/2732
+ // https://www.rfc-editor.org/errata/eid7132
+ assert_throws_js(TypeError, () => pc.addTransceiver('video', { sendEncodings: [{rid: "foo-bar"}] }));
+ assert_throws_js(TypeError, () => pc.addTransceiver('video', { sendEncodings: [{rid: "foo_bar"}] }));
+ }, 'addTransceiver with invalid rid characters throws TypeError');
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ // https://github.com/w3c/webrtc-pc/issues/2732
+ assert_throws_js(TypeError, () => pc.addTransceiver('video', { sendEncodings: [{rid: 'a'.repeat(256)}] }));
+ }, 'addTransceiver with rid longer than 255 characters throws TypeError');
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ assert_throws_js(RangeError, () => pc.addTransceiver('video', { sendEncodings: [{scaleResolutionDownBy: -1}] }));
+ assert_throws_js(RangeError, () => pc.addTransceiver('video', { sendEncodings: [{scaleResolutionDownBy: 0}] }));
+ assert_throws_js(RangeError, () => pc.addTransceiver('video', { sendEncodings: [{scaleResolutionDownBy: 0.5}] }));
+ }, `addTransceiver with scaleResolutionDownBy < 1 throws RangeError`);
+
+ /*
+ 5.2. create an RTCRtpSender
+ To create an RTCRtpSender with a MediaStreamTrack , track, a list of MediaStream
+ objects, streams, and optionally a list of RTCRtpEncodingParameters objects,
+ sendEncodings, run the following steps:
+ 5. Let sender have a [[send encodings]] internal slot, representing a list
+ of RTCRtpEncodingParameters dictionaries.
+
+ 6. If sendEncodings is given as input to this algorithm, and is non-empty,
+ set the [[send encodings]] slot to sendEncodings.
+
+ 5.2. getParameters
+ - encodings is set to the value of the [[send encodings]] internal slot.
+ */
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const { sender } = pc.addTransceiver('video', {
+ sendEncodings: [{
+ active: false,
+ maxBitrate: 8,
+ rid: 'foo'
+ }]
+ });
+
+ const param = sender.getParameters();
+ const encoding = param.encodings[0];
+
+ assert_equals(encoding.active, false);
+ assert_equals(encoding.maxBitrate, 8);
+ assert_not_own_property(encoding, "rid", "rid should be removed with a single encoding");
+
+ }, `sender.getParameters() should return sendEncodings set by addTransceiver()`);
+
+ /*
+ 5.2. setParameters
+ 3. Let N be the number of RTCRtpEncodingParameters stored in sender's internal
+ [[send encodings]] slot.
+ 7. If parameters.encodings.length is different from N, or if any parameter
+ in the parameters argument, marked as a Read-only parameter, has a value
+ that is different from the corresponding parameter value returned from
+ sender.getParameters(), abort these steps and return a promise rejected
+ with a newly created InvalidModificationError. Note that this also applies
+ to transactionId.
+ */
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const { sender } = pc.addTransceiver('video');
+
+ const param = sender.getParameters();
+
+ const { encodings } = param;
+ assert_equals(encodings.length, 1);
+
+ // While {} is valid RTCRtpEncodingParameters because all fields are
+ // optional, it is still invalid to be missing a rid when there are multiple
+ // encodings. Only trigger one kind of error here.
+ encodings.push({ rid: "foo" });
+ assert_equals(param.encodings.length, 2);
+
+ return promise_rejects_dom(t, 'InvalidModificationError',
+ sender.setParameters(param));
+ }, `sender.setParameters() with added encodings should reject with InvalidModificationError`);
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const { sender } = pc.addTransceiver('video', {sendEncodings: [{rid: "foo"}, {rid: "bar"}]});
+
+ const param = sender.getParameters();
+
+ const { encodings } = param;
+ assert_equals(encodings.length, 2);
+
+ encodings.pop();
+ assert_equals(param.encodings.length, 1);
+
+ return promise_rejects_dom(t, 'InvalidModificationError',
+ sender.setParameters(param));
+ }, `sender.setParameters() with removed encodings should reject with InvalidModificationError`);
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const { sender } = pc.addTransceiver('video', {sendEncodings: [{rid: "foo"}, {rid: "bar"}]});
+
+ const param = sender.getParameters();
+
+ const { encodings } = param;
+ assert_equals(encodings.length, 2);
+ encodings.push(encodings.shift());
+ assert_equals(param.encodings.length, 2);
+
+ return promise_rejects_dom(t, 'InvalidModificationError',
+ sender.setParameters(param));
+ }, `sender.setParameters() with reordered encodings should reject with InvalidModificationError`);
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const { sender } = pc.addTransceiver('video');
+
+ const param = sender.getParameters();
+
+ delete param.encodings;
+
+ return promise_rejects_js(t, TypeError,
+ sender.setParameters(param));
+ }, `sender.setParameters() with encodings unset should reject with TypeError`);
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const { sender } = pc.addTransceiver('video');
+
+ const param = sender.getParameters();
+
+ param.encodings = [];
+
+ return promise_rejects_dom(t, 'InvalidModificationError',
+ sender.setParameters(param));
+ }, `sender.setParameters() with empty encodings should reject with InvalidModificationError (video)`);
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const { sender } = pc.addTransceiver('audio');
+
+ const param = sender.getParameters();
+
+ param.encodings = [];
+
+ return promise_rejects_dom(t, 'InvalidModificationError',
+ sender.setParameters(param));
+ }, `sender.setParameters() with empty encodings should reject with InvalidModificationError (audio)`);
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const { sender } = pc.addTransceiver('video', {
+ sendEncodings: [{ rid: 'foo' }, { rid: 'baz' }],
+ });
+
+ const param = sender.getParameters();
+ const encoding = param.encodings[0];
+
+ assert_equals(encoding.rid, 'foo');
+
+ encoding.rid = 'bar';
+ return promise_rejects_dom(t, 'InvalidModificationError',
+ sender.setParameters(param));
+ }, `setParameters() with modified encoding.rid field should reject with InvalidModificationError`);
+
+ /*
+ 5.2. setParameters
+ 8. If the scaleResolutionDownBy parameter in the parameters argument has a
+ value less than 1.0, abort these steps and return a promise rejected with
+ a newly created RangeError.
+ */
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const { sender } = pc.addTransceiver('video');
+
+ const param = sender.getParameters();
+ const encoding = param.encodings[0];
+
+ encoding.scaleResolutionDownBy = 0.5;
+ await promise_rejects_js(t, RangeError, sender.setParameters(param));
+ encoding.scaleResolutionDownBy = 0;
+ await promise_rejects_js(t, RangeError, sender.setParameters(param));
+ encoding.scaleResolutionDownBy = -1;
+ await promise_rejects_js(t, RangeError, sender.setParameters(param));
+ }, `setParameters() with encoding.scaleResolutionDownBy field set to less than 1.0 should reject with RangeError`);
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const { sender } = pc.addTransceiver('video');
+
+ let param = sender.getParameters();
+ const encoding = param.encodings[0];
+
+ delete encoding.scaleResolutionDownBy;
+ await sender.setParameters(param);
+ param = sender.getParameters();
+ assert_equals(param.encodings[0].scaleResolutionDownBy, 1.0);
+ }, `setParameters() with missing encoding.scaleResolutionDownBy field should succeed, and set the value back to 1`);
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const { sender } = pc.addTransceiver('video');
+
+ const param = sender.getParameters();
+ const encoding = param.encodings[0];
+
+ encoding.scaleResolutionDownBy = 1.5;
+ return sender.setParameters(param)
+ .then(() => {
+ const param = sender.getParameters();
+ const encoding = param.encodings[0];
+
+ assert_approx_equals(encoding.scaleResolutionDownBy, 1.5, 0.01);
+ });
+ }, `setParameters() with encoding.scaleResolutionDownBy field set to greater than 1.0 should succeed`);
+
+ test_modified_encoding('video', 'active', false, true,
+ 'setParameters() with encoding.active false->true should succeed (video)');
+
+ test_modified_encoding('video', 'active', true, false,
+ 'setParameters() with encoding.active true->false should succeed (video)');
+
+ test_modified_encoding('video', 'maxBitrate', 10000, 20000,
+ 'setParameters() with modified encoding.maxBitrate should succeed (video)');
+
+ test_modified_encoding('audio', 'active', false, true,
+ 'setParameters() with encoding.active false->true should succeed (audio)');
+
+ test_modified_encoding('audio', 'active', true, false,
+ 'setParameters() with encoding.active true->false should succeed (audio)');
+
+ test_modified_encoding('audio', 'maxBitrate', 10000, 20000,
+ 'setParameters() with modified encoding.maxBitrate should succeed (audio)');
+
+ test_modified_encoding('video', 'scaleResolutionDownBy', 2, 4,
+ 'setParameters() with modified encoding.scaleResolutionDownBy should succeed');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCRtpParameters-headerExtensions.html b/testing/web-platform/tests/webrtc/RTCRtpParameters-headerExtensions.html
new file mode 100644
index 0000000000..7de2b75f4e
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCRtpParameters-headerExtensions.html
@@ -0,0 +1,74 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCRtpParameters headerExtensions</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="dictionary-helper.js"></script>
+<script src="RTCRtpParameters-helper.js"></script>
+<script>
+ 'use strict';
+
+ // Test is based on the following editor draft:
+ // https://w3c.github.io/webrtc-pc/archives/20170605/webrtc.html
+
+ // The following helper functions are called from RTCRtpParameters-helper.js:
+ // validateSenderRtpParameters
+
+ /*
+ 5.2. RTCRtpSender Interface
+ interface RTCRtpSender {
+ Promise<void> setParameters(optional RTCRtpParameters parameters);
+ RTCRtpParameters getParameters();
+ };
+
+ dictionary RTCRtpParameters {
+ DOMString transactionId;
+ sequence<RTCRtpEncodingParameters> encodings;
+ sequence<RTCRtpHeaderExtensionParameters> headerExtensions;
+ RTCRtcpParameters rtcp;
+ sequence<RTCRtpCodecParameters> codecs;
+ };
+
+ dictionary RTCRtpHeaderExtensionParameters {
+ [readonly]
+ DOMString uri;
+
+ [readonly]
+ unsigned short id;
+
+ [readonly]
+ boolean encrypted;
+ };
+
+ getParameters
+ - The headerExtensions sequence is populated based on the header extensions
+ that have been negotiated for sending.
+ */
+
+ /*
+ 5.2. setParameters
+ 7. If parameters.encodings.length is different from N, or if any parameter
+ in the parameters argument, marked as a Read-only parameter, has a value
+ that is different from the corresponding parameter value returned from
+ sender.getParameters(), abort these steps and return a promise rejected
+ with a newly created InvalidModificationError. Note that this also applies
+ to transactionId.
+ */
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const { sender } = pc.addTransceiver('audio');
+ const param = sender.getParameters();
+ validateSenderRtpParameters(param);
+
+ param.headerExtensions = [{
+ uri: 'non-existent.example.org',
+ id: 404,
+ encrypted: false
+ }];
+
+ return promise_rejects_dom(t, 'InvalidModificationError',
+ sender.setParameters(param));
+ }, `setParameters() with modified headerExtensions should reject with InvalidModificationError`);
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCRtpParameters-helper.js b/testing/web-platform/tests/webrtc/RTCRtpParameters-helper.js
new file mode 100644
index 0000000000..dd8ae0cc06
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCRtpParameters-helper.js
@@ -0,0 +1,259 @@
+'use strict';
+
+// Test is based on the following editor draft:
+// https://w3c.github.io/webrtc-pc/archives/20170605/webrtc.html
+
+// Helper function for testing RTCRtpParameters dictionary fields
+
+// This file depends on dictionary-helper.js which should
+// be loaded from the main HTML file.
+
+// An offer/answer exchange is necessary for getParameters() to have any
+// negotiated parameters to return.
+async function doOfferAnswerExchange(t, caller) {
+ const callee = new RTCPeerConnection();
+ t.add_cleanup(() => callee.close());
+ const offer = await caller.createOffer();
+ await caller.setLocalDescription(offer);
+ await callee.setRemoteDescription(offer);
+ const answer = await callee.createAnswer();
+ await callee.setLocalDescription(answer);
+ await caller.setRemoteDescription(answer);
+
+ return callee;
+}
+
+/*
+ Validates the RTCRtpParameters returned from RTCRtpSender.prototype.getParameters
+
+ 5.2. RTCRtpSender Interface
+ getParameters
+ - transactionId is set to a new unique identifier, used to match this getParameters
+ call to a setParameters call that may occur later.
+
+ - encodings is set to the value of the [[SendEncodings]] internal slot.
+
+ - The headerExtensions sequence is populated based on the header extensions that
+ have been negotiated for sending.
+
+ - The codecs sequence is populated based on the codecs that have been negotiated
+ for sending, and which the user agent is currently capable of sending. If
+ setParameters has removed or reordered codecs, getParameters MUST return the
+ shortened/reordered list. However, every time codecs are renegotiated by a
+ new offer/answer exchange, the list of codecs MUST be restored to the full
+ negotiated set, in the priority order indicated by the remote description,
+ in effect discarding the effects of setParameters.
+
+ - rtcp.cname is set to the CNAME of the associated RTCPeerConnection. rtcp.reducedSize
+ is set to true if reduced-size RTCP has been negotiated for sending, and false otherwise.
+ */
+function validateSenderRtpParameters(param) {
+ validateRtpParameters(param);
+
+ assert_array_field(param, 'encodings');
+ for(const encoding of param.encodings) {
+ validateEncodingParameters(encoding);
+ }
+
+ assert_not_equals(param.transactionId, undefined,
+ 'Expect sender param.transactionId to be set');
+
+ assert_not_equals(param.rtcp.cname, undefined,
+ 'Expect sender param.rtcp.cname to be set');
+
+ assert_not_equals(param.rtcp.reducedSize, undefined,
+ 'Expect sender param.rtcp.reducedSize to be set to either true or false');
+}
+
+/*
+ Validates the RTCRtpParameters returned from RTCRtpReceiver.prototype.getParameters
+
+ 5.3. RTCRtpReceiver Interface
+ getParameters
+ When getParameters is called, the RTCRtpParameters dictionary is constructed
+ as follows:
+
+ - The headerExtensions sequence is populated based on the header extensions that
+ the receiver is currently prepared to receive.
+
+ - The codecs sequence is populated based on the codecs that the receiver is currently
+ prepared to receive.
+
+ - rtcp.reducedSize is set to true if the receiver is currently prepared to receive
+ reduced-size RTCP packets, and false otherwise. rtcp.cname is left undefined.
+
+ - transactionId is left undefined.
+ */
+function validateReceiverRtpParameters(param) {
+ validateRtpParameters(param);
+
+ assert_equals(param.transactionId, undefined,
+ 'Expect receiver param.transactionId to be unset');
+
+ assert_not_equals(param.rtcp.reducedSize, undefined,
+ 'Expect receiver param.rtcp.reducedSize to be set');
+
+ assert_equals(param.rtcp.cname, undefined,
+ 'Expect receiver param.rtcp.cname to be unset');
+}
+
+/*
+ dictionary RTCRtpParameters {
+ DOMString transactionId;
+ sequence<RTCRtpEncodingParameters> encodings;
+ sequence<RTCRtpHeaderExtensionParameters> headerExtensions;
+ RTCRtcpParameters rtcp;
+ sequence<RTCRtpCodecParameters> codecs;
+ };
+
+ */
+function validateRtpParameters(param) {
+ assert_optional_string_field(param, 'transactionId');
+
+ assert_array_field(param, 'headerExtensions');
+ for(const headerExt of param.headerExtensions) {
+ validateHeaderExtensionParameters(headerExt);
+ }
+
+ assert_dict_field(param, 'rtcp');
+ validateRtcpParameters(param.rtcp);
+
+ assert_array_field(param, 'codecs');
+ for(const codec of param.codecs) {
+ validateCodecParameters(codec);
+ }
+}
+
+/*
+ dictionary RTCRtpEncodingParameters {
+ boolean active;
+ unsigned long maxBitrate;
+
+ [readonly]
+ DOMString rid;
+
+ double scaleResolutionDownBy;
+ };
+
+ */
+function validateEncodingParameters(encoding) {
+ assert_optional_boolean_field(encoding, 'active');
+ assert_optional_unsigned_int_field(encoding, 'maxBitrate');
+
+ assert_optional_string_field(encoding, 'rid');
+ assert_optional_number_field(encoding, 'scaleResolutionDownBy');
+}
+
+/*
+ dictionary RTCRtcpParameters {
+ [readonly]
+ DOMString cname;
+
+ [readonly]
+ boolean reducedSize;
+ };
+ */
+function validateRtcpParameters(rtcp) {
+ assert_optional_string_field(rtcp, 'cname');
+ assert_optional_boolean_field(rtcp, 'reducedSize');
+}
+
+/*
+ dictionary RTCRtpHeaderExtensionParameters {
+ [readonly]
+ DOMString uri;
+
+ [readonly]
+ unsigned short id;
+
+ [readonly]
+ boolean encrypted;
+ };
+ */
+function validateHeaderExtensionParameters(headerExt) {
+ assert_optional_string_field(headerExt, 'uri');
+ assert_optional_unsigned_int_field(headerExt, 'id');
+ assert_optional_boolean_field(headerExt, 'encrypted');
+}
+
+/*
+ dictionary RTCRtpCodecParameters {
+ [readonly]
+ unsigned short payloadType;
+
+ [readonly]
+ DOMString mimeType;
+
+ [readonly]
+ unsigned long clockRate;
+
+ [readonly]
+ unsigned short channels;
+
+ [readonly]
+ DOMString sdpFmtpLine;
+ };
+ */
+function validateCodecParameters(codec) {
+ assert_optional_unsigned_int_field(codec, 'payloadType');
+ assert_optional_string_field(codec, 'mimeType');
+ assert_optional_unsigned_int_field(codec, 'clockRate');
+ assert_optional_unsigned_int_field(codec, 'channels');
+ assert_optional_string_field(codec, 'sdpFmtpLine');
+}
+
+// Helper function to test that modifying an encoding field should succeed
+function test_modified_encoding(kind, field, value1, value2, desc) {
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const {
+ sender
+ } = pc.addTransceiver(kind, {
+ sendEncodings: [{
+ [field]: value1
+ }]
+ });
+ await doOfferAnswerExchange(t, pc);
+
+ const param1 = sender.getParameters();
+ validateSenderRtpParameters(param1);
+ const encoding1 = param1.encodings[0];
+
+ assert_equals(encoding1[field], value1);
+ encoding1[field] = value2;
+
+ await sender.setParameters(param1);
+ const param2 = sender.getParameters();
+ validateSenderRtpParameters(param2);
+ const encoding2 = param2.encodings[0];
+ assert_equals(encoding2[field], value2);
+ }, desc + ' with RTCRtpTransceiverInit');
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const {
+ sender
+ } = pc.addTransceiver(kind);
+ await doOfferAnswerExchange(t, pc);
+
+ const initParam = sender.getParameters();
+ validateSenderRtpParameters(initParam);
+ initParam.encodings[0][field] = value1;
+ await sender.setParameters(initParam);
+
+ const param1 = sender.getParameters();
+ validateSenderRtpParameters(param1);
+ const encoding1 = param1.encodings[0];
+
+ assert_equals(encoding1[field], value1);
+ encoding1[field] = value2;
+
+ await sender.setParameters(param1);
+ const param2 = sender.getParameters();
+ validateSenderRtpParameters(param2);
+ const encoding2 = param2.encodings[0];
+ assert_equals(encoding2[field], value2);
+ }, desc + ' without RTCRtpTransceiverInit');
+}
diff --git a/testing/web-platform/tests/webrtc/RTCRtpParameters-rtcp.html b/testing/web-platform/tests/webrtc/RTCRtpParameters-rtcp.html
new file mode 100644
index 0000000000..7965304520
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCRtpParameters-rtcp.html
@@ -0,0 +1,104 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCRtpParameters rtcp</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="dictionary-helper.js"></script>
+<script src="RTCRtpParameters-helper.js"></script>
+<script>
+ 'use strict';
+
+ // Test is based on the following editor draft:
+ // https://w3c.github.io/webrtc-pc/archives/20170605/webrtc.html
+
+ // The following helper functions are called from RTCRtpParameters-helper.js:
+ // validateSenderRtpParameters
+
+ /*
+ 5.2. RTCRtpSender Interface
+ interface RTCRtpSender {
+ Promise<void> setParameters(optional RTCRtpParameters parameters);
+ RTCRtpParameters getParameters();
+ };
+
+ dictionary RTCRtpParameters {
+ DOMString transactionId;
+ sequence<RTCRtpEncodingParameters> encodings;
+ sequence<RTCRtpHeaderExtensionParameters> headerExtensions;
+ RTCRtcpParameters rtcp;
+ sequence<RTCRtpCodecParameters> codecs;
+ };
+
+ dictionary RTCRtcpParameters {
+ [readonly]
+ DOMString cname;
+
+ [readonly]
+ boolean reducedSize;
+ };
+
+ getParameters
+ - rtcp.cname is set to the CNAME of the associated RTCPeerConnection.
+
+ rtcp.reducedSize is set to true if reduced-size RTCP has been negotiated for
+ sending, and false otherwise.
+ */
+
+ /*
+ 5.2. setParameters
+ 7. If parameters.encodings.length is different from N, or if any parameter
+ in the parameters argument, marked as a Read-only parameter, has a value
+ that is different from the corresponding parameter value returned from
+ sender.getParameters(), abort these steps and return a promise rejected
+ with a newly created InvalidModificationError. Note that this also applies
+ to transactionId.
+ */
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const { sender } = pc.addTransceiver('audio');
+
+ const param = sender.getParameters();
+ validateSenderRtpParameters(param);
+
+ const { rtcp } = param;
+
+ if(rtcp === undefined) {
+ param.rtcp = { cname: 'foo' };
+
+ } else if(rtcp.cname === undefined) {
+ rtcp.cname = 'foo';
+
+ } else {
+ rtcp.cname = `${rtcp.cname}-modified`;
+ }
+
+ return promise_rejects_dom(t, 'InvalidModificationError',
+ sender.setParameters(param));
+ }, `setParameters() with modified rtcp.cname should reject with InvalidModificationError`);
+
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const { sender } = pc.addTransceiver('audio');
+
+ const param = sender.getParameters();
+ validateSenderRtpParameters(param);
+
+ const { rtcp } = param;
+
+ if(rtcp === undefined) {
+ param.rtcp = { reducedSize: true };
+
+ } else if(rtcp.reducedSize === undefined) {
+ rtcp.reducedSize = true;
+
+ } else {
+ rtcp.reducedSize = !rtcp.reducedSize;
+ }
+
+ return promise_rejects_dom(t, 'InvalidModificationError',
+ sender.setParameters(param));
+ }, `setParameters() with modified rtcp.reducedSize should reject with InvalidModificationError`);
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCRtpParameters-transactionId.html b/testing/web-platform/tests/webrtc/RTCRtpParameters-transactionId.html
new file mode 100644
index 0000000000..a0fa0fab25
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCRtpParameters-transactionId.html
@@ -0,0 +1,190 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCRtpParameters transactionId</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="dictionary-helper.js"></script>
+<script src="RTCRtpParameters-helper.js"></script>
+<script>
+ 'use strict';
+
+ // Test is based on the following editor draft:
+ // https://w3c.github.io/webrtc-pc/archives/20170605/webrtc.html
+
+ /*
+ 5.1. RTCPeerConnection Interface Extensions
+ partial interface RTCPeerConnection {
+ RTCRtpTransceiver addTransceiver((MediaStreamTrack or DOMString) trackOrKind,
+ optional RTCRtpTransceiverInit init);
+ ...
+ };
+
+ dictionary RTCRtpTransceiverInit {
+ RTCRtpTransceiverDirection direction = "sendrecv";
+ sequence<MediaStream> streams;
+ sequence<RTCRtpEncodingParameters> sendEncodings;
+ };
+
+ addTransceiver
+ 2. If the dictionary argument is present, and it has a sendEncodings member,
+ let sendEncodings be that list of RTCRtpEncodingParameters objects, or an
+ empty list otherwise.
+ 7. Create an RTCRtpSender with track, streams and sendEncodings and let
+ sender be the result.
+
+ 5.2. RTCRtpSender Interface
+ interface RTCRtpSender {
+ Promise<void> setParameters(optional RTCRtpParameters parameters);
+ RTCRtpParameters getParameters();
+ };
+
+ dictionary RTCRtpParameters {
+ DOMString transactionId;
+ sequence<RTCRtpEncodingParameters> encodings;
+ sequence<RTCRtpHeaderExtensionParameters> headerExtensions;
+ RTCRtcpParameters rtcp;
+ sequence<RTCRtpCodecParameters> codecs;
+ };
+
+ getParameters
+ - transactionId is set to a new unique identifier, used to match this
+ getParameters call to a setParameters call that may occur later.
+ */
+
+ /*
+ 5.2. getParameters
+ - transactionId is set to a new unique identifier, used to match this
+ getParameters call to a setParameters call that may occur later.
+ */
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const { sender } = pc.addTransceiver('audio');
+
+ const param1 = sender.getParameters();
+ const param2 = sender.getParameters();
+ assert_equals(typeof param1.transactionId, "string");
+ assert_greater_than(param1.transactionId.length, 0);
+ assert_equals(typeof param2.transactionId, "string");
+ assert_greater_than(param2.transactionId.length, 0);
+ // Don't assert_equals() because the transcation ID is different on each run
+ // which makes the -expected.txt baseline different each failed run.
+ assert_true(param1.transactionId == param2.transactionId);
+
+ await undefined;
+ const param3 = sender.getParameters();
+ assert_equals(typeof param3.transactionId, "string");
+ assert_greater_than(param3.transactionId.length, 0);
+ assert_equals(param1.transactionId, param3.transactionId);
+ }, `sender.getParameters() should return the same transaction ID if called back-to-back without relinquishing the event loop, even if the microtask queue runs`);
+
+ test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const { sender } = pc.addTransceiver('audio');
+
+ const param1 = sender.getParameters();
+ sender.setParameters(param1);
+ const param2 = sender.getParameters();
+ assert_equals(typeof param1.transactionId, "string");
+ assert_greater_than(param1.transactionId.length, 0);
+ assert_equals(typeof param2.transactionId, "string");
+ assert_greater_than(param2.transactionId.length, 0);
+
+ // Don't assert_equals() because the transcation ID is different on each run
+ // which makes the -expected.txt baseline different each failed run.
+ assert_true(param1.transactionId == param2.transactionId);
+ }, `sender.getParameters() should return the same transaction ID if called back-to-back without relinquishing the event loop, even if there is an intervening call to setParameters`);
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const { sender } = pc.addTransceiver('audio');
+
+ const param1 = sender.getParameters();
+ await pc.createOffer();
+ const param2 = sender.getParameters();
+ assert_equals(typeof param1.transactionId, "string");
+ assert_greater_than(param1.transactionId.length, 0);
+ assert_equals(typeof param2.transactionId, "string");
+ assert_greater_than(param2.transactionId.length, 0);
+
+ assert_not_equals(param1.transactionId, param2.transactionId);
+ }, `sender.getParameters() should return a different transaction ID if the event loop is relinquished between multiple calls`);
+
+ /*
+ 5.2. setParameters
+ 7. If parameters.encodings.length is different from N, or if any parameter
+ in the parameters argument, marked as a Read-only parameter, has a value
+ that is different from the corresponding parameter value returned from
+ sender.getParameters(), abort these steps and return a promise rejected
+ with a newly created InvalidModificationError. Note that this also applies
+ to transactionId.
+ */
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const { sender } = pc.addTransceiver('audio');
+
+ const param = sender.getParameters();
+
+ const { transactionId } = param;
+ param.transactionId = `${transactionId}-modified`;
+
+ await promise_rejects_dom(t, 'InvalidModificationError',
+ sender.setParameters(param));
+ }, `sender.setParameters() with transaction ID different from last getParameters() should reject with InvalidModificationError`);
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const { sender } = pc.addTransceiver('audio');
+
+ const param = sender.getParameters();
+
+ delete param.transactionId;
+
+ await promise_rejects_js(t, TypeError,
+ sender.setParameters(param));
+ }, `sender.setParameters() with transaction ID unset should reject with TypeError`);
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const { sender } = pc.addTransceiver('audio');
+
+ const param = sender.getParameters();
+
+ await sender.setParameters(param);
+ await promise_rejects_dom(t, 'InvalidStateError', sender.setParameters(param))
+ }, `setParameters() twice with the same parameters should reject with InvalidStateError`);
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const { sender } = pc.addTransceiver('audio');
+
+ const param1 = sender.getParameters();
+ // Queue a task, does not really matter what kind
+ await pc.createOffer();
+ const param2 = sender.getParameters();
+
+ assert_not_equals(param1.transactionId, param2.transactionId);
+
+ await promise_rejects_dom(t, 'InvalidModificationError',
+ sender.setParameters(param1));
+ }, `setParameters() with parameters older than last getParameters() should reject with InvalidModificationError`);
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const { sender } = pc.addTransceiver('audio');
+
+ const param1 = sender.getParameters();
+ await pc.createOffer();
+
+ await promise_rejects_dom(t, 'InvalidStateError',
+ sender.setParameters(param1));
+ }, `setParameters() when the event loop has been relinquished since the last getParameters() should reject with InvalidStateError`);
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCRtpReceiver-getCapabilities.html b/testing/web-platform/tests/webrtc/RTCRtpReceiver-getCapabilities.html
new file mode 100644
index 0000000000..21dcae208a
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCRtpReceiver-getCapabilities.html
@@ -0,0 +1,39 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCRtpReceiver.getCapabilities</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="dictionary-helper.js"></script>
+<script src="RTCRtpCapabilities-helper.js"></script>
+<script>
+ 'use strict';
+
+ // Test is based on the following editor draft:
+ // https://w3c.github.io/webrtc-pc/archives/20170605/webrtc.html
+
+ // The following helper functions are called from RTCRtpCapabilities-helper.js:
+ // validateRtpCapabilities
+
+ /*
+ 5.3. RTCRtpReceiver Interface
+ interface RTCRtpReceiver {
+ ...
+ static RTCRtpCapabilities getCapabilities(DOMString kind);
+ };
+ */
+ test(() => {
+ const capabilities = RTCRtpReceiver.getCapabilities('audio');
+ validateRtpCapabilities(capabilities);
+ }, `RTCRtpSender.getCapabilities('audio') should return RTCRtpCapabilities dictionary`);
+
+ test(() => {
+ const capabilities = RTCRtpReceiver.getCapabilities('video');
+ validateRtpCapabilities(capabilities);
+ }, `RTCRtpSender.getCapabilities('video') should return RTCRtpCapabilities dictionary`);
+
+ test(() => {
+ const capabilities = RTCRtpReceiver.getCapabilities('dummy');
+ assert_equals(capabilities, null);
+ }, `RTCRtpSender.getCapabilities('dummy') should return null`);
+
+ </script>
diff --git a/testing/web-platform/tests/webrtc/RTCRtpReceiver-getContributingSources.https.html b/testing/web-platform/tests/webrtc/RTCRtpReceiver-getContributingSources.https.html
new file mode 100644
index 0000000000..7245d477cc
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCRtpReceiver-getContributingSources.https.html
@@ -0,0 +1,35 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCRtpReceiver.prototype.getContributingSources</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="RTCPeerConnection-helper.js"></script>
+<script>
+'use strict';
+
+async function connectAndExpectNoCsrcs(t, kind) {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const stream = await getNoiseStream({[kind]:true});
+ const [track] = stream.getTracks();
+ t.add_cleanup(() => track.stop());
+ pc1.addTrack(track, stream);
+
+ exchangeIceCandidates(pc1, pc2);
+ const trackEvent = await exchangeOfferAndListenToOntrack(t, pc1, pc2);
+ await exchangeAnswer(pc1, pc2);
+
+ assert_array_equals(trackEvent.receiver.getContributingSources(), []);
+}
+
+promise_test(async t => {
+ await connectAndExpectNoCsrcs(t, 'audio');
+}, '[audio] getContributingSources() returns an empty list in loopback call');
+
+promise_test(async t => {
+ await connectAndExpectNoCsrcs(t, 'video');
+}, '[video] getContributingSources() returns an empty list in loopback call');
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCRtpReceiver-getParameters.html b/testing/web-platform/tests/webrtc/RTCRtpReceiver-getParameters.html
new file mode 100644
index 0000000000..7047ce7d1f
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCRtpReceiver-getParameters.html
@@ -0,0 +1,73 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCRtpReceiver.prototype.getParameters</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="dictionary-helper.js"></script>
+<script src="RTCRtpParameters-helper.js"></script>
+<script>
+ 'use strict';
+
+ // Test is based on the following editor draft:
+ // https://w3c.github.io/webrtc-pc/archives/20170605/webrtc.html
+
+ // The following helper functions are called from RTCRtpParameters-helper.js:
+ // validateReceiverRtpParameters
+
+ /*
+ Validates the RTCRtpParameters returned from RTCRtpReceiver.prototype.getParameters
+
+ 5.3. RTCRtpReceiver Interface
+ getParameters
+ When getParameters is called, the RTCRtpParameters dictionary is constructed
+ as follows:
+
+ - The headerExtensions sequence is populated based on the header extensions that
+ the receiver is currently prepared to receive.
+
+ - The codecs sequence is populated based on the codecs that the receiver is currently
+ prepared to receive.
+
+ - rtcp.reducedSize is set to true if the receiver is currently prepared to receive
+ reduced-size RTCP packets, and false otherwise. rtcp.cname is left undefined.
+ */
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ pc.addTransceiver('audio');
+ const callee = await doOfferAnswerExchange(t, pc);
+ const param = callee.getTransceivers()[0].receiver.getParameters();
+ validateReceiverRtpParameters(param);
+
+ assert_greater_than(param.headerExtensions.length, 0);
+ assert_greater_than(param.codecs.length, 0);
+ }, 'getParameters() with audio receiver');
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ pc.addTransceiver('video');
+ const callee = await doOfferAnswerExchange(t, pc);
+ const param = callee.getTransceivers()[0].receiver.getParameters();
+ validateReceiverRtpParameters(param);
+
+ assert_greater_than(param.headerExtensions.length, 0);
+ assert_greater_than(param.codecs.length, 0);
+ }, 'getParameters() with video receiver');
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ pc.addTransceiver('video', {
+ sendEncodings: [
+ { rid: "rid1" },
+ { rid: "rid2" }
+ ]
+ });
+ const callee = await doOfferAnswerExchange(t, pc);
+ const param = callee.getTransceivers()[0].receiver.getParameters();
+ validateReceiverRtpParameters(param);
+ assert_greater_than(param.headerExtensions.length, 0);
+ assert_greater_than(param.codecs.length, 0);
+ }, 'getParameters() with simulcast video receiver');
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCRtpReceiver-getStats.https.html b/testing/web-platform/tests/webrtc/RTCRtpReceiver-getStats.https.html
new file mode 100644
index 0000000000..39948ed6f7
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCRtpReceiver-getStats.https.html
@@ -0,0 +1,145 @@
+<!doctype html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<title>RTCRtpReceiver.prototype.getStats</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="RTCPeerConnection-helper.js"></script>
+<script src="dictionary-helper.js"></script>
+<script src="RTCStats-helper.js"></script>
+<script>
+ 'use strict';
+
+ // Test is based on the following editor draft:
+ // https://w3c.github.io/webrtc-pc/archives/20170605/webrtc.html
+ // https://w3c.github.io/webrtc-stats/archives/20170614/webrtc-stats.html
+
+ // The following helper functions are called from RTCPeerConnection-helper.js:
+ // exchangeOfferAnswer
+
+ // The following helper function is called from RTCStats-helper.js
+ // validateStatsReport
+ // assert_stats_report_has_stats
+
+ /*
+ 5.3. RTCRtpReceiver Interface
+ interface RTCRtpReceiver {
+ Promise<RTCStatsReport> getStats();
+ ...
+ };
+
+ getStats
+ 1. Let selector be the RTCRtpReceiver object on which the method was invoked.
+ 2. Let p be a new promise, and run the following steps in parallel:
+ 1. Gather the stats indicated by selector according to the stats selection
+ algorithm.
+ 2. Resolve p with the resulting RTCStatsReport object, containing the
+ gathered stats.
+ 3. Return p.
+
+ 8.5. The stats selection algorithm
+ 4. If selector is an RTCRtpReceiver, gather stats for and add the following objects
+ to result:
+ - All RTCInboundRtpStreamStats objects corresponding to selector.
+ - All stats objects referenced directly or indirectly by the RTCInboundRtpStreamStats
+ added.
+ */
+
+ promise_test(async t => {
+ const caller = new RTCPeerConnection();
+ t.add_cleanup(() => caller.close());
+ const callee = new RTCPeerConnection();
+ t.add_cleanup(() => callee.close());
+
+ const stream = await getNoiseStream({audio:true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const [track] = stream.getTracks();
+ callee.addTrack(track, stream);
+
+ const { receiver } = caller.addTransceiver('audio');
+
+ await exchangeOfferAnswer(caller, callee);
+ const statsReport = await receiver.getStats();
+ validateStatsReport(statsReport);
+ assert_stats_report_has_stats(statsReport, ['inbound-rtp']);
+ }, 'receiver.getStats() via addTransceiver should return stats report containing inbound-rtp stats');
+
+ promise_test(async t => {
+ const caller = new RTCPeerConnection();
+ t.add_cleanup(() => caller.close());
+ const callee = new RTCPeerConnection();
+ t.add_cleanup(() => callee.close());
+ const stream = await getNoiseStream({audio:true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const [track] = stream.getTracks();
+ caller.addTrack(track, stream);
+
+ await exchangeOfferAnswer(caller, callee);
+ const receiver = callee.getReceivers()[0];
+ const statsReport = await receiver.getStats();
+ validateStatsReport(statsReport);
+ assert_stats_report_has_stats(statsReport, ['inbound-rtp']);
+ }, 'receiver.getStats() via addTrack should return stats report containing inbound-rtp stats');
+
+
+ promise_test(async t => {
+ const caller = new RTCPeerConnection();
+ t.add_cleanup(() => caller.close());
+ const callee = new RTCPeerConnection();
+ t.add_cleanup(() => callee.close());
+ const stream = await getNoiseStream({audio:true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const [track] = stream.getTracks();
+ caller.addTrack(track, stream);
+
+ await exchangeOfferAnswer(caller, callee);
+ const [receiver] = callee.getReceivers();
+ const [transceiver] = callee.getTransceivers();
+ const statsPromiseFirst = receiver.getStats();
+ transceiver.stop();
+ const statsReportFirst = await statsPromiseFirst;
+ const statsReportSecond = await receiver.getStats();
+ validateStatsReport(statsReportFirst);
+ validateStatsReport(statsReportSecond);
+ }, 'receiver.getStats() should work on a stopped transceiver');
+
+ promise_test(async t => {
+ const caller = new RTCPeerConnection();
+ t.add_cleanup(() => caller.close());
+ const callee = new RTCPeerConnection();
+ t.add_cleanup(() => callee.close());
+ const stream = await getNoiseStream({audio:true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const [track] = stream.getTracks();
+ caller.addTrack(track, stream);
+
+ await exchangeOfferAnswer(caller, callee);
+ const [receiver] = callee.getReceivers();
+ const statsPromiseFirst = receiver.getStats();
+ callee.close();
+ const statsReportFirst = await statsPromiseFirst;
+ const statsReportSecond = await receiver.getStats();
+ validateStatsReport(statsReportFirst);
+ validateStatsReport(statsReportSecond);
+ }, 'receiver.getStats() should work with a closed PeerConnection');
+
+ promise_test(async t => {
+ const caller = new RTCPeerConnection();
+ t.add_cleanup(() => caller.close());
+ const callee = new RTCPeerConnection();
+ t.add_cleanup(() => callee.close());
+ const stream = await getNoiseStream({audio:true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const [track] = stream.getTracks();
+ caller.addTrack(track, stream);
+
+ exchangeIceCandidates(caller, callee);
+ exchangeIceCandidates(callee, caller);
+ await exchangeOfferAnswer(caller, callee);
+ await waitForIceStateChange(callee, ['connected', 'completed']);
+ const receiver = callee.getReceivers()[0];
+ const statsReport = await receiver.getStats();
+ assert_stats_report_has_stats(statsReport, ['candidate-pair', 'local-candidate', 'remote-candidate']);
+ }, 'receiver.getStats() should return stats report containing ICE candidate stats');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCRtpReceiver-getSynchronizationSources.https.html b/testing/web-platform/tests/webrtc/RTCRtpReceiver-getSynchronizationSources.https.html
new file mode 100644
index 0000000000..8436a44ebc
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCRtpReceiver-getSynchronizationSources.https.html
@@ -0,0 +1,105 @@
+<!doctype html>
+<meta charset=utf-8>
+<!-- This file contains two tests that wait for 10 seconds each. -->
+<meta name="timeout" content="long">
+<title>RTCRtpReceiver.prototype.getSynchronizationSources</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="RTCPeerConnection-helper.js"></script>
+<script>
+'use strict';
+
+async function initiateSingleTrackCallAndReturnReceiver(t, kind) {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const stream = await getNoiseStream({[kind]:true});
+ const [track] = stream.getTracks();
+ t.add_cleanup(() => track.stop());
+ pc1.addTrack(track, stream);
+
+ exchangeIceCandidates(pc1, pc2);
+ const trackEvent = await exchangeOfferAndListenToOntrack(t, pc1, pc2);
+ await exchangeAnswer(pc1, pc2);
+ return trackEvent.receiver;
+}
+
+for (const kind of ['audio', 'video']) {
+ promise_test(async t => {
+ const receiver = await initiateSingleTrackCallAndReturnReceiver(t, kind);
+ await listenForSSRCs(t, receiver);
+ }, '[' + kind + '] getSynchronizationSources() eventually returns a ' +
+ 'non-empty list');
+
+ promise_test(async t => {
+ const startTime = performance.now();
+ const receiver = await initiateSingleTrackCallAndReturnReceiver(t, kind);
+ const [ssrc] = await listenForSSRCs(t, receiver);
+ assert_equals(typeof ssrc.timestamp, 'number');
+ assert_true(ssrc.timestamp >= startTime);
+ }, '[' + kind + '] RTCRtpSynchronizationSource.timestamp is a number');
+
+ promise_test(async t => {
+ const receiver = await initiateSingleTrackCallAndReturnReceiver(t, kind);
+ const [ssrc] = await listenForSSRCs(t, receiver);
+ assert_equals(typeof ssrc.rtpTimestamp, 'number');
+ assert_greater_than_equal(ssrc.rtpTimestamp, 0);
+ assert_less_than_equal(ssrc.rtpTimestamp, 0xffffffff);
+ }, '[' + kind + '] RTCRtpSynchronizationSource.rtpTimestamp is a number ' +
+ '[0, 2^32-1]');
+
+ promise_test(async t => {
+ const receiver = await initiateSingleTrackCallAndReturnReceiver(t, kind);
+ // Wait for packets to start flowing.
+ await listenForSSRCs(t, receiver);
+ // Wait for 10 seconds.
+ await new Promise(resolve => t.step_timeout(resolve, 10000));
+ let earliestTimestamp = undefined;
+ let latestTimestamp = undefined;
+ for (const ssrc of await listenForSSRCs(t, receiver)) {
+ if (earliestTimestamp == undefined || earliestTimestamp > ssrc.timestamp)
+ earliestTimestamp = ssrc.timestamp;
+ if (latestTimestamp == undefined || latestTimestamp < ssrc.timestamp)
+ latestTimestamp = ssrc.timestamp;
+ }
+ assert_true(latestTimestamp - earliestTimestamp <= 10000);
+ }, '[' + kind + '] getSynchronizationSources() does not contain SSRCs ' +
+ 'older than 10 seconds');
+
+ promise_test(async t => {
+ const startTime = performance.timeOrigin + performance.now();
+ const receiver = await initiateSingleTrackCallAndReturnReceiver(t, kind);
+ const [ssrc] = await listenForSSRCs(t, receiver);
+ const endTime = performance.timeOrigin + performance.now();
+ assert_true(startTime <= ssrc.timestamp && ssrc.timestamp <= endTime);
+ }, '[' + kind + '] RTCRtpSynchronizationSource.timestamp is comparable to ' +
+ 'performance.timeOrigin + performance.now()');
+
+ promise_test(async t => {
+ const receiver = await initiateSingleTrackCallAndReturnReceiver(t, kind);
+ const [ssrc] = await listenForSSRCs(t, receiver);
+ assert_equals(typeof ssrc.source, 'number');
+ }, '[' + kind + '] RTCRtpSynchronizationSource.source is a number');
+}
+
+promise_test(async t => {
+ const receiver = await initiateSingleTrackCallAndReturnReceiver(t, 'audio');
+ const [ssrc] = await listenForSSRCs(t, receiver);
+ assert_equals(typeof ssrc.audioLevel, 'number');
+ assert_greater_than_equal(ssrc.audioLevel, 0);
+ assert_less_than_equal(ssrc.audioLevel, 1);
+}, '[audio-only] RTCRtpSynchronizationSource.audioLevel is a number [0, 1]');
+
+// This test only passes if the implementation is sending the RFC 6464 extension
+// header and the "vad" extension attribute is not "off", otherwise
+// voiceActivityFlag is absent. TODO: Consider moving this test to an
+// optional-to-implement subfolder?
+promise_test(async t => {
+ const receiver = await initiateSingleTrackCallAndReturnReceiver(t, 'audio');
+ const [ssrc] = await listenForSSRCs(t, receiver);
+ assert_equals(typeof ssrc.voiceActivityFlag, 'boolean');
+}, '[audio-only] RTCRtpSynchronizationSource.voiceActivityFlag is a boolean');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCRtpSender-encode-same-track-twice.https.html b/testing/web-platform/tests/webrtc/RTCRtpSender-encode-same-track-twice.https.html
new file mode 100644
index 0000000000..568543da70
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCRtpSender-encode-same-track-twice.https.html
@@ -0,0 +1,66 @@
+<!doctype html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<title></title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src="RTCPeerConnection-helper.js"></script>
+<script>
+ 'use strict';
+
+ // A generous testing duration that will not time out on bots.
+ const kEncodeDurationMs = 10000;
+
+ // The crash this test aims to repro was easy to reproduce using a normal
+ // getUserMedia() track when running the browser normally, e.g. by navigating
+ // to https://jsfiddle.net/henbos/fc7gk3ve/11/. But for some reason, the fake
+ // tracks returned by getUserMedia() when inside this testing environment had
+ // a much harder time with reproducibility.
+ //
+ // By creating a high FPS canvas capture track we are able to repro reliably
+ // in this WPT environment as well.
+ function whiteNoise(width, height) {
+ const canvas =
+ Object.assign(document.createElement('canvas'), {width, height});
+ const ctx = canvas.getContext('2d');
+ ctx.fillRect(0, 0, width, height);
+ const p = ctx.getImageData(0, 0, width, height);
+ requestAnimationFrame(function draw () {
+ for (let i = 0; i < p.data.length; i++) {
+ const color = Math.random() * 255;
+ p.data[i++] = color;
+ p.data[i++] = color;
+ p.data[i++] = color;
+ }
+ ctx.putImageData(p, 0, 0);
+ requestAnimationFrame(draw);
+ });
+ return canvas.captureStream();
+ }
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const stream = whiteNoise(640, 480);
+ const [track] = stream.getTracks();
+ const t1 = pc1.addTransceiver("video", {direction:"sendonly"});
+ const t2 = pc1.addTransceiver("video", {direction:"sendonly"});
+ await t1.sender.replaceTrack(track);
+ await t2.sender.replaceTrack(track);
+
+ exchangeIceCandidates(pc1, pc2);
+ await pc1.setLocalDescription();
+ await pc2.setRemoteDescription(pc1.localDescription);
+ await pc2.setLocalDescription();
+ await pc1.setRemoteDescription(pc2.localDescription);
+
+ // In Chromium, each sender instantiates a VideoStreamEncoder during
+ // negotiation. This test reproduces https://crbug.com/webrtc/11485 where a
+ // race causes a crash when multiple VideoStreamEncoders are encoding the
+ // same MediaStreamTrack.
+ await new Promise(resolve => t.step_timeout(resolve, kEncodeDurationMs));
+ }, "Two RTCRtpSenders encoding the same track");
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCRtpSender-getCapabilities.html b/testing/web-platform/tests/webrtc/RTCRtpSender-getCapabilities.html
new file mode 100644
index 0000000000..3d41c62016
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCRtpSender-getCapabilities.html
@@ -0,0 +1,45 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCRtpSender.getCapabilities</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="dictionary-helper.js"></script>
+<script src="RTCRtpCapabilities-helper.js"></script>
+<script>
+ 'use strict';
+
+ // Test is based on the following editor draft:
+ // https://w3c.github.io/webrtc-pc/archives/20170605/webrtc.html
+
+ // The following helper functions are called from RTCRtpCapabilities-helper.js:
+ // validateRtpCapabilities
+
+ /*
+ 5.2. RTCRtpSender Interface
+ interface RTCRtpSender {
+ ...
+ static RTCRtpCapabilities getCapabilities(DOMString kind);
+ };
+
+ getCapabilities
+ The getCapabilities() method returns the most optimist view on the capabilities
+ of the system for sending media of the given kind. It does not reserve any
+ resources, ports, or other state but is meant to provide a way to discover
+ the types of capabilities of the browser including which codecs may be supported.
+ */
+ test(() => {
+ const capabilities = RTCRtpSender.getCapabilities('audio');
+ validateRtpCapabilities(capabilities);
+ }, `RTCRtpSender.getCapabilities('audio') should return RTCRtpCapabilities dictionary`);
+
+ test(() => {
+ const capabilities = RTCRtpSender.getCapabilities('video');
+ validateRtpCapabilities(capabilities);
+ }, `RTCRtpSender.getCapabilities('video') should return RTCRtpCapabilities dictionary`);
+
+ test(() => {
+ const capabilities = RTCRtpSender.getCapabilities('dummy');
+ assert_equals(capabilities, null);
+ }, `RTCRtpSender.getCapabilities('dummy') should return null`);
+
+ </script>
diff --git a/testing/web-platform/tests/webrtc/RTCRtpSender-getStats.https.html b/testing/web-platform/tests/webrtc/RTCRtpSender-getStats.https.html
new file mode 100644
index 0000000000..62c01aafa6
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCRtpSender-getStats.https.html
@@ -0,0 +1,97 @@
+<!doctype html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<title>RTCRtpSender.prototype.getStats</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="RTCPeerConnection-helper.js"></script>
+<script src="dictionary-helper.js"></script>
+<script src="RTCStats-helper.js"></script>
+<script>
+ 'use strict';
+
+ // Test is based on the following editor draft:
+ // webrtc-pc 20171130
+ // webrtc-stats 20171122
+
+ // The following helper functions are called from RTCPeerConnection-helper.js:
+ // exchangeOfferAnswer
+
+ // The following helper function is called from RTCStats-helper.js
+ // validateStatsReport
+ // assert_stats_report_has_stats
+
+ /*
+ 5.2. RTCRtpSender Interface
+ getStats
+ 1. Let selector be the RTCRtpSender object on which the method was invoked.
+ 2. Let p be a new promise, and run the following steps in parallel:
+ 1. Gather the stats indicated by selector according to the stats selection
+ algorithm.
+ 2. Resolve p with the resulting RTCStatsReport object, containing the
+ gathered stats.
+ 3. Return p.
+
+ 8.5. The stats selection algorithm
+ 3. If selector is an RTCRtpSender, gather stats for and add the following objects
+ to result:
+ - All RTCOutboundRtpStreamStats objects corresponding to selector.
+ - All stats objects referenced directly or indirectly by the RTCOutboundRtpStreamStats
+ objects added.
+ */
+
+ promise_test(async t => {
+ const caller = new RTCPeerConnection();
+ t.add_cleanup(() => caller.close());
+ const callee = new RTCPeerConnection();
+ t.add_cleanup(() => callee.close());
+
+ const stream = await getNoiseStream({audio:true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const [track] = stream.getTracks();
+ const { sender } = caller.addTransceiver(track);
+
+ await exchangeOfferAnswer(caller, callee);
+ const statsReport = await sender.getStats();
+ validateStatsReport(statsReport);
+ assert_stats_report_has_stats(statsReport, ['outbound-rtp']);
+ }, 'sender.getStats() via addTransceiver should return stats report containing outbound-rtp stats');
+
+ promise_test(async t => {
+ const caller = new RTCPeerConnection();
+ t.add_cleanup(() => caller.close());
+ const callee = new RTCPeerConnection();
+ t.add_cleanup(() => callee.close());
+ const stream = await getNoiseStream({audio:true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const [track] = stream.getTracks();
+ const sender = caller.addTrack(track, stream);
+
+ await exchangeOfferAnswer(caller, callee);
+ const statsReport = await sender.getStats();
+ validateStatsReport(statsReport);
+ assert_stats_report_has_stats(statsReport, ['outbound-rtp']);
+ }, 'sender.getStats() via addTrack should return stats report containing outbound-rtp stats');
+
+ promise_test(async t => {
+ const caller = new RTCPeerConnection();
+ t.add_cleanup(() => caller.close());
+ const callee = new RTCPeerConnection();
+ t.add_cleanup(() => callee.close());
+ const stream = await getNoiseStream({audio:true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const [track] = stream.getTracks();
+ const { sender } = caller.addTransceiver(track);
+
+ exchangeIceCandidates(caller, callee);
+ exchangeIceCandidates(callee, caller);
+ await exchangeOfferAnswer(caller, callee);
+ // Pairing should be possible as soon as we are 'checking', but to allow the
+ // pairing to happen asynchronously, we wait until 'connected' or
+ // 'completed' instead as it is not possible to reach these without a pair.
+ await waitForIceStateChange(caller, ['connected', 'completed']);
+ const statsReport = await sender.getStats();
+ assert_stats_report_has_stats(statsReport, ['candidate-pair', 'local-candidate', 'remote-candidate']);
+ }, 'sender.getStats() should return stats report containing ICE candidate stats');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCRtpSender-replaceTrack.https.html b/testing/web-platform/tests/webrtc/RTCRtpSender-replaceTrack.https.html
new file mode 100644
index 0000000000..bec44c53e4
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCRtpSender-replaceTrack.https.html
@@ -0,0 +1,338 @@
+<!doctype html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<title>RTCRtpSender.prototype.replaceTrack</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="RTCPeerConnection-helper.js"></script>
+<script>
+ 'use strict';
+
+ // Test is based on the following editor draft:
+ // https://w3c.github.io/webrtc-pc/archives/20170605/webrtc.html
+
+ /*
+ 5.2. RTCRtpSender Interface
+ interface RTCRtpSender {
+ readonly attribute MediaStreamTrack? track;
+ Promise<void> replaceTrack(MediaStreamTrack? withTrack);
+ ...
+ };
+
+ replaceTrack
+ Attempts to replace the track being sent with another track provided
+ (or with a null track), without renegotiation.
+ */
+
+ /*
+ 5.2. replaceTrack
+ 4. If connection's [[isClosed]] slot is true, return a promise rejected
+ with a newly created InvalidStateError and abort these steps.
+ */
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const stream = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const [track] = stream.getTracks();
+
+ const transceiver = pc.addTransceiver('audio');
+ const { sender } = transceiver;
+ pc.close();
+
+ return promise_rejects_dom(t, 'InvalidStateError',
+ sender.replaceTrack(track));
+ }, 'Calling replaceTrack on closed connection should reject with InvalidStateError');
+
+ /*
+ 5.2. replaceTrack
+ 7. If withTrack is non-null and withTrack.kind differs from the
+ transceiver kind of transceiver, return a promise rejected with a
+ newly created TypeError.
+ */
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const stream = await getNoiseStream({video: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const [track] = stream.getTracks();
+
+ const transceiver = pc.addTransceiver('audio');
+ const { sender } = transceiver;
+
+ return promise_rejects_js(t, TypeError,
+ sender.replaceTrack(track));
+ }, 'Calling replaceTrack with track of different kind should reject with TypeError');
+
+ /*
+ 5.2. replaceTrack
+ 5. If transceiver.stopped is true, return a promise rejected with a newly
+ created InvalidStateError.
+ */
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const stream = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const [track] = stream.getTracks();
+
+ const transceiver = pc.addTransceiver('audio');
+ const { sender } = transceiver;
+ transceiver.stop();
+
+ return promise_rejects_dom(t, 'InvalidStateError',
+ sender.replaceTrack(track));
+ }, 'Calling replaceTrack on stopped sender should reject with InvalidStateError');
+
+ /*
+ 5.2. replaceTrack
+ 8. If transceiver is not yet associated with a media description [JSEP]
+ (section 3.4.1.), then set sender's track attribute to withTrack, and
+ return a promise resolved with undefined.
+ */
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const stream = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const [track] = stream.getTracks();
+
+ const transceiver = pc.addTransceiver('audio');
+ const { sender } = transceiver;
+ assert_equals(sender.track, null);
+
+ return sender.replaceTrack(track)
+ .then(() => {
+ assert_equals(sender.track, track);
+ });
+ }, 'Calling replaceTrack on sender with null track and not set to session description should resolve with sender.track set to given track');
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const stream1 = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stream1.getTracks().forEach(track => track.stop()));
+ const [track1] = stream1.getTracks();
+ const stream2 = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stream2.getTracks().forEach(track => track.stop()));
+ const [track2] = stream2.getTracks();
+
+ const transceiver = pc.addTransceiver(track1);
+ const { sender } = transceiver;
+
+ assert_equals(sender.track, track1);
+
+ return sender.replaceTrack(track2)
+ .then(() => {
+ assert_equals(sender.track, track2);
+ });
+ }, 'Calling replaceTrack on sender not set to session description should resolve with sender.track set to given track');
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const stream = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const [track] = stream.getTracks();
+
+ const transceiver = pc.addTransceiver(track);
+ const { sender } = transceiver;
+
+ assert_equals(sender.track, track);
+
+ return sender.replaceTrack(null)
+ .then(() => {
+ assert_equals(sender.track, null);
+ });
+ }, 'Calling replaceTrack(null) on sender not set to session description should resolve with sender.track set to null');
+
+ /*
+ 5.2. replaceTrack
+ 10. Run the following steps in parallel:
+ 1. Determine if negotiation is needed to transmit withTrack in place
+ of the sender's existing track.
+
+ Negotiation is not needed if withTrack is null.
+
+ 3. Queue a task that runs the following steps:
+ 2. Set sender's track attribute to withTrack.
+ */
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const stream = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const [track] = stream.getTracks();
+
+ const transceiver = pc.addTransceiver(track);
+ const { sender } = transceiver;
+
+ assert_equals(sender.track, track);
+
+ return pc.createOffer()
+ .then(offer => pc.setLocalDescription(offer))
+ .then(() => sender.replaceTrack(null))
+ .then(() => {
+ assert_equals(sender.track, null);
+ });
+ }, 'Calling replaceTrack(null) on sender set to session description should resolve with sender.track set to null');
+
+ /*
+ 5.2. replaceTrack
+ 10. Run the following steps in parallel:
+ 1. Determine if negotiation is needed to transmit withTrack in place
+ of the sender's existing track.
+
+ Negotiation is not needed if the sender's existing track is
+ ended (which appears as though the track was muted).
+
+ 3. Queue a task that runs the following steps:
+ 2. Set sender's track attribute to withTrack.
+ */
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const stream1 = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stream1.getTracks().forEach(track => track.stop()));
+ const [track1] = stream1.getTracks();
+ const stream2 = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stream2.getTracks().forEach(track => track.stop()));
+ const [track2] = stream1.getTracks();
+
+ const transceiver = pc.addTransceiver(track1);
+ const { sender } = transceiver;
+ assert_equals(sender.track, track1);
+
+ track1.stop();
+
+ return pc.createOffer()
+ .then(offer => pc.setLocalDescription(offer))
+ .then(() => sender.replaceTrack(track2))
+ .then(() => {
+ assert_equals(sender.track, track2);
+ });
+ }, 'Calling replaceTrack on sender with stopped track and and set to session description should resolve with sender.track set to given track');
+
+ /*
+ 5.2. replaceTrack
+ 10. Run the following steps in parallel:
+ 1. Determine if negotiation is needed to transmit withTrack in place
+ of the sender's existing track.
+
+ (tracks generated with default parameters *should* be similar
+ enough to not require re-negotiation)
+
+ 3. Queue a task that runs the following steps:
+ 2. Set sender's track attribute to withTrack.
+ */
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const stream1 = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stream1.getTracks().forEach(track => track.stop()));
+ const [track1] = stream1.getTracks();
+ const stream2 = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stream2.getTracks().forEach(track => track.stop()));
+ const [track2] = stream1.getTracks();
+
+ const transceiver = pc.addTransceiver(track1);
+ const { sender } = transceiver;
+ assert_equals(sender.track, track1);
+
+ return pc.createOffer()
+ .then(offer => pc.setLocalDescription(offer))
+ .then(() => sender.replaceTrack(track2))
+ .then(() => {
+ assert_equals(sender.track, track2);
+ });
+ }, 'Calling replaceTrack on sender with similar track and and set to session description should resolve with sender.track set to new track');
+
+ /*
+ TODO
+ 5.2. replaceTrack
+ To avoid track identifiers changing on the remote receiving end when
+ a track is replaced, the sender must retain the original track
+ identifier and stream associations and use these in subsequent
+ negotiations.
+
+ Non-Testable
+ 5.2. replaceTrack
+ 10. Run the following steps in parallel:
+ 1. Determine if negotiation is needed to transmit withTrack in place
+ of the sender's existing track.
+
+ Ignore which MediaStream the track resides in and the id attribute
+ of the track in this determination.
+
+ If negotiation is needed, then reject p with a newly created
+ InvalidModificationError and abort these steps.
+
+ 2. If withTrack is null, have the sender stop sending, without
+ negotiating. Otherwise, have the sender switch seamlessly to
+ transmitting withTrack instead of the sender's existing track,
+ without negotiating.
+ 3. Queue a task that runs the following steps:
+ 1. If connection's [[isClosed]] slot is true, abort these steps.
+ */
+
+ promise_test(async t => {
+ const v = document.createElement('video');
+ v.autoplay = true;
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+ const stream1 = await getNoiseStream({video: {signal: 20}});
+ t.add_cleanup(() => stream1.getTracks().forEach(track => track.stop()));
+ const [track1] = stream1.getTracks();
+ const stream2 = await getNoiseStream({video: {signal: 250}});
+ t.add_cleanup(() => stream2.getTracks().forEach(track => track.stop()));
+ const [track2] = stream2.getTracks();
+ const sender = pc1.addTrack(track1);
+ pc2.ontrack = (e) => {
+ v.srcObject = new MediaStream([e.track]);
+ };
+ const metadataToBeLoaded = new Promise((resolve) => {
+ v.addEventListener('loadedmetadata', () => {
+ resolve();
+ });
+ });
+ exchangeIceCandidates(pc1, pc2);
+ exchangeOfferAnswer(pc1, pc2);
+ await metadataToBeLoaded;
+ await detectSignal(t, v, 20);
+ await sender.replaceTrack(track2);
+ await detectSignal(t, v, 250);
+ }, 'ReplaceTrack transmits the new track not the old track');
+
+ promise_test(async t => {
+ const v = document.createElement('video');
+ v.autoplay = true;
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+ const stream1 = await getNoiseStream({video: {signal: 20}});
+ t.add_cleanup(() => stream1.getTracks().forEach(track => track.stop()));
+ const [track1] = stream1.getTracks();
+ const stream2 = await getNoiseStream({video: {signal: 250}});
+ t.add_cleanup(() => stream2.getTracks().forEach(track => track.stop()));
+ const [track2] = stream2.getTracks();
+ const sender = pc1.addTrack(track1);
+ pc2.ontrack = (e) => {
+ v.srcObject = new MediaStream([e.track]);
+ };
+ const metadataToBeLoaded = new Promise((resolve) => {
+ v.addEventListener('loadedmetadata', () => {
+ resolve();
+ });
+ });
+ exchangeIceCandidates(pc1, pc2);
+ exchangeOfferAnswer(pc1, pc2);
+ await metadataToBeLoaded;
+ await detectSignal(t, v, 20);
+ await sender.replaceTrack(null);
+ await sender.replaceTrack(track2);
+ await detectSignal(t, v, 250);
+ }, 'ReplaceTrack null -> new track transmits the new track');
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCRtpSender-setParameters.html b/testing/web-platform/tests/webrtc/RTCRtpSender-setParameters.html
new file mode 100644
index 0000000000..94c572343d
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCRtpSender-setParameters.html
@@ -0,0 +1,52 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCRtpSender.prototype.setParameters</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="RTCPeerConnection-helper.js"></script>
+<script>
+ 'use strict';
+
+ // Test is based on the following editor draft:
+ // https://w3c.github.io/webrtc-pc/archives/20170605/webrtc.html
+
+ /*
+ 5.2. setParameters
+ 6. If transceiver.stopped is true, abort these steps and return a promise
+ rejected with a newly created InvalidStateError.
+ */
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const transceiver = pc.addTransceiver('audio');
+ const { sender } = transceiver;
+
+ const param = sender.getParameters();
+ transceiver.stop();
+
+ return promise_rejects_dom(t, 'InvalidStateError',
+ sender.setParameters(param));
+ }, `setParameters() when transceiver is stopped should reject with InvalidStateError`);
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const sender = pc.addTransceiver('audio').sender;
+ const param = sender.getParameters();
+ sender.setParameters(param);
+ await sender.setParameters(param);
+ }, `setParameters() with already used parameters should work if the event loop has not been relinquished`);
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const sender = pc.addTransceiver('audio').sender;
+ const param = sender.getParameters();
+ sender.setParameters(param);
+ await queueAWebrtcTask();
+
+ await promise_rejects_dom(t, 'InvalidStateError',
+ sender.setParameters(param));
+ }, `setParameters() with already used parameters should reject with InvalidStateError if the event loop has been relinquished`);
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCRtpSender-setStreams.https.html b/testing/web-platform/tests/webrtc/RTCRtpSender-setStreams.https.html
new file mode 100644
index 0000000000..03ae863d0f
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCRtpSender-setStreams.https.html
@@ -0,0 +1,127 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCRtpSender.prototype.setStreams</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="RTCPeerConnection-helper.js"></script>
+<script>
+'use strict';
+
+promise_test(async t => {
+ const caller = new RTCPeerConnection();
+ t.add_cleanup(() => caller.close());
+ const callee = new RTCPeerConnection();
+ t.add_cleanup(() => callee.close());
+ const stream = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const [track] = stream.getTracks();
+
+ const sender = caller.addTrack(track);
+ const stream1 = new MediaStream();
+ const stream2 = new MediaStream();
+ sender.setStreams(stream1, stream2);
+
+ const offer = await caller.createOffer();
+ callee.setRemoteDescription(offer);
+ return new Promise(resolve => callee.ontrack = t.step_func(event =>{
+ assert_equals(event.streams.length, 2);
+ const calleeStreamIds = event.streams.map(s => s.id);
+ assert_in_array(stream1.id, calleeStreamIds);
+ assert_in_array(stream2.id, calleeStreamIds);
+ resolve();
+ }));
+}, 'setStreams causes streams to be reported via ontrack on callee');
+
+promise_test(async t => {
+ const caller = new RTCPeerConnection();
+ t.add_cleanup(() => caller.close());
+ const callee = new RTCPeerConnection();
+ t.add_cleanup(() => callee.close());
+ const stream = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const [track] = stream.getTracks();
+
+ const sender = caller.addTrack(track);
+ sender.setStreams(stream);
+
+ const offer = await caller.createOffer();
+ callee.setRemoteDescription(offer);
+ return new Promise(resolve => callee.ontrack = t.step_func(event =>{
+ assert_equals(event.streams.length, 1);
+ assert_equals(stream.id, event.streams[0].id);
+ assert_equals(event.streams[0].getTracks()[0], event.track);
+ resolve();
+ }));
+}, 'setStreams can be used to reconstruct a stream with a track on the remote side');
+
+
+promise_test(async t => {
+ const caller = new RTCPeerConnection();
+ t.add_cleanup(() => caller.close());
+ const callee = new RTCPeerConnection();
+ t.add_cleanup(() => callee.close());
+
+ callee.ontrack = t.unreached_func();
+ const transceiver = caller.addTransceiver('audio', {direction: 'inactive'});
+ await exchangeOfferAnswer(caller, callee);
+
+ const stream1 = new MediaStream();
+ const stream2 = new MediaStream();
+ transceiver.direction = 'sendrecv';
+ transceiver.sender.setStreams(stream1, stream2);
+
+ const offer = await caller.createOffer();
+ callee.setRemoteDescription(offer);
+ return new Promise(resolve => callee.ontrack = t.step_func(event =>{
+ assert_equals(event.streams.length, 2);
+ const calleeStreamIds = event.streams.map(s => s.id);
+ assert_in_array(stream1.id, calleeStreamIds);
+ assert_in_array(stream2.id, calleeStreamIds);
+ assert_in_array(event.track, event.streams[0].getTracks());
+ assert_in_array(event.track, event.streams[1].getTracks());
+ resolve();
+ }));
+}, 'Adding streams and changing direction causes new streams to be reported via ontrack on callee');
+
+promise_test(async t => {
+ const caller = new RTCPeerConnection();
+ t.add_cleanup(() => caller.close());
+ const callee = new RTCPeerConnection();
+ t.add_cleanup(() => callee.close());
+
+ const stream1 = new MediaStream();
+ const stream2 = new MediaStream();
+ let calleeTrack = null;
+ callee.ontrack = t.step_func(event => {
+ assert_equals(event.streams.length, 0);
+ calleeTrack = event.track;
+ });
+ const transceiver = caller.addTransceiver('audio', {direction: 'sendrecv'});
+ await exchangeOfferAnswer(caller, callee);
+ assert_true(calleeTrack instanceof MediaStreamTrack);
+
+ transceiver.sender.setStreams(stream1, stream2);
+ const offer = await caller.createOffer();
+ callee.setRemoteDescription(offer);
+ return new Promise(resolve => callee.ontrack = t.step_func(event =>{
+ assert_equals(event.streams.length, 2);
+ const calleeStreamIds = event.streams.map(s => s.id);
+ assert_in_array(stream1.id, calleeStreamIds);
+ assert_in_array(stream2.id, calleeStreamIds);
+ assert_in_array(event.track, event.streams[0].getTracks());
+ assert_in_array(event.track, event.streams[1].getTracks());
+ assert_equals(event.track, calleeTrack);
+ resolve();
+ }));
+}, 'Adding streams to an active transceiver causes new streams to be reported via ontrack on callee');
+
+test(t => {
+ const pc = new RTCPeerConnection();
+ const stream1 = new MediaStream();
+ const stream2 = new MediaStream();
+ const transceiver = pc.addTransceiver('audio');
+
+ pc.close();
+ assert_throws_dom('InvalidStateError', () => transceiver.sender.setStreams(stream1, stream2));
+}, 'setStreams() fires InvalidStateError on a closed peer connection.');
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCRtpSender-transport.https.html b/testing/web-platform/tests/webrtc/RTCRtpSender-transport.https.html
new file mode 100644
index 0000000000..cd419ebc18
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCRtpSender-transport.https.html
@@ -0,0 +1,152 @@
+<!doctype html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<title>RTCRtpSender.transport</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="dictionary-helper.js"></script>
+<script src="RTCPeerConnection-helper.js"></script>
+<script>
+ 'use strict';
+
+ // Spec link: http://w3c.github.io/webrtc-pc/#dom-rtcrtpsender-transport
+ promise_test(async t => {
+ const caller = new RTCPeerConnection();
+ t.add_cleanup(() => caller.close());
+ const stream = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const [track] = stream.getTracks();
+ const sender = caller.addTrack(track);
+ assert_equals(sender.transport, null);
+ }, 'RTCRtpSender.transport is null when unconnected');
+
+ // Test for the simple/happy path of connecting a single track
+ promise_test(async t => {
+ const caller = new RTCPeerConnection();
+ t.add_cleanup(() => caller.close());
+ const stream = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const [track] = stream.getTracks();
+ const sender = caller.addTrack(track);
+ const callee = new RTCPeerConnection();
+ t.add_cleanup(() => callee.close());
+ exchangeIceCandidates(caller, callee);
+ await exchangeOfferAndListenToOntrack(t, caller, callee);
+ assert_not_equals(sender.transport, null);
+ const [transceiver] = caller.getTransceivers();
+ assert_equals(transceiver.sender.transport,
+ transceiver.receiver.transport);
+ assert_not_equals(sender.transport.iceTransport, null);
+ }, 'RTCRtpSender/receiver.transport has a value when connected');
+
+ // Test with multiple tracks, and checking details of when things show up
+ // for different bundle policies.
+ for (let bundle_policy of ['balanced', 'max-bundle', 'max-compat']) {
+ promise_test(async t => {
+ const caller = new RTCPeerConnection({bundlePolicy: bundle_policy});
+ t.add_cleanup(() => caller.close());
+ const stream = await getNoiseStream(
+ {audio: true, video:true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const [track1, track2] = stream.getTracks();
+ const sender1 = caller.addTrack(track1);
+ const sender2 = caller.addTrack(track2);
+ const callee = new RTCPeerConnection();
+ t.add_cleanup(() => callee.close());
+ exchangeIceCandidates(caller, callee);
+ const offer = await caller.createOffer();
+ assert_equals(sender1.transport, null);
+ assert_equals(sender2.transport, null);
+ await caller.setLocalDescription(offer);
+ assert_not_equals(sender1.transport, null);
+ assert_not_equals(sender2.transport, null);
+ const [caller_transceiver1, caller_transceiver2] = caller.getTransceivers();
+ assert_equals(sender1.transport, caller_transceiver1.sender.transport);
+ if (bundle_policy == 'max-bundle') {
+ assert_equals(caller_transceiver1.sender.transport,
+ caller_transceiver2.sender.transport);
+ } else {
+ assert_not_equals(caller_transceiver1.sender.transport,
+ caller_transceiver2.sender.transport);
+ }
+ await callee.setRemoteDescription(offer);
+ const [callee_transceiver1, callee_transceiver2] = callee.getTransceivers();
+ // According to spec, setRemoteDescription only updates the transports
+ // if the remote description is an answer.
+ assert_equals(callee_transceiver1.receiver.transport, null);
+ assert_equals(callee_transceiver2.receiver.transport, null);
+ const answer = await callee.createAnswer();
+ await callee.setLocalDescription(answer);
+ assert_not_equals(callee_transceiver1.receiver.transport, null);
+ assert_not_equals(callee_transceiver2.receiver.transport, null);
+ // At this point, bundle should have kicked in.
+ assert_equals(callee_transceiver1.receiver.transport,
+ callee_transceiver2.receiver.transport);
+ await caller.setRemoteDescription(answer);
+ assert_equals(caller_transceiver1.receiver.transport,
+ caller_transceiver2.receiver.transport);
+ }, 'RTCRtpSender/receiver.transport at the right time, with bundle policy ' + bundle_policy);
+
+ // Do the same test again, with DataChannel in the mix.
+ promise_test(async t => {
+ const caller = new RTCPeerConnection({bundlePolicy: bundle_policy});
+ t.add_cleanup(() => caller.close());
+ const stream = await getNoiseStream(
+ {audio: true, video:true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const [track1, track2] = stream.getTracks();
+ const sender1 = caller.addTrack(track1);
+ const sender2 = caller.addTrack(track2);
+ caller.createDataChannel('datachannel');
+ const callee = new RTCPeerConnection();
+ t.add_cleanup(() => callee.close());
+ exchangeIceCandidates(caller, callee);
+ const offer = await caller.createOffer();
+ assert_equals(sender1.transport, null);
+ assert_equals(sender2.transport, null);
+ if (caller.sctp) {
+ assert_equals(caller.sctp.transport, null);
+ }
+ await caller.setLocalDescription(offer);
+ assert_not_equals(sender1.transport, null);
+ assert_not_equals(sender2.transport, null);
+ assert_not_equals(caller.sctp.transport, null);
+ const [caller_transceiver1, caller_transceiver2] = caller.getTransceivers();
+ assert_equals(sender1.transport, caller_transceiver1.sender.transport);
+ if (bundle_policy == 'max-bundle') {
+ assert_equals(caller_transceiver1.sender.transport,
+ caller_transceiver2.sender.transport);
+ assert_equals(caller_transceiver1.sender.transport,
+ caller.sctp.transport);
+ } else {
+ assert_not_equals(caller_transceiver1.sender.transport,
+ caller_transceiver2.sender.transport);
+ assert_not_equals(caller_transceiver1.sender.transport,
+ caller.sctp.transport);
+ }
+ await callee.setRemoteDescription(offer);
+ const [callee_transceiver1, callee_transceiver2] = callee.getTransceivers();
+ // According to spec, setRemoteDescription only updates the transports
+ // if the remote description is an answer.
+ assert_equals(callee_transceiver1.receiver.transport, null);
+ assert_equals(callee_transceiver2.receiver.transport, null);
+ const answer = await callee.createAnswer();
+ await callee.setLocalDescription(answer);
+ assert_not_equals(callee_transceiver1.receiver.transport, null);
+ assert_not_equals(callee_transceiver2.receiver.transport, null);
+ assert_not_equals(callee.sctp.transport, null);
+ // At this point, bundle should have kicked in.
+ assert_equals(callee_transceiver1.receiver.transport,
+ callee_transceiver2.receiver.transport);
+ assert_equals(callee_transceiver1.receiver.transport,
+ callee.sctp.transport,
+ 'Callee SCTP transport does not match:');
+ await caller.setRemoteDescription(answer);
+ assert_equals(caller_transceiver1.receiver.transport,
+ caller_transceiver2.receiver.transport);
+ assert_equals(caller_transceiver1.receiver.transport,
+ caller.sctp.transport,
+ 'Caller SCTP transport does not match:');
+ }, 'RTCRtpSender/receiver/SCTP transport at the right time, with bundle policy ' + bundle_policy);
+ }
+ </script>
diff --git a/testing/web-platform/tests/webrtc/RTCRtpSender.https.html b/testing/web-platform/tests/webrtc/RTCRtpSender.https.html
new file mode 100644
index 0000000000..d17115c46a
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCRtpSender.https.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCRtpSender</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+ 'use strict';
+
+test((t) => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const t1 = pc.addTransceiver("audio");
+ const t2 = pc.addTransceiver("video");
+
+ assert_not_equals(t1.sender.dtmf, null);
+ assert_equals(t2.sender.dtmf, null);
+}, "Video sender @dtmf is null");
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCRtpTransceiver-direction.html b/testing/web-platform/tests/webrtc/RTCRtpTransceiver-direction.html
new file mode 100644
index 0000000000..e76bc1fbb7
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCRtpTransceiver-direction.html
@@ -0,0 +1,94 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCRtpTransceiver.prototype.direction</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="RTCPeerConnection-helper.js"></script>
+<script>
+ 'use strict';
+
+ // Test is based on the following editor draft:
+ // https://rawgit.com/w3c/webrtc-pc/8495678808d126d8bc764bf944996f32981fa6fd/webrtc.html
+
+ // The following helper functions are called from RTCPeerConnection-helper.js:
+ // generateAnswer
+
+ /*
+ 5.4. RTCRtpTransceiver Interface
+ interface RTCRtpTransceiver {
+ attribute RTCRtpTransceiverDirection direction;
+ readonly attribute RTCRtpTransceiverDirection? currentDirection;
+ ...
+ };
+ */
+
+ /*
+ 5.4. direction
+ 7. Set transceiver's [[Direction]] slot to newDirection.
+ */
+ test(t => {
+ const pc = new RTCPeerConnection();
+ const transceiver = pc.addTransceiver('audio');
+ assert_equals(transceiver.direction, 'sendrecv');
+ assert_equals(transceiver.currentDirection, null);
+
+ transceiver.direction = 'recvonly';
+ assert_equals(transceiver.direction, 'recvonly');
+ assert_equals(transceiver.currentDirection, null,
+ 'Expect transceiver.currentDirection to not change');
+
+ }, 'setting direction should change transceiver.direction');
+
+ /*
+ 5.4. direction
+ 3. If newDirection is equal to transceiver's [[Direction]] slot, abort
+ these steps.
+ */
+ test(t => {
+ const pc = new RTCPeerConnection();
+ const transceiver = pc.addTransceiver('audio', { direction: 'sendonly' });
+ assert_equals(transceiver.direction, 'sendonly');
+ transceiver.direction = 'sendonly';
+ assert_equals(transceiver.direction, 'sendonly');
+
+ }, 'setting direction with same direction should have no effect');
+
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const transceiver = pc.addTransceiver('audio', { direction: 'recvonly' });
+ assert_equals(transceiver.direction, 'recvonly');
+ assert_equals(transceiver.currentDirection, null);
+
+ return pc.createOffer()
+ .then(offer =>
+ pc.setLocalDescription(offer)
+ .then(() => generateAnswer(offer)))
+ .then(answer => pc.setRemoteDescription(answer))
+ .then(() => {
+ assert_equals(transceiver.currentDirection, 'inactive');
+ transceiver.direction = 'sendrecv';
+ assert_equals(transceiver.direction, 'sendrecv');
+ assert_equals(transceiver.currentDirection, 'inactive');
+ });
+ }, 'setting direction should change transceiver.direction independent of transceiver.currentDirection');
+
+ /*
+ TODO
+ An update of directionality does not take effect immediately. Instead, future calls
+ to createOffer and createAnswer mark the corresponding media description as
+ sendrecv, sendonly, recvonly or inactive as defined in [JSEP] (section 5.2.2.
+ and section 5.3.2.).
+
+ Tested in RTCPeerConnection-onnegotiationneeded.html
+ 5.4. direction
+ 6. Update the negotiation-needed flag for connection.
+
+ Coverage Report
+ Tested 6
+ Not Tested 1
+ Untestable 0
+ Total 7
+ */
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCRtpTransceiver-setCodecPreferences.html b/testing/web-platform/tests/webrtc/RTCRtpTransceiver-setCodecPreferences.html
new file mode 100644
index 0000000000..f779f5a94c
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCRtpTransceiver-setCodecPreferences.html
@@ -0,0 +1,322 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCRtpTransceiver.prototype.setCodecPreferences</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./third_party/sdp/sdp.js"></script>
+<script>
+ 'use strict';
+
+ // Test is based on the following editor draft:
+ // https://w3c.github.io/webrtc-pc/archives/20170605/webrtc.html
+
+ /*
+ 5.4. RTCRtpTransceiver Interface
+ interface RTCRtpTransceiver {
+ ...
+ void setCodecPreferences(sequence<RTCRtpCodecCapability> codecs);
+ };
+
+ setCodecPreferences
+ - Setting codecs to an empty sequence resets codec preferences to any
+ default value.
+
+ - The codecs sequence passed into setCodecPreferences can only contain
+ codecs that are returned by RTCRtpSender.getCapabilities(kind) or
+ RTCRtpReceiver.getCapabilities(kind), where kind is the kind of the
+ RTCRtpTransceiver on which the method is called. Additionally, the
+ RTCRtpCodecParameters dictionary members cannot be modified. If
+ codecs does not fulfill these requirements, the user agent MUST throw
+ an InvalidModificationError.
+ */
+ /*
+ * Chromium note: this requires build bots with H264 support. See
+ * https://bugs.chromium.org/p/chromium/issues/detail?id=840659
+ * for details on how to enable support.
+ */
+
+ test((t) => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const transceiver = pc.addTransceiver('audio');
+ const capabilities = RTCRtpSender.getCapabilities('audio');
+ transceiver.setCodecPreferences(capabilities.codecs);
+ }, `setCodecPreferences() on audio transceiver with codecs returned from RTCRtpSender.getCapabilities('audio') should succeed`);
+
+ test((t) => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const transceiver = pc.addTransceiver('video');
+ const capabilities = RTCRtpReceiver.getCapabilities('video');
+ transceiver.setCodecPreferences(capabilities.codecs);
+ }, `setCodecPreferences() on video transceiver with codecs returned from RTCRtpReceiver.getCapabilities('video') should succeed`);
+
+ test((t) => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const transceiver = pc.addTransceiver('audio');
+ const capabilities1 = RTCRtpSender.getCapabilities('audio');
+ const capabilities2 = RTCRtpReceiver.getCapabilities('audio');
+ transceiver.setCodecPreferences([...capabilities1.codecs, ... capabilities2.codecs]);
+ }, `setCodecPreferences() with both sender receiver codecs combined should succeed`);
+
+ test((t) => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const transceiver = pc.addTransceiver('audio');
+ transceiver.setCodecPreferences([]);
+ }, `setCodecPreferences([]) should succeed`);
+
+ test((t) => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const transceiver = pc.addTransceiver('audio');
+ const capabilities = RTCRtpSender.getCapabilities('audio');
+ const { codecs } = capabilities;
+
+ if(codecs.length >= 2) {
+ const tmp = codecs[0];
+ codecs[0] = codecs[1];
+ codecs[1] = tmp;
+ }
+
+ transceiver.setCodecPreferences(codecs);
+ }, `setCodecPreferences() with reordered codecs should succeed`);
+
+ test((t) => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const transceiver = pc.addTransceiver('video');
+ const capabilities = RTCRtpSender.getCapabilities('video');
+ const { codecs } = capabilities;
+ // This test verifies that the mandatory VP8 codec is present
+ // and can be set.
+ let tried = false;
+ codecs.forEach(codec => {
+ if (codec.mimeType.toLowerCase() === 'video/vp8') {
+ transceiver.setCodecPreferences([codecs[0]]);
+ tried = true;
+ }
+ });
+ assert_true(tried, 'VP8 video codec was found and tried');
+ }, `setCodecPreferences() with only VP8 should succeed`);
+
+ test(() => {
+ const pc = new RTCPeerConnection();
+ const transceiver = pc.addTransceiver('video');
+ const capabilities = RTCRtpSender.getCapabilities('video');
+ const { codecs } = capabilities;
+ // This test verifies that the mandatory H264 codec is present
+ // and can be set.
+ let tried = false;
+ codecs.forEach(codec => {
+ if (codec.mimeType.toLowerCase() === 'video/h264') {
+ transceiver.setCodecPreferences([codecs[0]]);
+ tried = true;
+ }
+ });
+ assert_true(tried, 'H264 video codec was found and tried');
+ }, `setCodecPreferences() with only H264 should succeed`);
+
+ async function getRTPMapLinesWithCodecAsFirst(firstCodec)
+ {
+ const capabilities = RTCRtpSender.getCapabilities('video').codecs;
+ capabilities.forEach((codec, idx) => {
+ if (codec.mimeType === firstCodec) {
+ capabilities.splice(idx, 1);
+ capabilities.unshift(codec);
+ }
+ });
+
+ const pc = new RTCPeerConnection();
+ const transceiver = pc.addTransceiver('video');
+ transceiver.setCodecPreferences(capabilities);
+ const offer = await pc.createOffer();
+
+ return offer.sdp.split('\r\n').filter(line => line.indexOf("a=rtpmap") === 0);
+ }
+
+ promise_test(async () => {
+ const lines = await getRTPMapLinesWithCodecAsFirst('video/H264');
+
+ assert_greater_than(lines.length, 1);
+ assert_true(lines[0].indexOf("H264") !== -1, "H264 should be the first codec");
+ }, `setCodecPreferences() should allow setting H264 as first codec`);
+
+ promise_test(async () => {
+ const lines = await getRTPMapLinesWithCodecAsFirst('video/VP8');
+
+ assert_greater_than(lines.length, 1);
+ assert_true(lines[0].indexOf("VP8") !== -1, "VP8 should be the first codec");
+ }, `setCodecPreferences() should allow setting VP8 as first codec`);
+
+ test((t) => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const transceiver = pc.addTransceiver('audio');
+ const capabilities = RTCRtpSender.getCapabilities('video');
+ assert_throws_dom('InvalidModificationError', () => transceiver.setCodecPreferences(capabilities.codecs));
+ }, `setCodecPreferences() on audio transceiver with codecs returned from getCapabilities('video') should throw InvalidModificationError`);
+
+ test((t) => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const transceiver = pc.addTransceiver('audio');
+ const codecs = [{
+ mimeType: 'data',
+ clockRate: 2000,
+ channels: 2,
+ sdpFmtpLine: '0-15'
+ }];
+
+ assert_throws_dom('InvalidModificationError', () => transceiver.setCodecPreferences(codecs));
+ }, `setCodecPreferences() with user defined codec with invalid mimeType should throw InvalidModificationError`);
+
+ test((t) => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const transceiver = pc.addTransceiver('audio');
+ const codecs = [{
+ mimeType: 'audio/piepiper',
+ clockRate: 2000,
+ channels: 2,
+ sdpFmtpLine: '0-15'
+ }];
+
+ assert_throws_dom('InvalidModificationError', () => transceiver.setCodecPreferences(codecs));
+ }, `setCodecPreferences() with user defined codec should throw InvalidModificationError`);
+
+ test((t) => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const transceiver = pc.addTransceiver('audio');
+ const capabilities = RTCRtpSender.getCapabilities('audio');
+ const codecs = [
+ ...capabilities.codecs,
+ {
+ mimeType: 'audio/piepiper',
+ clockRate: 2000,
+ channels: 2,
+ sdpFmtpLine: '0-15'
+ }];
+
+ assert_throws_dom('InvalidModificationError', () => transceiver.setCodecPreferences(codecs));
+ }, `setCodecPreferences() with user defined codec together with codecs returned from getCapabilities() should throw InvalidModificationError`);
+
+ test((t) => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const transceiver = pc.addTransceiver('audio');
+ const capabilities = RTCRtpSender.getCapabilities('audio');
+ const codecs = [capabilities.codecs[0]];
+ codecs[0].clockRate = codecs[0].clockRate / 2;
+
+ assert_throws_dom('InvalidModificationError', () => transceiver.setCodecPreferences(codecs));
+ }, `setCodecPreferences() with modified codec clock rate should throw InvalidModificationError`);
+
+ test((t) => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const transceiver = pc.addTransceiver('audio');
+ const capabilities = RTCRtpSender.getCapabilities('audio');
+ const codecs = [capabilities.codecs[0]];
+ codecs[0].channels = codecs[0].channels + 11;
+
+ assert_throws_dom('InvalidModificationError', () => transceiver.setCodecPreferences(codecs));
+ }, `setCodecPreferences() with modified codec channel count should throw InvalidModificationError`);
+
+ test((t) => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const transceiver = pc.addTransceiver('audio');
+ const capabilities = RTCRtpSender.getCapabilities('audio');
+ const codecs = [capabilities.codecs[0]];
+ codecs[0].sdpFmtpLine = "modifiedparameter=1";
+
+ assert_throws_dom('InvalidModificationError', () => transceiver.setCodecPreferences(codecs));
+ }, `setCodecPreferences() with modified codec parameters should throw InvalidModificationError`);
+
+ test((t) => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const transceiver = pc.addTransceiver('audio');
+ const capabilities = RTCRtpSender.getCapabilities('audio');
+
+ const { codecs } = capabilities;
+ assert_greater_than(codecs.length, 0,
+ 'Expect at least one codec available');
+
+ const [ codec ] = codecs;
+ const { channels=2 } = codec;
+ codec.channels = channels+1;
+
+ assert_throws_dom('InvalidModificationError', () => transceiver.setCodecPreferences(codecs));
+ }, `setCodecPreferences() with modified codecs returned from getCapabilities() should throw InvalidModificationError`);
+
+ promise_test(async (t) => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const transceiver = pc.addTransceiver('audio');
+ const {codecs} = RTCRtpSender.getCapabilities('audio');
+ // Reorder codecs, put PCMU/PCMA first.
+ let firstCodec;
+ let i;
+ for (i = 0; i < codecs.length; i++) {
+ const codec = codecs[i];
+ if (codec.mimeType === 'audio/PCMU' || codec.mimeType === 'audio/PCMA') {
+ codecs.splice(i, 1);
+ codecs.unshift(codec);
+ firstCodec = codec.mimeType.substr(6);
+ break;
+ }
+ }
+ assert_not_equals(firstCodec, undefined, 'PCMU or PCMA codec not found');
+ transceiver.setCodecPreferences(codecs);
+
+ const offer = await pc.createOffer();
+ const mediaSection = SDPUtils.getMediaSections(offer.sdp)[0];
+ const rtpParameters = SDPUtils.parseRtpParameters(mediaSection);
+ assert_equals(rtpParameters.codecs[0].name, firstCodec);
+ }, `setCodecPreferences() modifies the order of audio codecs in createOffer`);
+
+ promise_test(async (t) => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const transceiver = pc.addTransceiver('video');
+ const {codecs} = RTCRtpSender.getCapabilities('video');
+ // Reorder codecs, swap H264 and VP8.
+ let vp8 = -1;
+ let h264 = -1;
+ let firstCodec;
+ let i;
+ for (i = 0; i < codecs.length; i++) {
+ const codec = codecs[i];
+ if (codec.mimeType === 'video/VP8' && vp8 === -1) {
+ vp8 = i;
+ if (h264 !== -1) {
+ codecs[vp8] = codecs[h264];
+ codecs[h264] = codec;
+ firstCodec = 'VP8';
+ break;
+ }
+ }
+ if (codec.mimeType === 'video/H264' && h264 === -1) {
+ h264 = i;
+ if (vp8 !== -1) {
+ codecs[h264] = codecs[vp8];
+ codecs[vp8] = codec;
+ firstCodec = 'H264';
+ break;
+ }
+ }
+ }
+ assert_not_equals(firstCodec, undefined, 'VP8 and H264 codecs not found');
+ transceiver.setCodecPreferences(codecs);
+
+ const offer = await pc.createOffer();
+ const mediaSection = SDPUtils.getMediaSections(offer.sdp)[0];
+ const rtpParameters = SDPUtils.parseRtpParameters(mediaSection);
+ assert_equals(rtpParameters.codecs[0].name, firstCodec);
+ }, `setCodecPreferences() modifies the order of video codecs in createOffer`);
+
+ </script>
diff --git a/testing/web-platform/tests/webrtc/RTCRtpTransceiver-stop.html b/testing/web-platform/tests/webrtc/RTCRtpTransceiver-stop.html
new file mode 100644
index 0000000000..766b34d7b1
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCRtpTransceiver-stop.html
@@ -0,0 +1,155 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCRtpTransceiver.prototype.stop</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="RTCPeerConnection-helper.js"></script>
+<script>
+// FIXME: Add a test adding a transceiver, stopping it and trying to create an empty offer.
+
+promise_test(async (t)=> {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+
+ pc1.addTransceiver("audio", { direction: "sendonly" });
+ pc1.addTransceiver("video");
+ pc1.getTransceivers()[0].stop();
+
+ const offer = await pc1.createOffer();
+
+ assert_false(offer.sdp.includes("m=audio"), "offer should not contain an audio m-section");
+ assert_true(offer.sdp.includes("m=video"), "offer should contain a video m-section");
+}, "A transceiver added and stopped before the initial offer generation should not trigger an offer m-section generation");
+
+promise_test(async (t)=> {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+
+ pc1.addTransceiver("audio", { direction: "sendonly" });
+ pc1.addTransceiver("video");
+ assert_equals(null, pc1.getTransceivers()[1].receiver.transport);
+
+ pc1.getTransceivers()[1].stop();
+ assert_equals(pc1.getTransceivers()[1].receiver.transport, null);
+}, "A transceiver added and stopped should not crash when getting receiver's transport");
+
+promise_test(async (t)=> {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+
+ pc1.addTransceiver("audio");
+
+ await exchangeOfferAnswer(pc1, pc2);
+
+ pc1.addTransceiver("video");
+
+ pc1.getTransceivers()[0].stop();
+ pc1.getTransceivers()[1].stop();
+
+ const offer = await pc1.createOffer();
+
+ assert_true(offer.sdp.includes("m=audio"), "offer should contain an audio m-section");
+ assert_true(offer.sdp.includes("m=audio 0"), "The audio m-section should be rejected");
+
+ assert_false(offer.sdp.includes("m=video"), "offer should not contain a video m-section");
+}, "During renegotiation, adding and stopping a transceiver should not trigger a renegotiated offer m-section generation");
+
+promise_test(async (t)=> {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+
+ pc1.addTransceiver("audio");
+
+ await exchangeOfferAnswer(pc1, pc2);
+
+ pc1.getTransceivers()[0].direction = "sendonly";
+ pc1.getTransceivers()[0].stop();
+
+ const offer = await pc1.createOffer();
+
+ assert_true(offer.sdp.includes("a=inactive"), "The audio m-section should be inactive");
+}, "A stopped sendonly transceiver should generate an inactive m-section in the offer");
+
+promise_test(async (t)=> {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+
+ pc1.addTransceiver("audio");
+
+ await exchangeOfferAnswer(pc1, pc2);
+
+ pc1.getTransceivers()[0].direction = "inactive";
+ pc1.getTransceivers()[0].stop();
+
+ const offer = await pc1.createOffer();
+
+ assert_true(offer.sdp.includes("a=inactive"), "The audio m-section should be inactive");
+}, "A stopped inactive transceiver should generate an inactive m-section in the offer");
+
+promise_test(async (t) => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+ pc1.addTransceiver("audio");
+ await exchangeOfferAnswer(pc1, pc2);
+ pc1.getTransceivers()[0].stop();
+ await exchangeOfferAnswer(pc1, pc2);
+ await pc1.setLocalDescription(await pc1.createOffer());
+}, 'If a transceiver is stopped locally, setting a locally generated answer should still work');
+
+promise_test(async (t) => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+ pc1.addTransceiver("audio");
+ await exchangeOfferAnswer(pc1, pc2);
+ pc2.getTransceivers()[0].stop();
+ await exchangeOfferAnswer(pc2, pc1);
+ await pc1.setLocalDescription(await pc1.createOffer());
+}, 'If a transceiver is stopped remotely, setting a locally generated answer should still work');
+
+promise_test(async (t) => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+ pc1.addTransceiver("audio");
+ await exchangeOfferAnswer(pc1, pc2);
+ assert_equals(pc1.getTransceivers().length, 1);
+ assert_equals(pc2.getTransceivers().length, 1);
+ pc1.getTransceivers()[0].stop();
+ await exchangeOfferAnswer(pc1, pc2);
+ assert_equals(pc1.getTransceivers().length, 0);
+ assert_equals(pc2.getTransceivers().length, 0);
+ assert_equals(pc1.getSenders().length, 0, 'caller senders');
+ assert_equals(pc1.getReceivers().length, 0, 'caller receivers');
+ assert_equals(pc2.getSenders().length, 0, 'callee senders');
+ assert_equals(pc2.getReceivers().length, 0, 'callee receivers');
+}, 'If a transceiver is stopped, transceivers, senders and receivers should disappear after offer/answer');
+
+promise_test(async (t) => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+ pc1.addTransceiver("audio");
+ await exchangeOfferAnswer(pc1, pc2);
+ assert_equals(pc1.getTransceivers().length, 1);
+ assert_equals(pc2.getTransceivers().length, 1);
+ pc1Transceiver = pc1.getTransceivers()[0];
+ pc2Transceiver = pc2.getTransceivers()[0];
+ pc1.getTransceivers()[0].stop();
+ await exchangeOfferAnswer(pc1, pc2);
+ assert_equals(pc1Transceiver.direction, 'stopped');
+ assert_equals(pc2Transceiver.direction, 'stopped');
+}, 'If a transceiver is stopped, transceivers should end up in state stopped');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCRtpTransceiver-stopping.https.html b/testing/web-platform/tests/webrtc/RTCRtpTransceiver-stopping.https.html
new file mode 100644
index 0000000000..16be25fe13
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCRtpTransceiver-stopping.https.html
@@ -0,0 +1,217 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+'use strict';
+
+['audio', 'video'].forEach((kind) => {
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const transceiver = pc1.addTransceiver(kind);
+
+ // Complete O/A exchange such that the transceiver gets associated.
+ await pc1.setLocalDescription();
+ await pc2.setRemoteDescription(pc1.localDescription);
+ await pc2.setLocalDescription();
+ await pc1.setRemoteDescription(pc2.localDescription);
+ assert_not_equals(transceiver.mid, null, 'mid before stop()');
+ assert_not_equals(transceiver.direction, 'stopped',
+ 'direction before stop()');
+ assert_not_equals(transceiver.currentDirection, 'stopped',
+ 'currentDirection before stop()');
+
+ // Stop makes it stopping, but not stopped.
+ transceiver.stop();
+ assert_not_equals(transceiver.mid, null, 'mid after stop()');
+ assert_equals(transceiver.direction, 'stopped', 'direction after stop()');
+ assert_not_equals(transceiver.currentDirection, 'stopped',
+ 'currentDirection after stop()');
+
+ // Negotiating makes it stopped.
+ await pc1.setLocalDescription();
+ await pc2.setRemoteDescription(pc1.localDescription);
+ await pc2.setLocalDescription();
+ await pc1.setRemoteDescription(pc2.localDescription);
+ assert_equals(transceiver.mid, null, 'mid after negotiation');
+ assert_equals(transceiver.direction, 'stopped',
+ 'direction after negotiation');
+ assert_equals(transceiver.currentDirection, 'stopped',
+ 'currentDirection after negotiation');
+ }, `[${kind}] Locally stopped transceiver goes from stopping to stopped`);
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const transceiver = pc.addTransceiver(kind);
+ const trackEnded = new Promise(
+ r => { transceiver.receiver.track.onended = () => { r(); } });
+ assert_equals(transceiver.receiver.track.readyState, 'live');
+ transceiver.stop();
+ // Stopping triggers ending the track, but this happens asynchronously.
+ assert_equals(transceiver.receiver.track.readyState, 'live');
+ await trackEnded;
+ assert_equals(transceiver.receiver.track.readyState, 'ended');
+ }, `[${kind}] Locally stopping a transceiver ends the track`);
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const pc1Transceiver = pc1.addTransceiver(kind);
+ await pc1.setLocalDescription();
+ await pc2.setRemoteDescription(pc1.localDescription);
+ await pc2.setLocalDescription();
+ await pc1.setRemoteDescription(pc2.localDescription);
+ const [pc2Transceiver] = pc2.getTransceivers();
+
+ pc1Transceiver.stop();
+
+ await pc1.setLocalDescription();
+ assert_equals(pc2Transceiver.receiver.track.readyState, 'live');
+ // Applying the remote offer immediately ends the track, we don't need to
+ // create or apply an answer.
+ await pc2.setRemoteDescription(pc1.localDescription);
+ assert_equals(pc2Transceiver.receiver.track.readyState, 'ended');
+ }, `[${kind}] Remotely stopping a transceiver ends the track`);
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const pc1Transceiver = pc1.addTransceiver(kind);
+
+ // Complete O/A exchange such that the transceiver gets associated.
+ await pc1.setLocalDescription();
+ await pc2.setRemoteDescription(pc1.localDescription);
+ await pc2.setLocalDescription();
+ await pc1.setRemoteDescription(pc2.localDescription);
+ const [pc2Transceiver] = pc2.getTransceivers();
+ assert_not_equals(pc2Transceiver.mid, null, 'mid before stop()');
+ assert_not_equals(pc2Transceiver.direction, 'stopped',
+ 'direction before stop()');
+ assert_not_equals(pc2Transceiver.currentDirection, 'stopped',
+ 'currentDirection before stop()');
+
+ // Make the remote transceiver stopped.
+ pc1Transceiver.stop();
+
+ // Negotiating makes it stopped.
+ assert_equals(pc2.getTransceivers().length, 1);
+ await pc1.setLocalDescription();
+ await pc2.setRemoteDescription(pc1.localDescription);
+ // As soon as the remote offer is set, the transceiver is stopped but it is
+ // not disassociated or removed until setting the local answer.
+ assert_equals(pc2.getTransceivers().length, 1);
+ assert_not_equals(pc2Transceiver.mid, null, 'mid during negotiation');
+ assert_equals(pc2Transceiver.direction, 'stopped',
+ 'direction during negotiation');
+ assert_equals(pc2Transceiver.currentDirection, 'stopped',
+ 'currentDirection during negotiation');
+ await pc2.setLocalDescription();
+ assert_equals(pc2.getTransceivers().length, 0);
+ assert_equals(pc2Transceiver.mid, null, 'mid after negotiation');
+ assert_equals(pc2Transceiver.direction, 'stopped',
+ 'direction after negotiation');
+ assert_equals(pc2Transceiver.currentDirection, 'stopped',
+ 'currentDirection after negotiation');
+ }, `[${kind}] Remotely stopped transceiver goes directly to stopped`);
+
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const transceiver = pc.addTransceiver(kind);
+
+ // Rollback does not end the track, because the transceiver is not removed.
+ await pc.setLocalDescription();
+ await pc.setLocalDescription({type:'rollback'});
+ assert_equals(transceiver.receiver.track.readyState, 'live');
+ }, `[${kind}] Rollback when transceiver is not removed does not end track`);
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const pc1Transceiver = pc1.addTransceiver(kind);
+
+ // Start negotiation, causing a transceiver to be created.
+ await pc1.setLocalDescription();
+ await pc2.setRemoteDescription(pc1.localDescription);
+ const [pc2Transceiver] = pc2.getTransceivers();
+
+ // Rollback such that the transceiver is removed.
+ await pc2.setLocalDescription({type:'rollback'});
+ assert_equals(pc2.getTransceivers().length, 0);
+ assert_equals(pc2Transceiver.receiver.track.readyState, 'ended');
+ }, `[${kind}] Rollback when removing transceiver does end the track`);
+
+ // Same test as above but looking at direction and currentDirection.
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const pc1Transceiver = pc1.addTransceiver(kind);
+
+ // Start negotiation, causing a transceiver to be created.
+ await pc1.setLocalDescription();
+ await pc2.setRemoteDescription(pc1.localDescription);
+ const [pc2Transceiver] = pc2.getTransceivers();
+
+ // Rollback such that the transceiver is removed.
+ await pc2.setLocalDescription({type:'rollback'});
+ assert_equals(pc2.getTransceivers().length, 0);
+ // The removed transceiver is stopped.
+ assert_equals(pc2Transceiver.currentDirection, 'stopped',
+ 'currentDirection indicate stopped');
+ // A stopped transceiver is necessarily also stopping.
+ assert_equals(pc2Transceiver.direction, 'stopped',
+ 'direction indicate stopping');
+ // A stopped transceiver has no mid.
+ assert_equals(pc2Transceiver.mid, null, 'not associated');
+ }, `[${kind}] Rollback when removing transceiver makes it stopped`);
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const constraints = {};
+ constraints[kind] = true;
+ const stream = await navigator.mediaDevices.getUserMedia(constraints);
+ const [track] = stream.getTracks();
+
+ pc1.addTrack(track);
+ pc2.addTrack(track);
+ const transceiver = pc2.getTransceivers()[0];
+
+ const ontrackEvent = new Promise(r => {
+ pc2.ontrack = e => r(e.track);
+ });
+
+ // Simulate glare: both peer connections set local offers.
+ await pc1.setLocalDescription();
+ await pc2.setLocalDescription();
+ // Set remote offer, which implicitly rolls back the local offer. Because
+ // `transceiver` is an addTrack-transceiver, it should get repurposed.
+ await pc2.setRemoteDescription(pc1.localDescription);
+ assert_equals(transceiver.receiver.track.readyState, 'live');
+ // Sanity check: the track should still be live when ontrack fires.
+ assert_equals((await ontrackEvent).readyState, 'live');
+ }, `[${kind}] Glare when transceiver is not removed does not end track`);
+});
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCRtpTransceiver.https.html b/testing/web-platform/tests/webrtc/RTCRtpTransceiver.https.html
new file mode 100644
index 0000000000..943550d4b7
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCRtpTransceiver.https.html
@@ -0,0 +1,2297 @@
+<!doctype html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<title>RTCRtpTransceiver</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="RTCPeerConnection-helper.js"></script>
+<script>
+ 'use strict';
+
+ const checkThrows = async (func, exceptionName, description) => {
+ try {
+ await func();
+ assert_true(false, description + " throws " + exceptionName);
+ } catch (e) {
+ assert_equals(e.name, exceptionName, description + " throws " + exceptionName);
+ }
+ };
+
+ const stopTracks = (...streams) => {
+ streams.forEach(stream => stream.getTracks().forEach(track => track.stop()));
+ };
+
+ const collectEvents = (target, name, check) => {
+ const events = [];
+ const handler = e => {
+ check(e);
+ events.push(e);
+ };
+
+ target.addEventListener(name, handler);
+
+ const finishCollecting = () => {
+ target.removeEventListener(name, handler);
+ return events;
+ };
+
+ return {finish: finishCollecting};
+ };
+
+ const collectAddTrackEvents = stream => {
+ const checkEvent = e => {
+ assert_true(e.track instanceof MediaStreamTrack, "Track is set on event");
+ assert_true(stream.getTracks().includes(e.track),
+ "track in addtrack event is in the stream");
+ };
+ return collectEvents(stream, "addtrack", checkEvent);
+ };
+
+ const collectRemoveTrackEvents = stream => {
+ const checkEvent = e => {
+ assert_true(e.track instanceof MediaStreamTrack, "Track is set on event");
+ assert_true(!stream.getTracks().includes(e.track),
+ "track in removetrack event is not in the stream");
+ };
+ return collectEvents(stream, "removetrack", checkEvent);
+ };
+
+ const collectTrackEvents = pc => {
+ const checkEvent = e => {
+ assert_true(e.track instanceof MediaStreamTrack, "Track is set on event");
+ assert_true(e.receiver instanceof RTCRtpReceiver, "Receiver is set on event");
+ assert_true(e.transceiver instanceof RTCRtpTransceiver, "Transceiver is set on event");
+ assert_true(Array.isArray(e.streams), "Streams is set on event");
+ e.streams.forEach(stream => {
+ assert_true(stream.getTracks().includes(e.track),
+ "Each stream in event contains the track");
+ });
+ assert_equals(e.receiver, e.transceiver.receiver,
+ "Receiver belongs to transceiver");
+ assert_equals(e.track, e.receiver.track,
+ "Track belongs to receiver");
+ };
+
+ return collectEvents(pc, "track", checkEvent);
+ };
+
+ const setRemoteDescriptionReturnTrackEvents = async (pc, desc) => {
+ const trackEventCollector = collectTrackEvents(pc);
+ await pc.setRemoteDescription(desc);
+ return trackEventCollector.finish();
+ };
+
+ const offerAnswer = async (offerer, answerer) => {
+ const offer = await offerer.createOffer();
+ await answerer.setRemoteDescription(offer);
+ await offerer.setLocalDescription(offer);
+ const answer = await answerer.createAnswer();
+ await offerer.setRemoteDescription(answer);
+ await answerer.setLocalDescription(answer);
+ };
+
+ const trickle = (t, pc1, pc2) => {
+ pc1.onicecandidate = t.step_func(async e => {
+ try {
+ await pc2.addIceCandidate(e.candidate);
+ } catch (e) {
+ assert_true(false, "addIceCandidate threw error: " + e.name);
+ }
+ });
+ };
+
+ const iceConnected = pc => {
+ return new Promise((resolve, reject) => {
+ const iceCheck = () => {
+ if (pc.iceConnectionState == "connected") {
+ assert_true(true, "ICE connected");
+ resolve();
+ }
+
+ if (pc.iceConnectionState == "failed") {
+ assert_true(false, "ICE failed");
+ reject();
+ }
+ };
+
+ iceCheck();
+ pc.oniceconnectionstatechange = iceCheck;
+ });
+ };
+
+ const negotiationNeeded = pc => {
+ return new Promise(resolve => pc.onnegotiationneeded = resolve);
+ };
+
+ const countEvents = (target, name) => {
+ const result = {count: 0};
+ target.addEventListener(name, e => result.count++);
+ return result;
+ };
+
+ const gotMuteEvent = async track => {
+ await new Promise(r => track.addEventListener("mute", r, {once: true}));
+
+ assert_true(track.muted, "track should be muted after onmute");
+ };
+
+ const gotUnmuteEvent = async track => {
+ await new Promise(r => track.addEventListener("unmute", r, {once: true}));
+
+ assert_true(!track.muted, "track should not be muted after onunmute");
+ };
+
+ // comparable() - produces copy of object that is JSON comparable.
+ // o = original object (required)
+ // t = template of what to examine. Useful if o is non-enumerable (optional)
+
+ const comparable = (o, t = o) => {
+ if (typeof o != 'object' || !o) {
+ return o;
+ }
+ if (Array.isArray(t) && Array.isArray(o)) {
+ return o.map((n, i) => comparable(n, t[i]));
+ }
+ return Object.keys(t).sort()
+ .reduce((r, key) => (r[key] = comparable(o[key], t[key]), r), {});
+ };
+
+ const stripKeyQuotes = s => s.replace(/"(\w+)":/g, "$1:");
+
+ const hasProps = (observed, expected) => {
+ const observable = comparable(observed, expected);
+ assert_equals(stripKeyQuotes(JSON.stringify(observable)),
+ stripKeyQuotes(JSON.stringify(comparable(expected))));
+ };
+
+ const hasPropsAndUniqueMids = (observed, expected) => {
+ hasProps(observed, expected);
+
+ const mids = [];
+ observed.forEach((transceiver, i) => {
+ if (!("mid" in expected[i])) {
+ assert_not_equals(transceiver.mid, null);
+ assert_equals(typeof transceiver.mid, "string");
+ }
+ if (transceiver.mid) {
+ assert_false(mids.includes(transceiver.mid), "mid must be unique");
+ mids.push(transceiver.mid);
+ }
+ });
+ };
+
+ const checkAddTransceiverNoTrack = async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ hasProps(pc.getTransceivers(), []);
+
+ pc.addTransceiver("audio");
+ pc.addTransceiver("video");
+
+ hasProps(pc.getTransceivers(),
+ [
+ {
+ receiver: {track: {kind: "audio", readyState: "live", muted: true}},
+ sender: {track: null},
+ direction: "sendrecv",
+ mid: null,
+ currentDirection: null,
+ },
+ {
+ receiver: {track: {kind: "video", readyState: "live", muted: true}},
+ sender: {track: null},
+ direction: "sendrecv",
+ mid: null,
+ currentDirection: null,
+ }
+ ]);
+ };
+
+ const checkAddTransceiverWithTrack = async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const stream = await getNoiseStream({audio: true, video: true});
+ t.add_cleanup(() => stopTracks(stream));
+ const audio = stream.getAudioTracks()[0];
+ const video = stream.getVideoTracks()[0];
+
+ pc.addTransceiver(audio);
+ pc.addTransceiver(video);
+
+ hasProps(pc.getTransceivers(),
+ [
+ {
+ receiver: {track: {kind: "audio"}},
+ sender: {track: audio},
+ direction: "sendrecv",
+ mid: null,
+ currentDirection: null,
+ },
+ {
+ receiver: {track: {kind: "video"}},
+ sender: {track: video},
+ direction: "sendrecv",
+ mid: null,
+ currentDirection: null,
+ }
+ ]);
+ };
+
+ const checkAddTransceiverWithAddTrack = async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const stream = await getNoiseStream({audio: true, video: true});
+ t.add_cleanup(() => stopTracks(stream));
+ const audio = stream.getAudioTracks()[0];
+ const video = stream.getVideoTracks()[0];
+
+ pc.addTrack(audio, stream);
+ pc.addTrack(video, stream);
+
+ hasProps(pc.getTransceivers(),
+ [
+ {
+ receiver: {track: {kind: "audio"}},
+ sender: {track: audio},
+ direction: "sendrecv",
+ mid: null,
+ currentDirection: null,
+ },
+ {
+ receiver: {track: {kind: "video"}},
+ sender: {track: video},
+ direction: "sendrecv",
+ mid: null,
+ currentDirection: null,
+ }
+ ]);
+ };
+
+ const checkAddTransceiverWithDirection = async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ pc.addTransceiver("audio", {direction: "recvonly"});
+ pc.addTransceiver("video", {direction: "recvonly"});
+
+ hasProps(pc.getTransceivers(),
+ [
+ {
+ receiver: {track: {kind: "audio"}},
+ sender: {track: null},
+ direction: "recvonly",
+ mid: null,
+ currentDirection: null,
+ },
+ {
+ receiver: {track: {kind: "video"}},
+ sender: {track: null},
+ direction: "recvonly",
+ mid: null,
+ currentDirection: null,
+ }
+ ]);
+ };
+
+ const checkAddTransceiverWithSetRemoteOfferSending = async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+
+ const stream = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stopTracks(stream));
+ const track = stream.getAudioTracks()[0];
+ pc1.addTransceiver(track, {streams: [stream]});
+
+ const offer = await pc1.createOffer();
+
+ const trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, offer);
+ hasProps(trackEvents,
+ [
+ {
+ track: pc2.getTransceivers()[0].receiver.track,
+ streams: [{id: stream.id}]
+ }
+ ]);
+
+
+ hasPropsAndUniqueMids(pc2.getTransceivers(),
+ [
+ {
+ receiver: {track: {kind: "audio"}},
+ sender: {track: null},
+ direction: "recvonly",
+ currentDirection: null,
+ }
+ ]);
+ };
+
+ const checkAddTransceiverWithSetRemoteOfferNoSend = async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+
+ const stream = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stopTracks(stream));
+ const track = stream.getAudioTracks()[0];
+ pc1.addTransceiver(track);
+ pc1.getTransceivers()[0].direction = "recvonly";
+
+ const offer = await pc1.createOffer();
+ const trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, offer);
+ hasProps(trackEvents, []);
+
+ hasPropsAndUniqueMids(pc2.getTransceivers(),
+ [
+ {
+ receiver: {track: {kind: "audio"}},
+ sender: {track: null},
+ // rtcweb-jsep says this is recvonly, w3c-webrtc does not...
+ direction: "recvonly",
+ currentDirection: null,
+ }
+ ]);
+ };
+
+ const checkAddTransceiverBadKind = async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ try {
+ pc.addTransceiver("foo");
+ assert_true(false, 'addTransceiver("foo") throws');
+ }
+ catch (e) {
+ if (e instanceof TypeError) {
+ assert_true(true, 'addTransceiver("foo") throws a TypeError');
+ } else {
+ assert_true(false, 'addTransceiver("foo") throws a TypeError');
+ }
+ }
+
+ hasProps(pc.getTransceivers(), []);
+ };
+
+ const checkNoMidOffer = async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+
+ const stream = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stopTracks(stream));
+ const track = stream.getAudioTracks()[0];
+ pc1.addTrack(track, stream);
+
+ const offer = await pc1.createOffer();
+ await pc1.setLocalDescription(offer);
+
+ // Remove mid attr
+ offer.sdp = offer.sdp.replace("a=mid:", "a=unknownattr:");
+ offer.sdp = offer.sdp.replace("a=group:", "a=unknownattr:");
+ await pc2.setRemoteDescription(offer);
+
+ hasPropsAndUniqueMids(pc2.getTransceivers(),
+ [
+ {
+ receiver: {track: {kind: "audio"}},
+ sender: {track: null},
+ direction: "recvonly",
+ currentDirection: null,
+ }
+ ]);
+
+ const answer = await pc2.createAnswer();
+ await pc2.setLocalDescription(answer);
+ await pc1.setRemoteDescription(answer);
+ };
+
+ const checkNoMidAnswer = async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+
+ const stream = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stopTracks(stream));
+ const track = stream.getAudioTracks()[0];
+ pc1.addTrack(track, stream);
+
+ const offer = await pc1.createOffer();
+ await pc1.setLocalDescription(offer);
+ await pc2.setRemoteDescription(offer);
+
+ hasPropsAndUniqueMids(pc1.getTransceivers(),
+ [
+ {
+ receiver: {track: {kind: "audio"}},
+ sender: {track: {kind: "audio"}},
+ direction: "sendrecv",
+ currentDirection: null,
+ }
+ ]);
+
+ const lastMid = pc1.getTransceivers()[0].mid;
+
+ let answer = await pc2.createAnswer();
+ // Remove mid attr
+ answer.sdp = answer.sdp.replace("a=mid:", "a=unknownattr:");
+ // Remove group attr also
+ answer.sdp = answer.sdp.replace("a=group:", "a=unknownattr:");
+ await pc1.setRemoteDescription(answer);
+
+ hasProps(pc1.getTransceivers(),
+ [
+ {
+ receiver: {track: {kind: "audio"}},
+ sender: {track: {kind: "audio"}},
+ direction: "sendrecv",
+ currentDirection: "sendonly",
+ mid: lastMid
+ }
+ ]);
+
+ const reoffer = await pc1.createOffer();
+ await pc1.setLocalDescription(reoffer);
+ hasProps(pc1.getTransceivers(),
+ [
+ {
+ receiver: {track: {kind: "audio"}},
+ sender: {track: {kind: "audio"}},
+ direction: "sendrecv",
+ currentDirection: "sendonly",
+ mid: lastMid
+ }
+ ]);
+ };
+
+ const checkAddTransceiverNoTrackDoesntPair = async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+
+ pc1.addTransceiver("audio");
+ pc2.addTransceiver("audio");
+
+ const offer = await pc1.createOffer();
+ const trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, offer);
+ hasProps(trackEvents,
+ [
+ {
+ track: pc2.getTransceivers()[1].receiver.track,
+ streams: []
+ }
+ ]);
+
+ hasPropsAndUniqueMids(pc2.getTransceivers(),
+ [
+ {mid: null}, // no addTrack magic, doesn't auto-pair
+ {} // Created by SRD
+ ]);
+ };
+
+ const checkAddTransceiverWithTrackDoesntPair = async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+ pc1.addTransceiver("audio");
+
+ const stream = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stopTracks(stream));
+ const track = stream.getAudioTracks()[0];
+ pc2.addTransceiver(track);
+
+ const offer = await pc1.createOffer();
+ const trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, offer);
+ hasProps(trackEvents,
+ [
+ {
+ track: pc2.getTransceivers()[1].receiver.track,
+ streams: []
+ }
+ ]);
+
+ hasPropsAndUniqueMids(pc2.getTransceivers(),
+ [
+ {mid: null, sender: {track}},
+ {sender: {track: null}} // Created by SRD
+ ]);
+ };
+
+ const checkAddTransceiverThenReplaceTrackDoesntPair = async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+ pc1.addTransceiver("audio");
+ pc2.addTransceiver("audio");
+
+ const stream = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stopTracks(stream));
+ const track = stream.getAudioTracks()[0];
+ await pc2.getTransceivers()[0].sender.replaceTrack(track);
+
+ const offer = await pc1.createOffer();
+ const trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, offer);
+ hasProps(trackEvents,
+ [
+ {
+ track: pc2.getTransceivers()[1].receiver.track,
+ streams: []
+ }
+ ]);
+
+ hasPropsAndUniqueMids(pc2.getTransceivers(),
+ [
+ {mid: null, sender: {track}},
+ {sender: {track: null}} // Created by SRD
+ ]);
+ };
+
+ const checkAddTransceiverThenAddTrackPairs = async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+ pc1.addTransceiver("audio");
+ pc2.addTransceiver("audio");
+
+ const stream = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stopTracks(stream));
+ const track = stream.getAudioTracks()[0];
+ pc2.addTrack(track, stream);
+
+ const offer = await pc1.createOffer();
+ const trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, offer);
+ hasProps(trackEvents,
+ [
+ {
+ track: pc2.getTransceivers()[0].receiver.track,
+ streams: []
+ }
+ ]);
+
+ // addTransceiver-transceivers cannot attach to a remote offers, so a second
+ // transceiver is created and associated whilst the first transceiver
+ // remains unassociated.
+ assert_equals(pc2.getTransceivers()[0].mid, null);
+ assert_not_equals(pc2.getTransceivers()[1].mid, null);
+ };
+
+ const checkAddTrackPairs = async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+ pc1.addTransceiver("audio");
+
+ const stream = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stopTracks(stream));
+ const track = stream.getAudioTracks()[0];
+ pc2.addTrack(track, stream);
+
+ const offer = await pc1.createOffer();
+ const trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, offer);
+ hasProps(trackEvents,
+ [
+ {
+ track: pc2.getTransceivers()[0].receiver.track,
+ streams: []
+ }
+ ]);
+
+ hasPropsAndUniqueMids(pc2.getTransceivers(),
+ [
+ {sender: {track}}
+ ]);
+ };
+
+ const checkReplaceTrackNullDoesntPreventPairing = async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+ pc1.addTransceiver("audio");
+
+ const stream = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stopTracks(stream));
+ const track = stream.getAudioTracks()[0];
+ pc2.addTrack(track, stream);
+ await pc2.getTransceivers()[0].sender.replaceTrack(null);
+
+ const offer = await pc1.createOffer();
+ const trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, offer);
+ hasProps(trackEvents,
+ [
+ {
+ track: pc2.getTransceivers()[0].receiver.track,
+ streams: []
+ }
+ ]);
+
+ hasPropsAndUniqueMids(pc2.getTransceivers(),
+ [
+ {sender: {track: null}}
+ ]);
+ };
+
+ const checkRemoveAndReadd = async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+ const stream = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stopTracks(stream));
+ const track = stream.getAudioTracks()[0];
+ pc1.addTrack(track, stream);
+
+ await offerAnswer(pc1, pc2);
+
+ pc1.removeTrack(pc1.getSenders()[0]);
+ pc1.addTrack(track, stream);
+
+ hasProps(pc1.getTransceivers(),
+ [
+ {
+ sender: {track: null},
+ direction: "recvonly"
+ },
+ {
+ sender: {track},
+ direction: "sendrecv"
+ }
+ ]);
+
+ // pc1 is offerer
+ await offerAnswer(pc1, pc2);
+
+ hasProps(pc2.getTransceivers(),
+ [
+ {currentDirection: "inactive"},
+ {currentDirection: "recvonly"}
+ ]);
+
+ pc1.removeTrack(pc1.getSenders()[1]);
+ pc1.addTrack(track, stream);
+
+ hasProps(pc1.getTransceivers(),
+ [
+ {
+ sender: {track: null},
+ direction: "recvonly"
+ },
+ {
+ sender: {track: null},
+ direction: "recvonly"
+ },
+ {
+ sender: {track},
+ direction: "sendrecv"
+ }
+ ]);
+
+ // pc1 is answerer. We need to create a new transceiver so pc1 will have
+ // something to attach the re-added track to
+ pc2.addTransceiver("audio");
+
+ await offerAnswer(pc2, pc1);
+
+ hasProps(pc2.getTransceivers(),
+ [
+ {currentDirection: "inactive"},
+ {currentDirection: "inactive"},
+ {currentDirection: "sendrecv"}
+ ]);
+ };
+
+ const checkAddTrackExistingTransceiverThenRemove = async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ pc.addTransceiver("audio");
+ const stream = await getNoiseStream({audio: true});
+ const audio = stream.getAudioTracks()[0];
+ let sender = pc.addTrack(audio, stream);
+ pc.removeTrack(sender);
+
+ // Cause transceiver to be associated
+ await pc.setLocalDescription(await pc.createOffer());
+
+ // Make sure add/remove works still
+ sender = pc.addTrack(audio, stream);
+ pc.removeTrack(sender);
+
+ stopTracks(stream);
+ };
+
+ const checkRemoveTrackNegotiation = async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+ const stream = await getNoiseStream({audio: true, video: true});
+ t.add_cleanup(() => stopTracks(stream));
+ const audio = stream.getAudioTracks()[0];
+ pc1.addTrack(audio, stream);
+ const video = stream.getVideoTracks()[0];
+ pc1.addTrack(video, stream);
+ // We want both a sendrecv and sendonly transceiver to test that the
+ // appropriate direction changes happen.
+ pc1.getTransceivers()[1].direction = "sendonly";
+
+ let offer = await pc1.createOffer();
+
+ // Get a reference to the stream
+ let trackEventCollector = collectTrackEvents(pc2);
+ await pc2.setRemoteDescription(offer);
+ let pc2TrackEvents = trackEventCollector.finish();
+ hasProps(pc2TrackEvents,
+ [
+ {streams: [{id: stream.id}]},
+ {streams: [{id: stream.id}]}
+ ]);
+ const receiveStream = pc2TrackEvents[0].streams[0];
+
+ // Verify that rollback causes onremovetrack to fire for the added tracks
+ let removetrackEventCollector = collectRemoveTrackEvents(receiveStream);
+ await pc2.setRemoteDescription({type: "rollback"});
+ let removedtracks = removetrackEventCollector.finish().map(e => e.track);
+ assert_equals(removedtracks.length, 2,
+ "Rollback should have removed two tracks");
+ assert_true(removedtracks.includes(pc2TrackEvents[0].track),
+ "First track should be removed");
+ assert_true(removedtracks.includes(pc2TrackEvents[1].track),
+ "Second track should be removed");
+
+ offer = await pc1.createOffer();
+
+ let addtrackEventCollector = collectAddTrackEvents(receiveStream);
+ trackEventCollector = collectTrackEvents(pc2);
+ await pc2.setRemoteDescription(offer);
+ pc2TrackEvents = trackEventCollector.finish();
+ let addedtracks = addtrackEventCollector.finish().map(e => e.track);
+ assert_equals(addedtracks.length, 2,
+ "pc2.setRemoteDescription(offer) should've added 2 tracks to receive stream");
+ assert_true(addedtracks.includes(pc2TrackEvents[0].track),
+ "First track should be added");
+ assert_true(addedtracks.includes(pc2TrackEvents[1].track),
+ "Second track should be added");
+
+ await pc1.setLocalDescription(offer);
+ let answer = await pc2.createAnswer();
+ await pc1.setRemoteDescription(answer);
+ await pc2.setLocalDescription(answer);
+ pc1.removeTrack(pc1.getSenders()[0]);
+
+ hasProps(pc1.getSenders(),
+ [
+ {track: null},
+ {track: video}
+ ]);
+
+ hasProps(pc1.getTransceivers(),
+ [
+ {
+ sender: {track: null},
+ direction: "recvonly"
+ },
+ {
+ sender: {track: video},
+ direction: "sendonly"
+ }
+ ]);
+
+ await negotiationNeeded(pc1);
+
+ pc1.removeTrack(pc1.getSenders()[1]);
+
+ hasProps(pc1.getSenders(),
+ [
+ {track: null},
+ {track: null}
+ ]);
+
+ hasProps(pc1.getTransceivers(),
+ [
+ {
+ sender: {track: null},
+ direction: "recvonly"
+ },
+ {
+ sender: {track: null},
+ direction: "inactive"
+ }
+ ]);
+
+ // pc1 as offerer
+ offer = await pc1.createOffer();
+
+ removetrackEventCollector = collectRemoveTrackEvents(receiveStream);
+ await pc2.setRemoteDescription(offer);
+ removedtracks = removetrackEventCollector.finish().map(e => e.track);
+ assert_equals(removedtracks.length, 2, "Should have two removed tracks");
+ assert_true(removedtracks.includes(pc2TrackEvents[0].track),
+ "First track should be removed");
+ assert_true(removedtracks.includes(pc2TrackEvents[1].track),
+ "Second track should be removed");
+
+ addtrackEventCollector = collectAddTrackEvents(receiveStream);
+ await pc2.setRemoteDescription({type: "rollback"});
+ addedtracks = addtrackEventCollector.finish().map(e => e.track);
+ assert_equals(addedtracks.length, 2, "Rollback should have added two tracks");
+
+ // pc2 as offerer
+ offer = await pc2.createOffer();
+ await pc2.setLocalDescription(offer);
+ await pc1.setRemoteDescription(offer);
+ answer = await pc1.createAnswer();
+ await pc1.setLocalDescription(answer);
+
+ removetrackEventCollector = collectRemoveTrackEvents(receiveStream);
+ await pc2.setRemoteDescription(answer);
+ removedtracks = removetrackEventCollector.finish().map(e => e.track);
+ assert_equals(removedtracks.length, 2, "Should have two removed tracks");
+
+ hasProps(pc2.getTransceivers(),
+ [
+ {
+ currentDirection: "inactive"
+ },
+ {
+ currentDirection: "inactive"
+ }
+ ]);
+ };
+
+ const checkSetDirection = async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ pc.addTransceiver("audio");
+
+ pc.getTransceivers()[0].direction = "sendonly";
+ hasProps(pc.getTransceivers(),[{direction: "sendonly"}]);
+ pc.getTransceivers()[0].direction = "recvonly";
+ hasProps(pc.getTransceivers(),[{direction: "recvonly"}]);
+ pc.getTransceivers()[0].direction = "inactive";
+ hasProps(pc.getTransceivers(),[{direction: "inactive"}]);
+ pc.getTransceivers()[0].direction = "sendrecv";
+ hasProps(pc.getTransceivers(),[{direction: "sendrecv"}]);
+ };
+
+ const checkCurrentDirection = async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+
+ const stream = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stopTracks(stream));
+ const track = stream.getAudioTracks()[0];
+ pc1.addTrack(track, stream);
+ pc2.addTrack(track, stream);
+ hasProps(pc1.getTransceivers(), [{currentDirection: null}]);
+
+ let offer = await pc1.createOffer();
+ hasProps(pc1.getTransceivers(), [{currentDirection: null}]);
+
+ await pc1.setLocalDescription(offer);
+ hasProps(pc1.getTransceivers(), [{currentDirection: null}]);
+
+ let trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, offer);
+ hasProps(trackEvents,
+ [
+ {
+ track: pc2.getTransceivers()[0].receiver.track,
+ streams: [{id: stream.id}]
+ }
+ ]);
+
+ hasProps(pc2.getTransceivers(), [{currentDirection: null}]);
+
+ let answer = await pc2.createAnswer();
+ hasProps(pc2.getTransceivers(), [{currentDirection: null}]);
+
+ await pc2.setLocalDescription(answer);
+ hasProps(pc2.getTransceivers(), [{currentDirection: "sendrecv"}]);
+
+ trackEvents = await setRemoteDescriptionReturnTrackEvents(pc1, answer);
+ hasProps(trackEvents,
+ [
+ {
+ track: pc1.getTransceivers()[0].receiver.track,
+ streams: [{id: stream.id}]
+ }
+ ]);
+
+ hasProps(pc1.getTransceivers(), [{currentDirection: "sendrecv"}]);
+
+ pc2.getTransceivers()[0].direction = "sendonly";
+
+ offer = await pc2.createOffer();
+ hasProps(pc2.getTransceivers(), [{currentDirection: "sendrecv"}]);
+
+ await pc2.setLocalDescription(offer);
+ hasProps(pc2.getTransceivers(), [{currentDirection: "sendrecv"}]);
+
+ trackEvents = await setRemoteDescriptionReturnTrackEvents(pc1, offer);
+ hasProps(trackEvents, []);
+
+ hasProps(pc1.getTransceivers(), [{currentDirection: "sendrecv"}]);
+
+ answer = await pc1.createAnswer();
+ hasProps(pc1.getTransceivers(), [{currentDirection: "sendrecv"}]);
+
+ await pc1.setLocalDescription(answer);
+ hasProps(pc1.getTransceivers(), [{currentDirection: "recvonly"}]);
+
+ trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, answer);
+ hasProps(trackEvents, []);
+
+ hasProps(pc2.getTransceivers(), [{currentDirection: "sendonly"}]);
+
+ pc2.getTransceivers()[0].direction = "sendrecv";
+
+ offer = await pc2.createOffer();
+ hasProps(pc2.getTransceivers(), [{currentDirection: "sendonly"}]);
+
+ await pc2.setLocalDescription(offer);
+ hasProps(pc2.getTransceivers(), [{currentDirection: "sendonly"}]);
+
+ trackEvents = await setRemoteDescriptionReturnTrackEvents(pc1, offer);
+ hasProps(trackEvents, []);
+
+ hasProps(pc1.getTransceivers(), [{currentDirection: "recvonly"}]);
+
+ answer = await pc1.createAnswer();
+ hasProps(pc1.getTransceivers(), [{currentDirection: "recvonly"}]);
+
+ await pc1.setLocalDescription(answer);
+ hasProps(pc1.getTransceivers(), [{currentDirection: "sendrecv"}]);
+
+ trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, answer);
+ hasProps(trackEvents,
+ [
+ {
+ track: pc2.getTransceivers()[0].receiver.track,
+ streams: [{id: stream.id}]
+ }
+ ]);
+
+ hasProps(pc2.getTransceivers(), [{currentDirection: "sendrecv"}]);
+
+ pc2.close();
+ hasProps(pc2.getTransceivers(), [{currentDirection: "stopped"}]);
+ };
+
+ const checkSendrecvWithNoSendTrack = async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+
+ const stream = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stopTracks(stream));
+ const track = stream.getAudioTracks()[0];
+ pc1.addTransceiver("audio");
+ pc1.getTransceivers()[0].direction = "sendrecv";
+ pc2.addTrack(track, stream);
+
+ const offer = await pc1.createOffer();
+
+ let trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, offer);
+ hasProps(trackEvents,
+ [
+ {
+ track: pc2.getTransceivers()[0].receiver.track,
+ streams: []
+ }
+ ]);
+
+ trickle(t, pc1, pc2);
+ await pc1.setLocalDescription(offer);
+
+ const answer = await pc2.createAnswer();
+ trackEvents = await setRemoteDescriptionReturnTrackEvents(pc1, answer);
+ // Spec language doesn't say anything about checking whether the transceiver
+ // is stopped here.
+ hasProps(trackEvents,
+ [
+ {
+ track: pc1.getTransceivers()[0].receiver.track,
+ streams: [{id: stream.id}]
+ }
+ ]);
+
+ trickle(t, pc2, pc1);
+ await pc2.setLocalDescription(answer);
+
+ await iceConnected(pc1);
+ await iceConnected(pc2);
+ };
+
+ const checkSendrecvWithTracklessStream = async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+
+ const stream = new MediaStream();
+ pc1.addTransceiver("audio", {streams: [stream]});
+
+ const offer = await pc1.createOffer();
+
+ const trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, offer);
+ hasProps(trackEvents,
+ [
+ {
+ track: pc2.getTransceivers()[0].receiver.track,
+ streams: [{id: stream.id}]
+ }
+ ]);
+ };
+
+ const checkMute = async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const stream1 = await getNoiseStream({audio: true, video: true});
+ t.add_cleanup(() => stopTracks(stream1));
+ const audio1 = stream1.getAudioTracks()[0];
+ pc1.addTrack(audio1, stream1);
+ const countMuteAudio1 = countEvents(pc1.getTransceivers()[0].receiver.track, "mute");
+ const countUnmuteAudio1 = countEvents(pc1.getTransceivers()[0].receiver.track, "unmute");
+
+ const video1 = stream1.getVideoTracks()[0];
+ pc1.addTrack(video1, stream1);
+ const countMuteVideo1 = countEvents(pc1.getTransceivers()[1].receiver.track, "mute");
+ const countUnmuteVideo1 = countEvents(pc1.getTransceivers()[1].receiver.track, "unmute");
+
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+ const stream2 = await getNoiseStream({audio: true, video: true});
+ t.add_cleanup(() => stopTracks(stream2));
+ const audio2 = stream2.getAudioTracks()[0];
+ pc2.addTrack(audio2, stream2);
+ const countMuteAudio2 = countEvents(pc2.getTransceivers()[0].receiver.track, "mute");
+ const countUnmuteAudio2 = countEvents(pc2.getTransceivers()[0].receiver.track, "unmute");
+
+ const video2 = stream2.getVideoTracks()[0];
+ pc2.addTrack(video2, stream2);
+ const countMuteVideo2 = countEvents(pc2.getTransceivers()[1].receiver.track, "mute");
+ const countUnmuteVideo2 = countEvents(pc2.getTransceivers()[1].receiver.track, "unmute");
+
+
+ // Check that receive tracks start muted
+ hasProps(pc1.getTransceivers(),
+ [
+ {receiver: {track: {kind: "audio", muted: true}}},
+ {receiver: {track: {kind: "video", muted: true}}}
+ ]);
+
+ hasProps(pc1.getTransceivers(),
+ [
+ {receiver: {track: {kind: "audio", muted: true}}},
+ {receiver: {track: {kind: "video", muted: true}}}
+ ]);
+
+ let offer = await pc1.createOffer();
+ await pc2.setRemoteDescription(offer);
+ trickle(t, pc1, pc2);
+ await pc1.setLocalDescription(offer);
+ let answer = await pc2.createAnswer();
+ await pc1.setRemoteDescription(answer);
+ trickle(t, pc2, pc1);
+ await pc2.setLocalDescription(answer);
+
+ let gotUnmuteAudio1 = gotUnmuteEvent(pc1.getTransceivers()[0].receiver.track);
+ let gotUnmuteVideo1 = gotUnmuteEvent(pc1.getTransceivers()[1].receiver.track);
+
+ let gotUnmuteAudio2 = gotUnmuteEvent(pc2.getTransceivers()[0].receiver.track);
+ let gotUnmuteVideo2 = gotUnmuteEvent(pc2.getTransceivers()[1].receiver.track);
+ // Jump out before waiting if a track is unmuted before RTP starts flowing.
+ assert_true(pc1.getTransceivers()[0].receiver.track.muted);
+ assert_true(pc1.getTransceivers()[1].receiver.track.muted);
+ assert_true(pc2.getTransceivers()[0].receiver.track.muted);
+ assert_true(pc2.getTransceivers()[1].receiver.track.muted);
+
+ await iceConnected(pc1);
+ await iceConnected(pc2);
+
+
+ // Check that receive tracks are unmuted when RTP starts flowing
+ await gotUnmuteAudio1;
+ await gotUnmuteVideo1;
+ await gotUnmuteAudio2;
+ await gotUnmuteVideo2;
+
+ // Check whether disabling recv locally causes onmute
+ pc1.getTransceivers()[0].direction = "sendonly";
+ pc1.getTransceivers()[1].direction = "sendonly";
+ offer = await pc1.createOffer();
+ await pc2.setRemoteDescription(offer);
+ await pc1.setLocalDescription(offer);
+ answer = await pc2.createAnswer();
+ const gotMuteAudio1 = gotMuteEvent(pc1.getTransceivers()[0].receiver.track);
+ const gotMuteVideo1 = gotMuteEvent(pc1.getTransceivers()[1].receiver.track);
+ await pc1.setRemoteDescription(answer);
+ await pc2.setLocalDescription(answer);
+ await gotMuteAudio1;
+ await gotMuteVideo1;
+
+ // Check whether disabling on remote causes onmute
+ pc1.getTransceivers()[0].direction = "inactive";
+ pc1.getTransceivers()[1].direction = "inactive";
+ offer = await pc1.createOffer();
+ const gotMuteAudio2 = gotMuteEvent(pc2.getTransceivers()[0].receiver.track);
+ const gotMuteVideo2 = gotMuteEvent(pc2.getTransceivers()[1].receiver.track);
+ await pc2.setRemoteDescription(offer);
+ await gotMuteAudio2;
+ await gotMuteVideo2;
+ await pc1.setLocalDescription(offer);
+ answer = await pc2.createAnswer();
+ await pc1.setRemoteDescription(answer);
+ await pc2.setLocalDescription(answer);
+
+ // Check whether onunmute fires when we turn everything on again
+ pc1.getTransceivers()[0].direction = "sendrecv";
+ pc1.getTransceivers()[1].direction = "sendrecv";
+ offer = await pc1.createOffer();
+ await pc2.setRemoteDescription(offer);
+ // Set these up before sLD, since that sets [[Receptive]] to true, which
+ // could allow an unmute to occur from a packet that was sent before we
+ // negotiated inactive!
+ gotUnmuteAudio1 = gotUnmuteEvent(pc1.getTransceivers()[0].receiver.track);
+ gotUnmuteVideo1 = gotUnmuteEvent(pc1.getTransceivers()[1].receiver.track);
+ await pc1.setLocalDescription(offer);
+ answer = await pc2.createAnswer();
+ gotUnmuteAudio2 = gotUnmuteEvent(pc2.getTransceivers()[0].receiver.track);
+ gotUnmuteVideo2 = gotUnmuteEvent(pc2.getTransceivers()[1].receiver.track);
+ await pc1.setRemoteDescription(answer);
+ await pc2.setLocalDescription(answer);
+ await gotUnmuteAudio1;
+ await gotUnmuteVideo1;
+ await gotUnmuteAudio2;
+ await gotUnmuteVideo2;
+
+ // Wait a little, just in case some stray events fire
+ await new Promise(r => t.step_timeout(r, 100));
+
+ assert_equals(1, countMuteAudio1.count, "Got 1 mute event for pc1's audio track");
+ assert_equals(1, countMuteVideo1.count, "Got 1 mute event for pc1's video track");
+ assert_equals(1, countMuteAudio2.count, "Got 1 mute event for pc2's audio track");
+ assert_equals(1, countMuteVideo2.count, "Got 1 mute event for pc2's video track");
+ assert_equals(2, countUnmuteAudio1.count, "Got 2 unmute events for pc1's audio track");
+ assert_equals(2, countUnmuteVideo1.count, "Got 2 unmute events for pc1's video track");
+ assert_equals(2, countUnmuteAudio2.count, "Got 2 unmute events for pc2's audio track");
+ assert_equals(2, countUnmuteVideo2.count, "Got 2 unmute events for pc2's video track");
+ };
+
+ const checkStop = async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const stream = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stopTracks(stream));
+ const track = stream.getAudioTracks()[0];
+ pc1.addTrack(track, stream);
+
+ let offer = await pc1.createOffer();
+ await pc1.setLocalDescription(offer);
+
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+ await pc2.setRemoteDescription(offer);
+
+ pc2.addTrack(track, stream);
+
+ const answer = await pc2.createAnswer();
+ await pc2.setLocalDescription(answer);
+ await pc1.setRemoteDescription(answer);
+
+ let stoppedTransceiver = pc1.getTransceivers()[0];
+ let onended = new Promise(resolve => {
+ stoppedTransceiver.receiver.track.onended = resolve;
+ });
+ stoppedTransceiver.stop();
+ assert_equals(pc1.getReceivers().length, 1, 'getReceivers exposes a receiver of a stopped transceiver before negotiation');
+ assert_equals(pc1.getSenders().length, 1, 'getSenders exposes a sender of a stopped transceiver before negotiation');
+ await onended;
+ // The transceiver has [[stopping]] = true, [[stopped]] = false
+ hasPropsAndUniqueMids(pc1.getTransceivers(),
+ [
+ {
+ sender: {track: {kind: "audio"}},
+ receiver: {track: {kind: "audio", readyState: "ended"}},
+ currentDirection: "sendrecv",
+ direction: "stopped"
+ }
+ ]);
+
+ const transceiver = pc1.getTransceivers()[0];
+
+ checkThrows(() => transceiver.sender.setParameters(
+ transceiver.sender.getParameters()),
+ "InvalidStateError", "setParameters on stopped transceiver");
+
+ const stream2 = await getNoiseStream({audio: true});
+ const track2 = stream.getAudioTracks()[0];
+ checkThrows(() => transceiver.sender.replaceTrack(track2),
+ "InvalidStateError", "replaceTrack on stopped transceiver");
+
+ checkThrows(() => transceiver.direction = "sendrecv",
+ "InvalidStateError", "set direction on stopped transceiver");
+
+ checkThrows(() => transceiver.sender.dtmf.insertDTMF("111"),
+ "InvalidStateError", "insertDTMF on stopped transceiver");
+
+ // Shouldn't throw
+ stoppedTransceiver.stop();
+
+ offer = await pc1.createOffer();
+ await pc1.setLocalDescription(offer);
+
+ const stoppedCalleeTransceiver = pc2.getTransceivers()[0];
+ onended = new Promise(resolve => {
+ stoppedCalleeTransceiver.receiver.track.onended = resolve;
+ });
+
+ await pc2.setRemoteDescription(offer);
+
+ await onended;
+ // pc2's transceiver was stopped remotely.
+ // The track ends when setRemeoteDescription(offer) is set.
+ hasProps(pc2.getTransceivers(),
+ [
+ {
+ sender: {track: {kind: "audio"}},
+ receiver: {track: {kind: "audio", readyState: "ended"}},
+ currentDirection: "stopped",
+ direction: "stopped"
+ }
+ ]);
+ // After setLocalDescription(answer), the transceiver has
+ // [[stopping]] = true, [[stopped]] = true, and is removed from pc2.
+ const stoppingAnswer = await pc2.createAnswer();
+ await pc2.setLocalDescription(stoppingAnswer);
+ assert_equals(pc2.getTransceivers().length, 0);
+ assert_equals(pc2.getReceivers().length, 0, 'getReceivers does not expose a receiver of a stopped transceiver after negotiation');
+ assert_equals(pc2.getSenders().length, 0, 'getSenders does not expose a sender of a stopped transceiver after negotiation');
+
+ // Shouldn't throw either
+ stoppedTransceiver.stop();
+ await pc1.setRemoteDescription(stoppingAnswer);
+ assert_equals(pc1.getReceivers().length, 0, 'getReceivers does not expose a receiver of a stopped transceiver after negotiation');
+ assert_equals(pc1.getSenders().length, 0, 'getSenders does not expose a sender of a stopped transceiver after negotiation');
+
+ pc1.close();
+ pc2.close();
+
+ // Spec says the closed check comes before the stopped check, so this
+ // should throw now.
+ checkThrows(() => stoppedTransceiver.stop(),
+ "InvalidStateError", "RTCRtpTransceiver.stop() with closed PC");
+ };
+
+ const checkStopAfterCreateOffer = async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+
+ const stream = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stopTracks(stream));
+ const track = stream.getAudioTracks()[0];
+ pc1.addTrack(track, stream);
+ pc2.addTrack(track, stream);
+
+ let offer = await pc1.createOffer();
+
+ const transceiverThatWasStopped = pc1.getTransceivers()[0];
+ transceiverThatWasStopped.stop();
+ await pc2.setRemoteDescription(offer)
+ trickle(t, pc1, pc2);
+ await pc1.setLocalDescription(offer);
+
+ let answer = await pc2.createAnswer();
+ const negotiationNeededAwaiter = negotiationNeeded(pc1);
+ const trackEvents = await setRemoteDescriptionReturnTrackEvents(pc1, answer);
+ // Spec language doesn't say anything about checking whether the transceiver
+ // is stopped here.
+ hasProps(trackEvents,
+ [
+ {
+ track: pc1.getTransceivers()[0].receiver.track,
+ streams: [{id: stream.id}]
+ }
+ ]);
+
+ assert_equals(transceiverThatWasStopped, pc1.getTransceivers()[0]);
+ // The transceiver should still be [[stopping]]=true, [[stopped]]=false.
+ hasPropsAndUniqueMids(pc1.getTransceivers(),
+ [
+ {
+ currentDirection: "sendrecv",
+ direction: "stopped"
+ }
+ ]);
+
+ await negotiationNeededAwaiter;
+
+ trickle(t, pc2, pc1);
+
+ await pc2.setLocalDescription(answer);
+
+ await iceConnected(pc1);
+ await iceConnected(pc2);
+
+ offer = await pc1.createOffer();
+ await pc1.setLocalDescription(offer);
+ await pc2.setRemoteDescription(offer);
+ answer = await pc2.createAnswer();
+ await pc2.setLocalDescription(answer);
+ await pc1.setRemoteDescription(answer);
+ assert_equals(pc1.getTransceivers().length, 0);
+ assert_equals(pc2.getTransceivers().length, 0);
+ };
+
+ const checkStopAfterSetLocalOffer = async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+
+ const stream = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stopTracks(stream));
+ const track = stream.getAudioTracks()[0];
+ pc1.addTrack(track, stream);
+ pc2.addTrack(track, stream);
+
+ let offer = await pc1.createOffer();
+
+ await pc2.setRemoteDescription(offer)
+ trickle(t, pc1, pc2);
+ await pc1.setLocalDescription(offer);
+
+ pc1.getTransceivers()[0].stop();
+
+ let answer = await pc2.createAnswer();
+ const negotiationNeededAwaiter = negotiationNeeded(pc1);
+ const trackEvents = await setRemoteDescriptionReturnTrackEvents(pc1, answer);
+ // Spec language doesn't say anything about checking whether the transceiver
+ // is stopped here.
+ hasProps(trackEvents,
+ [
+ {
+ track: pc1.getTransceivers()[0].receiver.track,
+ streams: [{id: stream.id}]
+ }
+ ]);
+
+ hasPropsAndUniqueMids(pc1.getTransceivers(),
+ [
+ {
+ direction: "stopped",
+ currentDirection: "sendrecv"
+ }
+ ]);
+ await negotiationNeededAwaiter;
+
+ trickle(t, pc2, pc1);
+ await pc2.setLocalDescription(answer);
+
+ await iceConnected(pc1);
+ await iceConnected(pc2);
+
+ offer = await pc1.createOffer();
+ await pc1.setLocalDescription(offer);
+ await pc2.setRemoteDescription(offer);
+ answer = await pc2.createAnswer();
+ await pc2.setLocalDescription(answer);
+ await pc1.setRemoteDescription(answer);
+
+ assert_equals(pc1.getTransceivers().length, 0);
+ assert_equals(pc2.getTransceivers().length, 0);
+ };
+
+ const checkStopAfterSetRemoteOffer = async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+
+ const stream = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stopTracks(stream));
+ const track = stream.getAudioTracks()[0];
+ pc1.addTrack(track, stream);
+ pc2.addTrack(track, stream);
+
+ const offer = await pc1.createOffer();
+
+ await pc2.setRemoteDescription(offer)
+ await pc1.setLocalDescription(offer);
+
+ // Stop on _answerer_ side now. Should not stop transceiver in answer,
+ // but cause firing of negotiationNeeded at pc2, and disabling
+ // of the transceiver with direction = inactive in answer.
+ pc2.getTransceivers()[0].stop();
+ assert_equals(pc2.getTransceivers()[0].direction, 'stopped');
+
+ const answer = await pc2.createAnswer();
+ const trackEvents = await setRemoteDescriptionReturnTrackEvents(pc1, answer);
+ hasProps(trackEvents, []);
+
+ hasProps(pc2.getTransceivers(),
+ [
+ {
+ direction: "stopped",
+ currentDirection: null,
+ }
+ ]);
+
+ const negotiationNeededAwaiter = negotiationNeeded(pc2);
+ await pc2.setLocalDescription(answer);
+ hasProps(pc2.getTransceivers(),
+ [
+ {
+ direction: "stopped",
+ currentDirection: "inactive",
+ }
+ ]);
+
+ await negotiationNeededAwaiter;
+ };
+
+ const checkStopAfterCreateAnswer = async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+
+ const stream = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stopTracks(stream));
+ const track = stream.getAudioTracks()[0];
+ pc1.addTrack(track, stream);
+ pc2.addTrack(track, stream);
+
+ let offer = await pc1.createOffer();
+
+ await pc2.setRemoteDescription(offer)
+ trickle(t, pc1, pc2);
+ await pc1.setLocalDescription(offer);
+
+ let answer = await pc2.createAnswer();
+
+ // Too late for this to go in the answer. ICE should succeed.
+ pc2.getTransceivers()[0].stop();
+
+ const trackEvents = await setRemoteDescriptionReturnTrackEvents(pc1, answer);
+ hasProps(trackEvents,
+ [
+ {
+ track: pc1.getTransceivers()[0].receiver.track,
+ streams: [{id: stream.id}]
+ }
+ ]);
+
+ hasPropsAndUniqueMids(pc2.getTransceivers(),
+ [
+ {
+ direction: "stopped",
+ currentDirection: null,
+ }
+ ]);
+
+ trickle(t, pc2, pc1);
+ // The negotiationneeded event is fired during processing of
+ // setLocalDescription()
+ const negotiationNeededAwaiter = negotiationNeeded(pc2);
+ await pc2.setLocalDescription(answer);
+ hasPropsAndUniqueMids(pc2.getTransceivers(),
+ [
+ {
+ direction: "stopped",
+ currentDirection: "sendrecv",
+ }
+ ]);
+
+ await negotiationNeededAwaiter;
+ await iceConnected(pc1);
+ await iceConnected(pc2);
+
+ offer = await pc1.createOffer();
+ await pc1.setLocalDescription(offer);
+ await pc2.setRemoteDescription(offer);
+ answer = await pc2.createAnswer();
+ await pc2.setLocalDescription(answer);
+ await pc1.setRemoteDescription(answer);
+
+ // Since this offer/answer exchange was initiated from pc1,
+ // pc2 still doesn't get to say that it has a stopped transceiver,
+ // but does get to set it to inactive.
+ hasProps(pc1.getTransceivers(),
+ [
+ {
+ direction: "sendrecv",
+ currentDirection: "inactive",
+ }
+ ]);
+
+ hasProps(pc2.getTransceivers(),
+ [
+ {
+ direction: "stopped",
+ currentDirection: "inactive",
+ }
+ ]);
+ };
+
+ const checkStopAfterSetLocalAnswer = async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+
+ const stream = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stopTracks(stream));
+ const track = stream.getAudioTracks()[0];
+ pc1.addTrack(track, stream);
+ pc2.addTrack(track, stream);
+
+ let offer = await pc1.createOffer();
+
+ await pc2.setRemoteDescription(offer)
+ trickle(t, pc1, pc2);
+ await pc1.setLocalDescription(offer);
+
+ let answer = await pc2.createAnswer();
+
+ const trackEvents = await setRemoteDescriptionReturnTrackEvents(pc1, answer);
+ hasProps(trackEvents,
+ [
+ {
+ track: pc1.getTransceivers()[0].receiver.track,
+ streams: [{id: stream.id}]
+ }
+ ]);
+
+ trickle(t, pc2, pc1);
+ await pc2.setLocalDescription(answer);
+
+ // ICE should succeed.
+ pc2.getTransceivers()[0].stop();
+
+ hasPropsAndUniqueMids(pc2.getTransceivers(),
+ [
+ {
+ direction: "stopped",
+ currentDirection: "sendrecv",
+ }
+ ]);
+
+ await negotiationNeeded(pc2);
+ await iceConnected(pc1);
+ await iceConnected(pc2);
+
+ // Initiate an offer/answer exchange from pc2 in order
+ // to negotiate the stopped transceiver.
+ offer = await pc2.createOffer();
+ await pc2.setLocalDescription(offer);
+ await pc1.setRemoteDescription(offer);
+ answer = await pc1.createAnswer();
+ await pc1.setLocalDescription(answer);
+ await pc2.setRemoteDescription(answer);
+
+ assert_equals(pc1.getTransceivers().length, 0);
+ assert_equals(pc2.getTransceivers().length, 0);
+ };
+
+ const checkStopAfterClose = async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+
+ const stream = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stopTracks(stream));
+ const track = stream.getAudioTracks()[0];
+ pc1.addTrack(track, stream);
+ pc2.addTrack(track, stream);
+
+ const offer = await pc1.createOffer();
+ await pc2.setRemoteDescription(offer)
+ await pc1.setLocalDescription(offer);
+ const answer = await pc2.createAnswer();
+ await pc2.setLocalDescription(answer);
+ await pc1.setRemoteDescription(answer);
+
+ pc1.close();
+ await checkThrows(() => pc1.getTransceivers()[0].stop(),
+ "InvalidStateError",
+ "Stopping a transceiver on a closed PC should throw.");
+ };
+
+ const checkLocalRollback = async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const stream = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stopTracks(stream));
+ const track = stream.getAudioTracks()[0];
+ pc.addTrack(track, stream);
+
+ let offer = await pc.createOffer();
+ await pc.setLocalDescription(offer);
+
+ hasPropsAndUniqueMids(pc.getTransceivers(),
+ [
+ {
+ receiver: {track: {kind: "audio"}},
+ sender: {track},
+ direction: "sendrecv",
+ currentDirection: null,
+ }
+ ]);
+
+ // Verify that rollback doesn't stomp things it should not
+ pc.getTransceivers()[0].direction = "sendonly";
+ const stream2 = await getNoiseStream({audio: true});
+ const track2 = stream2.getAudioTracks()[0];
+ await pc.getTransceivers()[0].sender.replaceTrack(track2);
+
+ await pc.setLocalDescription({type: "rollback"});
+
+ hasProps(pc.getTransceivers(),
+ [
+ {
+ receiver: {track: {kind: "audio"}},
+ sender: {track: track2},
+ direction: "sendonly",
+ mid: null,
+ currentDirection: null,
+ }
+ ]);
+
+ // Make sure stop() isn't rolled back either.
+ offer = await pc.createOffer();
+ await pc.setLocalDescription(offer);
+ pc.getTransceivers()[0].stop();
+ await pc.setLocalDescription({type: "rollback"});
+
+ hasProps(pc.getTransceivers(), [
+ {
+ direction: "stopped",
+ }
+ ]);
+ };
+
+ const checkRollbackAndSetRemoteOfferWithDifferentType = async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+
+ const audioStream = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stopTracks(audioStream));
+ const audioTrack = audioStream.getAudioTracks()[0];
+ pc1.addTrack(audioTrack, audioStream);
+
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const videoStream = await getNoiseStream({video: true});
+ t.add_cleanup(() => stopTracks(videoStream));
+ const videoTrack = videoStream.getVideoTracks()[0];
+ pc2.addTrack(videoTrack, videoStream);
+
+ await pc1.setLocalDescription(await pc1.createOffer());
+ await pc1.setLocalDescription({type: "rollback"});
+
+ hasProps(pc1.getTransceivers(),
+ [
+ {
+ receiver: {track: {kind: "audio"}},
+ sender: {track: audioTrack},
+ direction: "sendrecv",
+ mid: null,
+ currentDirection: null,
+ }
+ ]);
+
+ hasProps(pc2.getTransceivers(),
+ [
+ {
+ receiver: {track: {kind: "video"}},
+ sender: {track: videoTrack},
+ direction: "sendrecv",
+ mid: null,
+ currentDirection: null,
+ }
+ ]);
+
+ await offerAnswer(pc2, pc1);
+
+ hasPropsAndUniqueMids(pc1.getTransceivers(),
+ [
+ {
+ receiver: {track: {kind: "audio"}},
+ sender: {track: audioTrack},
+ direction: "sendrecv",
+ mid: null,
+ currentDirection: null,
+ },
+ {
+ receiver: {track: {kind: "video"}},
+ sender: {track: null},
+ direction: "recvonly",
+ currentDirection: "recvonly",
+ }
+ ]);
+
+ hasPropsAndUniqueMids(pc2.getTransceivers(),
+ [
+ {
+ receiver: {track: {kind: "video"}},
+ sender: {track: videoTrack},
+ direction: "sendrecv",
+ currentDirection: "sendonly",
+ }
+ ]);
+
+ await offerAnswer(pc1, pc2);
+ };
+
+ const checkRemoteRollback = async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+
+ const stream = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stopTracks(stream));
+ const track = stream.getAudioTracks()[0];
+ pc1.addTrack(track, stream);
+
+ let offer = await pc1.createOffer();
+
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+ await pc2.setRemoteDescription(offer);
+
+ const removedTransceiver = pc2.getTransceivers()[0];
+
+ const onended = new Promise(resolve => {
+ removedTransceiver.receiver.track.onended = resolve;
+ });
+
+ await pc2.setRemoteDescription({type: "rollback"});
+
+ // Transceiver should be _gone_
+ hasProps(pc2.getTransceivers(), []);
+
+ hasProps(removedTransceiver,
+ {
+ mid: null,
+ currentDirection: "stopped"
+ }
+ );
+
+ await onended;
+
+ hasProps(removedTransceiver,
+ {
+ receiver: {track: {readyState: "ended"}},
+ mid: null,
+ currentDirection: "stopped"
+ }
+ );
+
+ // Setting the same offer again should do the same thing as before
+ await pc2.setRemoteDescription(offer);
+ hasPropsAndUniqueMids(pc2.getTransceivers(),
+ [
+ {
+ receiver: {track: {kind: "audio"}},
+ sender: {track: null},
+ direction: "recvonly",
+ currentDirection: null,
+ }
+ ]);
+
+ const mid0 = pc2.getTransceivers()[0].mid;
+
+ // Give pc2 a track with replaceTrack
+ const stream2 = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stopTracks(stream2));
+ const track2 = stream2.getAudioTracks()[0];
+ await pc2.getTransceivers()[0].sender.replaceTrack(track2);
+ pc2.getTransceivers()[0].direction = "sendrecv";
+ hasProps(pc2.getTransceivers(),
+ [
+ {
+ receiver: {track: {kind: "audio"}},
+ sender: {track: track2},
+ direction: "sendrecv",
+ mid: mid0,
+ currentDirection: null,
+ }
+ ]);
+
+ await pc2.setRemoteDescription({type: "rollback"});
+
+ // Transceiver should be _gone_, again. replaceTrack doesn't prevent this,
+ // nor does setting direction.
+ hasProps(pc2.getTransceivers(), []);
+
+ // Setting the same offer for a _third_ time should do the same thing
+ await pc2.setRemoteDescription(offer);
+ hasProps(pc2.getTransceivers(),
+ [
+ {
+ receiver: {track: {kind: "audio"}},
+ sender: {track: null},
+ direction: "recvonly",
+ mid: mid0,
+ currentDirection: null,
+ }
+ ]);
+
+ // We should be able to add the same track again
+ pc2.addTrack(track2, stream2);
+ hasProps(pc2.getTransceivers(),
+ [
+ {
+ receiver: {track: {kind: "audio"}},
+ sender: {track: track2},
+ direction: "sendrecv",
+ mid: mid0,
+ currentDirection: null,
+ }
+ ]);
+
+ await pc2.setRemoteDescription({type: "rollback"});
+ // Transceiver should _not_ be gone this time, because addTrack touched it.
+ hasProps(pc2.getTransceivers(),
+ [
+ {
+ receiver: {track: {kind: "audio"}},
+ sender: {track: track2},
+ direction: "sendrecv",
+ mid: null,
+ currentDirection: null,
+ }
+ ]);
+
+ // Complete negotiation so we can test interactions with transceiver.stop()
+ await pc1.setLocalDescription(offer);
+
+ // After all this SRD/rollback, we should still get the track event
+ let trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, offer);
+
+ assert_equals(trackEvents.length, 1);
+ hasProps(trackEvents,
+ [
+ {
+ track: pc2.getTransceivers()[0].receiver.track,
+ streams: [{id: stream.id}]
+ }
+ ]);
+
+ const answer = await pc2.createAnswer();
+ await pc2.setLocalDescription(answer);
+
+ // Make sure all this rollback hasn't messed up the signaling
+ trackEvents = await setRemoteDescriptionReturnTrackEvents(pc1, answer);
+ assert_equals(trackEvents.length, 1);
+ hasProps(trackEvents,
+ [
+ {
+ track: pc1.getTransceivers()[0].receiver.track,
+ streams: [{id: stream2.id}]
+ }
+ ]);
+ hasProps(pc1.getTransceivers(),
+ [
+ {
+ receiver: {track: {kind: "audio"}},
+ sender: {track},
+ direction: "sendrecv",
+ mid: mid0,
+ currentDirection: "sendrecv",
+ }
+ ]);
+
+ // Don't bother waiting for ICE and such
+
+ // Check to see whether rolling back a remote track removal works
+ pc1.getTransceivers()[0].direction = "recvonly";
+ offer = await pc1.createOffer();
+
+ trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, offer);
+ hasProps(trackEvents, []);
+
+ trackEvents =
+ await setRemoteDescriptionReturnTrackEvents(pc2, {type: "rollback"});
+
+ assert_equals(trackEvents.length, 1, 'track event from remote rollback');
+ hasProps(trackEvents,
+ [
+ {
+ track: pc2.getTransceivers()[0].receiver.track,
+ streams: [{id: stream.id}]
+ }
+ ]);
+
+ // Check to see that stop() cannot be rolled back
+ pc1.getTransceivers()[0].stop();
+ offer = await pc1.createOffer();
+
+ await pc2.setRemoteDescription(offer);
+ hasProps(pc2.getTransceivers(),
+ [
+ {
+ receiver: {track: {kind: "audio"}},
+ sender: {track: track2},
+ direction: "stopped",
+ mid: mid0,
+ currentDirection: "stopped",
+ }
+ ]);
+
+ // stop() cannot be rolled back!
+ // Transceiver should have [[stopping]]=true, [[stopped]]=false.
+ await pc2.setRemoteDescription({type: "rollback"});
+ hasProps(pc2.getTransceivers(),
+ [
+ {
+ receiver: {track: {kind: "audio"}},
+ sender: {track: {kind: "audio"}},
+ direction: "stopped",
+ mid: mid0,
+ currentDirection: "stopped",
+ }
+ ]);
+ };
+
+ const checkBundleTagRejected = async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const stream1 = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stopTracks(stream1));
+ const track1 = stream1.getAudioTracks()[0];
+ const stream2 = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stopTracks(stream2));
+ const track2 = stream2.getAudioTracks()[0];
+
+ pc1.addTrack(track1, stream1);
+ pc1.addTrack(track2, stream2);
+
+ await offerAnswer(pc1, pc2);
+
+ pc2.getTransceivers()[0].stop();
+
+ await offerAnswer(pc1, pc2);
+ await offerAnswer(pc2, pc1);
+ };
+
+ const checkMsectionReuse = async t => {
+ // Use max-compat to make it easier to check for disabled m-sections
+ const pc1 = new RTCPeerConnection({ bundlePolicy: "max-compat" });
+ const pc2 = new RTCPeerConnection({ bundlePolicy: "max-compat" });
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+
+ const stream = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stopTracks(stream));
+ const track = stream.getAudioTracks()[0];
+ pc1.addTrack(track, stream);
+ const [pc1Transceiver] = pc1.getTransceivers();
+
+ await pc1.setLocalDescription();
+ await pc2.setRemoteDescription(pc1.localDescription);
+
+ // Answerer stops transceiver. The m-section is not immediately rejected
+ // (a follow-up O/A exchange is needed) but it should become inactive in
+ // the meantime.
+ const stoppedMid0 = pc2.getTransceivers()[0].mid;
+ const [pc2Transceiver] = pc2.getTransceivers();
+ pc2Transceiver.stop();
+ assert_equals(pc2.getTransceivers()[0].direction, "stopped");
+ assert_not_equals(pc2.getTransceivers()[0].currentDirection, "stopped");
+
+ await pc2.setLocalDescription();
+ await pc1.setRemoteDescription(pc2.localDescription);
+
+ // Still not stopped - but inactive is reflected!
+ assert_equals(pc1Transceiver.mid, stoppedMid0);
+ assert_equals(pc1Transceiver.direction, "sendrecv");
+ assert_equals(pc1Transceiver.currentDirection, "inactive");
+ assert_equals(pc2Transceiver.mid, stoppedMid0);
+ assert_equals(pc2Transceiver.direction, "stopped");
+ assert_equals(pc2Transceiver.currentDirection, "inactive");
+
+ // Now do the follow-up O/A exchange pc2 -> pc1.
+ await pc2.setLocalDescription();
+ await pc1.setRemoteDescription(pc2.localDescription);
+ await pc1.setLocalDescription();
+ await pc2.setRemoteDescription(pc1.localDescription);
+
+ // Now they're stopped, and have been removed from the PCs.
+ assert_equals(pc1.getTransceivers().length, 0);
+ assert_equals(pc2.getTransceivers().length, 0);
+ assert_equals(pc1Transceiver.mid, null);
+ assert_equals(pc1Transceiver.direction, "stopped");
+ assert_equals(pc1Transceiver.currentDirection, "stopped");
+ assert_equals(pc2Transceiver.mid, null);
+ assert_equals(pc2Transceiver.direction, "stopped");
+ assert_equals(pc2Transceiver.currentDirection, "stopped");
+
+ // Check that m-section is reused on both ends
+ const stream2 = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stopTracks(stream2));
+ const track2 = stream2.getAudioTracks()[0];
+
+ pc1.addTrack(track2, stream2);
+ let offer = await pc1.createOffer();
+ assert_equals(offer.sdp.match(/m=/g).length, 1,
+ "Exactly one m-line in offer, because it was reused");
+ hasProps(pc1.getTransceivers(),
+ [
+ {
+ sender: {track: track2}
+ }
+ ]);
+
+ assert_not_equals(pc1.getTransceivers()[0].mid, stoppedMid0);
+
+ pc2.addTrack(track, stream);
+ offer = await pc2.createOffer();
+ assert_equals(offer.sdp.match(/m=/g).length, 1,
+ "Exactly one m-line in offer, because it was reused");
+ hasProps(pc2.getTransceivers(),
+ [
+ {
+ sender: {track}
+ }
+ ]);
+
+ assert_not_equals(pc2.getTransceivers()[0].mid, stoppedMid0);
+
+ await pc2.setLocalDescription(offer);
+ await pc1.setRemoteDescription(offer);
+ let answer = await pc1.createAnswer();
+ await pc1.setLocalDescription(answer);
+ await pc2.setRemoteDescription(answer);
+ hasPropsAndUniqueMids(pc1.getTransceivers(),
+ [
+ {
+ sender: {track: track2},
+ currentDirection: "sendrecv"
+ }
+ ]);
+
+ const mid0 = pc1.getTransceivers()[0].mid;
+
+ hasProps(pc2.getTransceivers(),
+ [
+ {
+ sender: {track},
+ currentDirection: "sendrecv",
+ mid: mid0
+ }
+ ]);
+
+ // stop the transceiver, and add a track. Verify that we don't reuse
+ // prematurely in our offer. (There should be one rejected m-section, and a
+ // new one for the new track)
+ const stoppedMid1 = pc1.getTransceivers()[0].mid;
+ pc1.getTransceivers()[0].stop();
+ const stream3 = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stopTracks(stream3));
+ const track3 = stream3.getAudioTracks()[0];
+ pc1.addTrack(track3, stream3);
+ offer = await pc1.createOffer();
+ assert_equals(offer.sdp.match(/m=/g).length, 2,
+ "Exactly 2 m-lines in offer, because it is too early to reuse");
+ assert_equals(offer.sdp.match(/m=audio 0 /g).length, 1,
+ "One m-line is rejected");
+
+ await pc1.setLocalDescription(offer);
+
+ let trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, offer);
+ hasProps(trackEvents,
+ [
+ {
+ track: pc2.getTransceivers()[1].receiver.track,
+ streams: [{id: stream3.id}]
+ }
+ ]);
+
+ answer = await pc2.createAnswer();
+ await pc2.setLocalDescription(answer);
+
+ trackEvents = await setRemoteDescriptionReturnTrackEvents(pc1, answer);
+ hasProps(trackEvents, []);
+
+ hasPropsAndUniqueMids(pc2.getTransceivers(),
+ [
+ {
+ sender: {track: null},
+ currentDirection: "recvonly"
+ }
+ ]);
+
+ // Verify that we don't reuse the mid from the stopped transceiver
+ const mid1 = pc2.getTransceivers()[0].mid;
+ assert_not_equals(mid1, stoppedMid1);
+
+ pc2.addTrack(track3, stream3);
+ // There are two ways to handle this new track; reuse the recvonly
+ // transceiver created above, or create a new transceiver and reuse the
+ // disabled m-section. We're supposed to do the former.
+ offer = await pc2.createOffer();
+ assert_equals(offer.sdp.match(/m=/g).length, 2, "Exactly 2 m-lines in offer");
+ assert_equals(offer.sdp.match(/m=audio 0 /g).length, 1,
+ "One m-line is rejected, because the other was used");
+
+ hasProps(pc2.getTransceivers(),
+ [
+ {
+ mid: mid1,
+ sender: {track: track3},
+ currentDirection: "recvonly",
+ direction: "sendrecv"
+ }
+ ]);
+
+ // Add _another_ track; this should reuse the disabled m-section
+ const stream4 = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stopTracks(stream4));
+ const track4 = stream4.getAudioTracks()[0];
+ pc2.addTrack(track4, stream4);
+ offer = await pc2.createOffer();
+ await pc2.setLocalDescription(offer);
+ hasPropsAndUniqueMids(pc2.getTransceivers(),
+ [
+ {
+ mid: mid1
+ },
+ {
+ sender: {track: track4},
+ }
+ ]);
+
+ // Fourth transceiver should have a new mid
+ assert_not_equals(pc2.getTransceivers()[1].mid, stoppedMid0);
+ assert_not_equals(pc2.getTransceivers()[1].mid, stoppedMid1);
+
+ assert_equals(offer.sdp.match(/m=/g).length, 2,
+ "Exactly 2 m-lines in offer, because m-section was reused");
+ assert_equals(offer.sdp.match(/m=audio 0 /g), null,
+ "No rejected m-line, because it was reused");
+ };
+
+ const checkStopAfterCreateOfferWithReusedMsection = async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const stream = await getNoiseStream({audio: true, video: true});
+ t.add_cleanup(() => stopTracks(stream));
+ const audio = stream.getAudioTracks()[0];
+ const video = stream.getVideoTracks()[0];
+
+ pc1.addTrack(audio, stream);
+ pc1.addTrack(video, stream);
+
+ await offerAnswer(pc1, pc2);
+ pc1.getTransceivers()[1].stop();
+ await offerAnswer(pc1, pc2);
+
+ // Second (video) m-section has been negotiated disabled.
+ const transceiver = pc1.addTransceiver("video");
+ const offer = await pc1.createOffer();
+ transceiver.stop();
+ await pc1.setLocalDescription(offer);
+ await pc2.setRemoteDescription(offer);
+
+ const answer = await pc2.createAnswer();
+ await pc2.setLocalDescription(answer);
+ await pc1.setRemoteDescription(answer);
+ };
+
+ const checkAddIceCandidateToStoppedTransceiver = async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const stream = await getNoiseStream({audio: true, video: true});
+ t.add_cleanup(() => stopTracks(stream));
+ const audio = stream.getAudioTracks()[0];
+ const video = stream.getVideoTracks()[0];
+
+ pc1.addTrack(audio, stream);
+ pc1.addTrack(video, stream);
+
+ pc2.addTrack(audio, stream);
+ pc2.addTrack(video, stream);
+
+ await pc1.setLocalDescription(await pc1.createOffer());
+ pc1.getTransceivers()[1].stop();
+ pc1.setLocalDescription({type: "rollback"});
+
+ const offer = await pc2.createOffer();
+ await pc2.setLocalDescription(offer);
+ await pc1.setRemoteDescription(offer);
+
+ await pc1.addIceCandidate(
+ {
+ candidate: "candidate:0 1 UDP 2122252543 192.168.1.112 64261 typ host",
+ sdpMid: pc2.getTransceivers()[1].mid
+ });
+ };
+
+const tests = [
+ checkAddTransceiverNoTrack,
+ checkAddTransceiverWithTrack,
+ checkAddTransceiverWithAddTrack,
+ checkAddTransceiverWithDirection,
+ checkAddTransceiverWithSetRemoteOfferSending,
+ checkAddTransceiverWithSetRemoteOfferNoSend,
+ checkAddTransceiverBadKind,
+ checkNoMidOffer,
+ checkNoMidAnswer,
+ checkSetDirection,
+ checkCurrentDirection,
+ checkSendrecvWithNoSendTrack,
+ checkSendrecvWithTracklessStream,
+ checkAddTransceiverNoTrackDoesntPair,
+ checkAddTransceiverWithTrackDoesntPair,
+ checkAddTransceiverThenReplaceTrackDoesntPair,
+ checkAddTransceiverThenAddTrackPairs,
+ checkAddTrackPairs,
+ checkReplaceTrackNullDoesntPreventPairing,
+ checkRemoveAndReadd,
+ checkAddTrackExistingTransceiverThenRemove,
+ checkRemoveTrackNegotiation,
+ checkMute,
+ checkStop,
+ checkStopAfterCreateOffer,
+ checkStopAfterSetLocalOffer,
+ checkStopAfterSetRemoteOffer,
+ checkStopAfterCreateAnswer,
+ checkStopAfterSetLocalAnswer,
+ checkStopAfterClose,
+ checkLocalRollback,
+ checkRollbackAndSetRemoteOfferWithDifferentType,
+ checkRemoteRollback,
+ checkMsectionReuse,
+ checkStopAfterCreateOfferWithReusedMsection,
+ checkAddIceCandidateToStoppedTransceiver,
+ checkBundleTagRejected
+].forEach(test => promise_test(test, test.name));
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCSctpTransport-constructor.html b/testing/web-platform/tests/webrtc/RTCSctpTransport-constructor.html
new file mode 100644
index 0000000000..484967f76b
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCSctpTransport-constructor.html
@@ -0,0 +1,125 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>RTCSctpTransport constructor</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="RTCPeerConnection-helper.js"></script>
+<script>
+'use strict';
+
+// Test is based on the following revision:
+// https://rawgit.com/w3c/webrtc-pc/1cc5bfc3ff18741033d804c4a71f7891242fb5b3/webrtc.html
+
+// The following helper functions are called from RTCPeerConnection-helper.js:
+// generateDataChannelOffer()
+// generateAnswer()
+
+/*
+ 6.1.
+
+ partial interface RTCPeerConnection {
+ readonly attribute RTCSctpTransport? sctp;
+ ...
+ };
+
+ 6.1.1.
+
+ interface RTCSctpTransport {
+ readonly attribute RTCDtlsTransport transport;
+ readonly attribute RTCSctpTransportState state;
+ readonly attribute unrestricted double maxMessageSize;
+ attribute EventHandler onstatechange;
+ };
+
+ 4.4.1.1. Constructor
+ 9. Let connection have an [[SctpTransport]] internal slot, initialized to null.
+
+ 4.4.1.6. Set the RTCSessionSessionDescription
+ 2.2.6. If description is of type "answer" or "pranswer", then run the
+ following steps:
+ 1. If description initiates the establishment of a new SCTP association, as defined in
+ [SCTP-SDP], Sections 10.3 and 10.4, create an RTCSctpTransport with an initial state
+ of "connecting" and assign the result to the [[SctpTransport]] slot.
+ */
+
+promise_test(async (t) => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+
+ assert_equals(pc1.sctp, null, 'RTCSctpTransport must be null');
+
+ const offer = await generateAudioReceiveOnlyOffer(pc1);
+ await Promise.all([pc1.setLocalDescription(offer), pc2.setRemoteDescription(offer)]);
+ const answer = await pc2.createAnswer();
+ await pc1.setRemoteDescription(answer);
+
+ assert_equals(pc1.sctp, null, 'RTCSctpTransport must remain null');
+}, 'setRemoteDescription() with answer not containing data media should not initialize pc.sctp');
+
+promise_test(async (t) => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+
+ assert_equals(pc1.sctp, null, 'RTCSctpTransport must be null');
+
+ const offer = await generateAudioReceiveOnlyOffer(pc2);
+ await Promise.all([pc2.setLocalDescription(offer), pc1.setRemoteDescription(offer)]);
+ const answer = await pc1.createAnswer();
+ await pc1.setLocalDescription(answer);
+
+ assert_equals(pc1.sctp, null, 'RTCSctpTransport must remain null');
+}, 'setLocalDescription() with answer not containing data media should not initialize pc.sctp');
+
+function validateSctpTransport(sctp) {
+ assert_not_equals(sctp, null, 'RTCSctpTransport must be available');
+
+ assert_true(sctp instanceof RTCSctpTransport,
+ 'Expect pc.sctp to be instance of RTCSctpTransport');
+
+ assert_true(sctp.transport instanceof RTCDtlsTransport,
+ 'Expect sctp.transport to be instance of RTCDtlsTransport');
+
+ assert_equals(sctp.state, 'connecting', 'RTCSctpTransport should be in the connecting state');
+
+ // Note: Yes, Number.POSITIVE_INFINITY is also a 'number'
+ assert_equals(typeof sctp.maxMessageSize, 'number',
+ 'Expect sctp.maxMessageSize to be a number');
+}
+
+promise_test(async (t) => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+
+ assert_equals(pc1.sctp, null, 'RTCSctpTransport must be null');
+
+ const offer = await generateDataChannelOffer(pc1);
+ await Promise.all([pc1.setLocalDescription(offer), pc2.setRemoteDescription(offer)]);
+ const answer = await pc2.createAnswer();
+ await pc1.setRemoteDescription(answer);
+
+ validateSctpTransport(pc1.sctp);
+}, 'setRemoteDescription() with answer containing data media should initialize pc.sctp');
+
+promise_test(async (t) => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+
+ assert_equals(pc1.sctp, null, 'RTCSctpTransport must be null');
+
+ const offer = await generateDataChannelOffer(pc2);
+ await Promise.all([pc2.setLocalDescription(offer), pc1.setRemoteDescription(offer)]);
+ const answer = await pc1.createAnswer();
+ await pc1.setLocalDescription(answer);
+
+ validateSctpTransport(pc1.sctp);
+}, 'setLocalDescription() with answer containing data media should initialize pc.sctp');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCSctpTransport-events.html b/testing/web-platform/tests/webrtc/RTCSctpTransport-events.html
new file mode 100644
index 0000000000..57b691a9cd
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCSctpTransport-events.html
@@ -0,0 +1,55 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCIceTransport</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="RTCPeerConnection-helper.js"></script>
+<script>
+'use strict';
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+ pc1.createDataChannel('');
+ assert_equals(null, pc1.sctp);
+ assert_equals(null, pc2.sctp);
+ const offer = await pc1.createOffer();
+ await pc1.setLocalDescription(offer);
+ assert_not_equals(null, pc1.sctp);
+ await pc2.setRemoteDescription(offer);
+ assert_not_equals(null, pc2.sctp);
+ const answer = await pc2.createAnswer();
+ await pc2.setLocalDescription(answer);
+ await pc1.setRemoteDescription(answer);
+ // Since this test does not exchange candidates, state remains "connecting".
+ assert_equals(pc1.sctp.state, "connecting");
+ assert_equals(pc2.sctp.state, "connecting");
+}, 'SctpTransport objects are created at appropriate times');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+ exchangeIceCandidates(pc1, pc2);
+ pc1.createDataChannel('');
+ const offer = await pc1.createOffer();
+ await pc1.setLocalDescription(offer);
+ const pc1ConnectedWaiter = waitForState(pc1.sctp, 'connected');
+ await pc2.setRemoteDescription(offer);
+ const pc2ConnectedWaiter = waitForState(pc2.sctp, 'connected');
+ const answer = await pc2.createAnswer();
+ await pc2.setLocalDescription(answer);
+ await pc1.setRemoteDescription(answer);
+ await pc1ConnectedWaiter;
+ await pc2ConnectedWaiter;
+ const pc1ClosedWaiter = waitForState(pc1.sctp, 'closed');
+ const pc2ClosedWaiter = waitForState(pc2.sctp, 'closed');
+ pc1.close();
+ await pc1ClosedWaiter;
+ await pc2ClosedWaiter;
+}, 'SctpTransport reaches connected and closed state');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCSctpTransport-maxChannels.html b/testing/web-platform/tests/webrtc/RTCSctpTransport-maxChannels.html
new file mode 100644
index 0000000000..b173e11c74
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCSctpTransport-maxChannels.html
@@ -0,0 +1,49 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCSctpTransport.prototype.maxChannels</title>
+<link rel="help" href="https://w3c.github.io/webrtc-pc/#rtcsctptransport-interface">
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src="RTCPeerConnection-helper.js"></script>
+<script>
+'use strict';
+
+promise_test(async (t) => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ assert_equals(pc.sctp, null, 'RTCSctpTransport must be null');
+ pc.createDataChannel('test');
+ const offer = await pc.createOffer();
+ await pc.setRemoteDescription(offer);
+ const answer = await pc.createAnswer();
+ await pc.setLocalDescription(answer);
+
+ assert_not_equals(pc.sctp, null, 'RTCSctpTransport must be available');
+ assert_equals(pc.sctp.maxChannels, null, 'maxChannels must not be set');
+}, 'An unconnected peerconnection must not have maxChannels set');
+
+promise_test(async (t) => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+ exchangeIceCandidates(pc1, pc2);
+ pc1.createDataChannel('');
+ const offer = await pc1.createOffer();
+ await pc1.setLocalDescription(offer);
+ const pc1ConnectedWaiter = waitForState(pc1.sctp, 'connected');
+ await pc2.setRemoteDescription(offer);
+ const pc2ConnectedWaiter = waitForState(pc2.sctp, 'connected');
+ const answer = await pc2.createAnswer();
+ await pc2.setLocalDescription(answer);
+ await pc1.setRemoteDescription(answer);
+ assert_equals(null, pc1.sctp.maxChannels);
+ assert_equals(null, pc2.sctp.maxChannels);
+ await pc1ConnectedWaiter;
+ await pc2ConnectedWaiter;
+ assert_not_equals(null, pc1.sctp.maxChannels);
+ assert_not_equals(null, pc2.sctp.maxChannels);
+ assert_equals(pc1.sctp.maxChannels, pc2.sctp.maxChannels);
+}, 'maxChannels gets instantiated after connecting');
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCSctpTransport-maxMessageSize.html b/testing/web-platform/tests/webrtc/RTCSctpTransport-maxMessageSize.html
new file mode 100644
index 0000000000..9976761150
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCSctpTransport-maxMessageSize.html
@@ -0,0 +1,206 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCSctpTransport.prototype.maxMessageSize</title>
+<link rel="help" href="https://w3c.github.io/webrtc-pc/#rtcsctptransport-interface">
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src="RTCPeerConnection-helper.js"></script>
+<script>
+'use strict';
+
+// This test has an assert_unreached() that requires that the variable
+// canSendSize (initiated below) must be 0 or greater than 2. The reason
+// is that we need two non-zero values for testing the following two cases:
+//
+// * if remote MMS `1` < canSendSize it should result in `1`.
+// * renegotiation of the above case with remoteMMS `2` should result in `2`.
+//
+// This is a bit unfortunate but shouldn't have any practical impact.
+
+// Helper class to read SDP attributes and generate SDPs with modified attribute values
+class SDPAttributeHelper {
+ constructor(attrName, valueRegExpStr) {
+ this.attrName = attrName;
+ this.re = new RegExp(`^a=${attrName}:(${valueRegExpStr})\\r\\n`, 'm');
+ }
+
+ getValue(sdp) {
+ const matches = sdp.match(this.re);
+ return matches ? matches[1] : null;
+ }
+
+ sdpWithValue(sdp, value) {
+ const matches = sdp.match(this.re);
+ const sdpParts = sdp.split(matches[0]);
+ const attributeLine = arguments.length > 1 ? `a=${this.attrName}:${value}\r\n` : '';
+ return `${sdpParts[0]}${attributeLine}${sdpParts[1]}`;
+ }
+
+ sdpWithoutAttribute(sdp) {
+ return this.sdpWithValue(sdp);
+ }
+}
+
+const mmsAttributeHelper = new SDPAttributeHelper('max-message-size', '\\d+');
+let canSendSize = null;
+const remoteSize1 = 1;
+const remoteSize2 = 2;
+
+promise_test(async (t) => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ assert_equals(pc.sctp, null, 'RTCSctpTransport must be null');
+
+ let offer = await generateDataChannelOffer(pc);
+ assert_not_equals(mmsAttributeHelper.getValue(offer.sdp), null,
+ 'SDP should have max-message-size attribute');
+ offer = { type: 'offer', sdp: mmsAttributeHelper.sdpWithValue(offer.sdp, 0) };
+ await pc.setRemoteDescription(offer);
+ const answer = await pc.createAnswer();
+ await pc.setLocalDescription(answer);
+
+ assert_not_equals(pc.sctp, null, 'RTCSctpTransport must be available');
+ canSendSize = pc.sctp.maxMessageSize === Number.POSITIVE_INFINITY ? 0 : pc.sctp.maxMessageSize;
+ if (canSendSize !== 0 && canSendSize < remoteSize2) {
+ assert_unreached(
+ 'This test needs canSendSize to be 0 or > 2 for further "below" and "above" tests');
+ }
+}, 'Determine the local side send limitation (canSendSize) by offering a max-message-size of 0');
+
+promise_test(async (t) => {
+ assert_not_equals(canSendSize, null, 'canSendSize needs to be determined');
+
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ assert_equals(pc.sctp, null, 'RTCSctpTransport must be null');
+
+ let offer = await generateDataChannelOffer(pc);
+ assert_not_equals(mmsAttributeHelper.getValue(offer.sdp), null,
+ 'SDP should have max-message-size attribute');
+
+ // Remove the max-message-size SDP attribute
+ offer = { type: 'offer', sdp: mmsAttributeHelper.sdpWithoutAttribute(offer.sdp) };
+ await pc.setRemoteDescription(offer);
+ const answer = await pc.createAnswer();
+ await pc.setLocalDescription(answer);
+
+ assert_not_equals(pc.sctp, null, 'RTCSctpTransport must be available');
+ // Test outcome depends on canSendSize value
+ if (canSendSize !== 0) {
+ assert_equals(pc.sctp.maxMessageSize, Math.min(65536, canSendSize),
+ 'Missing SDP attribute and a non-zero canSendSize should give an maxMessageSize of min(65536, canSendSize)');
+ } else {
+ assert_equals(pc.sctp.maxMessageSize, 65536,
+ 'Missing SDP attribute and a canSendSize of 0 should give an maxMessageSize of 65536');
+ }
+}, 'Remote offer SDP missing max-message-size attribute');
+
+promise_test(async (t) => {
+ assert_not_equals(canSendSize, null, 'canSendSize needs to be determined');
+
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ assert_equals(pc.sctp, null, 'RTCSctpTransport must be null');
+
+ let offer = await generateDataChannelOffer(pc);
+ assert_not_equals(mmsAttributeHelper.getValue(offer.sdp), null,
+ 'SDP should have max-message-size attribute');
+
+ offer = { type: 'offer', sdp: mmsAttributeHelper.sdpWithValue(offer.sdp, remoteSize1) };
+ await pc.setRemoteDescription(offer);
+ const answer = await pc.createAnswer();
+ await pc.setLocalDescription(answer);
+
+ assert_not_equals(pc.sctp, null, 'RTCSctpTransport must be available');
+ assert_equals(pc.sctp.maxMessageSize, remoteSize1,
+ 'maxMessageSize should be the value provided by the remote peer (as long as it is less than canSendSize)');
+}, 'max-message-size with a (non-zero) value provided by the remote peer');
+
+promise_test(async (t) => {
+ assert_not_equals(canSendSize, null, 'canSendSize needs to be determined');
+
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ assert_equals(pc.sctp, null, 'RTCSctpTransport must be null');
+
+ let offer = await generateDataChannelOffer(pc);
+ assert_not_equals(mmsAttributeHelper.getValue(offer.sdp), null,
+ 'SDP should have max-message-size attribute');
+
+ offer = { type: 'offer', sdp: mmsAttributeHelper.sdpWithValue(offer.sdp, remoteSize1) };
+ await pc.setRemoteDescription(offer);
+ let answer = await pc.createAnswer();
+ await pc.setLocalDescription(answer);
+
+ assert_not_equals(pc.sctp, null, 'RTCSctpTransport must be available');
+ assert_equals(pc.sctp.maxMessageSize, remoteSize1,
+ 'maxMessageSize should be the value provided by the remote peer (as long as it is less than canSendSize)');
+
+ // Start new O/A exchange that updates max-message-size to remoteSize2
+ offer = await pc.createOffer();
+ offer = { type: 'offer', sdp: mmsAttributeHelper.sdpWithValue(offer.sdp, remoteSize2)};
+ await pc.setRemoteDescription(offer);
+ answer = await pc.createAnswer();
+ await pc.setLocalDescription(answer);
+
+ assert_not_equals(pc.sctp, null, 'RTCSctpTransport must be available');
+ assert_equals(pc.sctp.maxMessageSize, remoteSize2,
+ 'maxMessageSize should be the new value provided by the remote peer (as long as it is less than canSendSize)');
+
+ // Start new O/A exchange that updates max-message-size to zero
+ offer = await pc.createOffer();
+ offer = { type: 'offer', sdp: mmsAttributeHelper.sdpWithValue(offer.sdp, 0)};
+ await pc.setRemoteDescription(offer);
+ answer = await pc.createAnswer();
+ await pc.setLocalDescription(answer);
+
+ assert_not_equals(pc.sctp, null, 'RTCSctpTransport must be available');
+ assert_equals(pc.sctp.maxMessageSize, canSendSize,
+ 'maxMessageSize should be canSendSize');
+
+ // Start new O/A exchange that updates max-message-size to remoteSize1 again
+ offer = await pc.createOffer();
+ offer = { type: 'offer', sdp: mmsAttributeHelper.sdpWithValue(offer.sdp, remoteSize1)};
+ await pc.setRemoteDescription(offer);
+ answer = await pc.createAnswer();
+ await pc.setLocalDescription(answer);
+
+ assert_not_equals(pc.sctp, null, 'RTCSctpTransport must be available');
+ assert_equals(pc.sctp.maxMessageSize, remoteSize1,
+ 'maxMessageSize should be the new value provided by the remote peer (as long as it is less than canSendSize)');
+}, 'Renegotiate max-message-size with various values provided by the remote peer');
+
+promise_test(async (t) => {
+ assert_not_equals(canSendSize, null, 'canSendSize needs to be determined');
+
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ assert_equals(pc.sctp, null, 'RTCSctpTransport must be null');
+ const largerThanCanSendSize = canSendSize === 0 ? 0 : canSendSize + 1;
+
+ let offer = await generateDataChannelOffer(pc);
+ assert_not_equals(mmsAttributeHelper.getValue(offer.sdp), null,
+ 'SDP should have max-message-size attribute');
+
+ offer = { type: 'offer', sdp: mmsAttributeHelper.sdpWithValue(offer.sdp, largerThanCanSendSize) };
+ await pc.setRemoteDescription(offer);
+ const answer = await pc.createAnswer();
+ await pc.setLocalDescription(answer);
+
+ assert_not_equals(pc.sctp, null, 'RTCSctpTransport must be available');
+ // Test outcome depends on canSendSize value
+ if (canSendSize !== 0) {
+ assert_equals(pc.sctp.maxMessageSize, canSendSize,
+ 'A remote value larger than a non-zero canSendSize should limit maxMessageSize to canSendSize');
+ } else {
+ assert_equals(pc.sctp.maxMessageSize, Number.POSITIVE_INFINITY,
+ 'A remote value of zero and canSendSize zero should result in "infinity"');
+ }
+}, 'max-message-size with a (non-zero) value larger than canSendSize provided by the remote peer');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCStats-helper.js b/testing/web-platform/tests/webrtc/RTCStats-helper.js
new file mode 100644
index 0000000000..29d4940a8a
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCStats-helper.js
@@ -0,0 +1,973 @@
+'use strict';
+
+// Test is based on the following editor draft:
+// webrtc-pc 20171130
+// webrtc-stats 20171122
+
+// This file depends on dictionary-helper.js which should
+// be loaded from the main HTML file.
+
+/*
+ [webrtc-stats]
+ 6.1. RTCStatsType enum
+ enum RTCStatsType {
+ "codec",
+ "inbound-rtp",
+ "outbound-rtp",
+ "remote-inbound-rtp",
+ "remote-outbound-rtp",
+ "csrc",
+ "peer-connection",
+ "data-channel",
+ "transport",
+ "candidate-pair",
+ "local-candidate",
+ "remote-candidate",
+ "certificate",
+ "ice-server"
+ };
+ */
+const statsValidatorTable = {
+ 'codec': validateCodecStats,
+ 'inbound-rtp': validateInboundRtpStreamStats,
+ 'outbound-rtp': validateOutboundRtpStreamStats,
+ 'remote-inbound-rtp': validateRemoteInboundRtpStreamStats,
+ 'remote-outbound-rtp': validateRemoteOutboundRtpStreamStats,
+ 'media-source': validateMediaSourceStats,
+ 'csrc': validateContributingSourceStats,
+ 'peer-connection': validatePeerConnectionStats,
+ 'data-channel': validateDataChannelStats,
+ 'transport': validateTransportStats,
+ 'candidate-pair': validateIceCandidatePairStats,
+ 'local-candidate': validateIceCandidateStats,
+ 'remote-candidate': validateIceCandidateStats,
+ 'certificate': validateCertificateStats,
+ 'ice-server': validateIceServerStats
+};
+
+// Validate that the stats objects in a stats report
+// follows the respective definitions.
+// Stats objects with unknown type are ignored and
+// only basic validation is done.
+function validateStatsReport(statsReport) {
+ for(const [id, stats] of statsReport.entries()) {
+ assert_equals(stats.id, id,
+ 'expect stats.id to be the same as the key in statsReport');
+
+ const validator = statsValidatorTable[stats.type];
+ if(validator) {
+ validator(statsReport, stats);
+ } else {
+ validateRtcStats(statsReport, stats);
+ }
+ }
+}
+
+// Assert that the stats report have stats objects of
+// given types
+function assert_stats_report_has_stats(statsReport, statsTypes) {
+ const hasTypes = new Set([...statsReport.values()]
+ .map(stats => stats.type));
+
+ for(const type of statsTypes) {
+ assert_true(hasTypes.has(type),
+ `Expect statsReport to contain stats object of type ${type}`);
+ }
+}
+
+function findStatsFromReport(statsReport, predicate, message) {
+ for (const stats of statsReport.values()) {
+ if (predicate(stats)) {
+ return stats;
+ }
+ }
+
+ assert_unreached(message || 'none of stats in statsReport satisfy given condition')
+}
+
+// Get stats object of type that is expected to be
+// found in the statsReport
+function getRequiredStats(statsReport, type) {
+ for(const stats of statsReport.values()) {
+ if(stats.type === type) {
+ return stats;
+ }
+ }
+
+ assert_unreached(`required stats of type ${type} is not found in stats report`);
+}
+
+// Get stats object by the stats ID.
+// This is used to retreive other stats objects
+// linked to a stats object
+function getStatsById(statsReport, statsId) {
+ assert_true(statsReport.has(statsId),
+ `Expect stats report to have stats object with id ${statsId}`);
+
+ return statsReport.get(statsId);
+}
+
+// Validate an ID field in a stats object by making sure
+// that the linked stats object is found in the stats report
+// and have the type field value same as expected type
+// It doesn't validate the other fields of the linked stats
+// as validateStatsReport already does all validations
+function validateIdField(statsReport, stats, field, type) {
+ assert_string_field(stats, field);
+ const linkedStats = getStatsById(statsReport, stats[field]);
+ assert_equals(linkedStats.type, type,
+ `Expect linked stats object to have type ${type}`);
+}
+
+function validateOptionalIdField(statsReport, stats, field, type) {
+ if(stats[field] !== undefined) {
+ validateIdField(statsReport, stats, field, type);
+ }
+}
+
+/*
+ [webrtc-pc]
+ 8.4. RTCStats Dictionary
+ dictionary RTCStats {
+ required DOMHighResTimeStamp timestamp;
+ required RTCStatsType type;
+ required DOMString id;
+ };
+ */
+function validateRtcStats(statsReport, stats) {
+ assert_number_field(stats, 'timestamp');
+ assert_string_field(stats, 'type');
+ assert_string_field(stats, 'id');
+}
+
+/*
+ [webrtc-stats]
+ 7.1. RTCRtpStreamStats dictionary
+ dictionary RTCRtpStreamStats : RTCStats {
+ unsigned long ssrc;
+ DOMString kind;
+ DOMString transportId;
+ DOMString codecId;
+ };
+
+ kind of type DOMString
+ Either "audio" or "video".
+
+ [webrtc-pc]
+ 8.6. Mandatory To Implement Stats
+ - RTCRtpStreamStats, with attributes ssrc, kind, transportId, codecId
+ */
+function validateRtpStreamStats(statsReport, stats) {
+ validateRtcStats(statsReport, stats);
+
+ assert_unsigned_int_field(stats, 'ssrc');
+ assert_string_field(stats, 'kind');
+ assert_enum_field(stats, 'kind', ['audio', 'video'])
+
+ validateIdField(statsReport, stats, 'transportId', 'transport');
+ validateIdField(statsReport, stats, 'codecId', 'codec');
+
+}
+
+/*
+ [webrtc-stats]
+ 7.2. RTCCodecStats dictionary
+ dictionary RTCCodecStats : RTCStats {
+ required unsigned long payloadType;
+ RTCCodecType codecType;
+ required DOMString transportId;
+ required DOMString mimeType;
+ unsigned long clockRate;
+ unsigned long channels;
+ DOMString sdpFmtpLine;
+ };
+
+ enum RTCCodecType {
+ "encode",
+ "decode",
+ };
+
+ [webrtc-pc]
+ 8.6. Mandatory To Implement Stats
+ - RTCCodecStats, with attributes payloadType, codecType, mimeType, clockRate, channels, sdpFmtpLine
+ */
+
+function validateCodecStats(statsReport, stats) {
+ validateRtcStats(statsReport, stats);
+
+ assert_unsigned_int_field(stats, 'payloadType');
+ assert_optional_enum_field(stats, 'codecType', ['encode', 'decode']);
+
+ validateOptionalIdField(statsReport, stats, 'transportId', 'transport');
+
+ assert_string_field(stats, 'mimeType');
+ assert_unsigned_int_field(stats, 'clockRate');
+ if (stats.kind === 'audio') {
+ assert_unsigned_int_field(stats, 'channels');
+ }
+ assert_string_field(stats, 'sdpFmtpLine');
+}
+
+/*
+ [webrtc-stats]
+ 7.3. RTCReceivedRtpStreamStats dictionary
+ dictionary RTCReceivedRtpStreamStats : RTCRtpStreamStats {
+ unsigned long long packetsReceived;
+ long long packetsLost;
+ double jitter;
+ unsigned long long packetsDiscarded;
+ unsigned long long packetsRepaired;
+ unsigned long long burstPacketsLost;
+ unsigned long long burstPacketsDiscarded;
+ unsigned long burstLossCount;
+ unsigned long burstDiscardCount;
+ double burstLossRate;
+ double burstDiscardRate;
+ double gapLossRate;
+ double gapDiscardRate;
+ unsigned long framesDropped;
+ unsigned long partialFramesLost;
+ unsigned long fullFramesLost;
+ };
+
+ [webrtc-pc]
+ 8.6. Mandatory To Implement Stats
+ - RTCReceivedRtpStreamStats, with all required attributes from its
+ inherited dictionaries, and also attributes packetsReceived,
+ packetsLost, jitter, packetsDiscarded, framesDropped
+ */
+function validateReceivedRtpStreamStats(statsReport, stats) {
+ validateRtpStreamStats(statsReport, stats);
+
+ assert_unsigned_int_field(stats, 'packetsReceived');
+ assert_unsigned_int_field(stats, 'packetsLost');
+
+ assert_number_field(stats, 'jitter');
+
+ assert_unsigned_int_field(stats, 'packetsDiscarded');
+ assert_unsigned_int_field(stats, 'framesDropped');
+
+ assert_optional_unsigned_int_field(stats, 'packetsRepaired');
+ assert_optional_unsigned_int_field(stats, 'burstPacketsLost');
+ assert_optional_unsigned_int_field(stats, 'burstPacketsDiscarded');
+ assert_optional_unsigned_int_field(stats, 'burstLossCount');
+ assert_optional_unsigned_int_field(stats, 'burstDiscardCount');
+
+ assert_optional_number_field(stats, 'burstLossRate');
+ assert_optional_number_field(stats, 'burstDiscardRate');
+ assert_optional_number_field(stats, 'gapLossRate');
+ assert_optional_number_field(stats, 'gapDiscardRate');
+
+ assert_optional_unsigned_int_field(stats, 'partialFramesLost');
+ assert_optional_unsigned_int_field(stats, 'fullFramesLost');
+}
+
+/*
+ [webrtc-stats]
+ 7.4. RTCInboundRtpStreamStats dictionary
+ dictionary RTCInboundRtpStreamStats : RTCReceivedRtpStreamStats {
+ DOMString trackIdentifier;
+ DOMString remoteId;
+ unsigned long framesDecoded;
+ unsigned long keyFramesDecoded;
+ unsigned long frameWidth;
+ unsigned long frameHeight;
+ unsigned long frameBitDepth;
+ double framesPerSecond;
+ unsigned long long qpSum;
+ double totalDecodeTime;
+ double totalInterFrameDelay;
+ double totalSquaredInterFrameDelay;
+ boolean voiceActivityFlag;
+ DOMHighResTimeStamp lastPacketReceivedTimestamp;
+ double averageRtcpInterval;
+ unsigned long long headerBytesReceived;
+ unsigned long long fecPacketsReceived;
+ unsigned long long fecPacketsDiscarded;
+ unsigned long long bytesReceived;
+ unsigned long long packetsFailedDecryption;
+ unsigned long long packetsDuplicated;
+ record<USVString, unsigned long long> perDscpPacketsReceived;
+ unsigned long nackCount;
+ unsigned long firCount;
+ unsigned long pliCount;
+ unsigned long sliCount;
+ DOMHighResTimeStamp estimatedPlayoutTimestamp;
+ double jitterBufferDelay;
+ unsigned long long jitterBufferEmittedCount;
+ unsigned long long totalSamplesReceived;
+ unsigned long long samplesDecodedWithSilk;
+ unsigned long long samplesDecodedWithCelt;
+ unsigned long long concealedSamples;
+ unsigned long long silentConcealedSamples;
+ unsigned long long concealmentEvents;
+ unsigned long long insertedSamplesForDeceleration;
+ unsigned long long removedSamplesForAcceleration;
+ double audioLevel;
+ double totalAudioEnergy;
+ double totalSamplesDuration;
+ unsigned long framesReceived;
+ DOMString decoderImplementation;
+ };
+
+ [webrtc-pc]
+ 8.6. Mandatory To Implement Stats
+ - RTCInboundRtpStreamStats, with all required attributes from its inherited
+ dictionaries, and also attributes remoteId, framesDecoded, nackCount, framesReceived, bytesReceived, totalAudioEnergy, totalSampleDuration
+ */
+function validateInboundRtpStreamStats(statsReport, stats) {
+ validateReceivedRtpStreamStats(statsReport, stats);
+ assert_string_field(stats, 'trackIdentifier');
+ validateOptionalIdField(statsReport, stats, 'remoteId', 'remote-outbound-rtp');
+ assert_unsigned_int_field(stats, 'framesDecoded');
+ assert_optional_unsigned_int_field(stats, 'keyFramesDecoded');
+ assert_optional_unsigned_int_field(stats, 'frameWidth');
+ assert_optional_unsigned_int_field(stats, 'frameHeight');
+ assert_optional_unsigned_int_field(stats, 'frameBitDepth');
+ assert_optional_number_field(stats, 'framesPerSecond');
+ assert_optional_unsigned_int_field(stats, 'qpSum');
+ assert_optional_number_field(stats, 'totalDecodeTime');
+ assert_optional_number_field(stats, 'totalInterFrameDelay');
+ assert_optional_number_field(stats, 'totalSquaredInterFrameDelay');
+
+ assert_optional_boolean_field(stats, 'voiceActivityFlag');
+
+ assert_optional_number_field(stats, 'lastPacketReceivedTimeStamp');
+ assert_optional_number_field(stats, 'averageRtcpInterval');
+
+ assert_optional_unsigned_int_field(stats, 'fecPacketsReceived');
+ assert_optional_unsigned_int_field(stats, 'fecPacketsDiscarded');
+ assert_unsigned_int_field(stats, 'bytesReceived');
+ assert_optional_unsigned_int_field(stats, 'packetsFailedDecryption');
+ assert_optional_unsigned_int_field(stats, 'packetsDuplicated');
+
+ assert_optional_dict_field(stats, 'perDscpPacketsReceived');
+ if (stats['perDscpPacketsReceived']) {
+ Object.keys(stats['perDscpPacketsReceived'])
+ .forEach(k =>
+ assert_equals(typeof k, 'string', 'Expect keys of perDscpPacketsReceived to be strings')
+ );
+ Object.values(stats['perDscpPacketsReceived'])
+ .forEach(v =>
+ assert_true(Number.isInteger(v) && (v >= 0), 'Expect values of perDscpPacketsReceived to be strings')
+ );
+ }
+
+ assert_unsigned_int_field(stats, 'nackCount');
+
+ assert_optional_unsigned_int_field(stats, 'firCount');
+ assert_optional_unsigned_int_field(stats, 'pliCount');
+ assert_optional_unsigned_int_field(stats, 'sliCount');
+
+ assert_optional_number_field(stats, 'estimatedPlayoutTimestamp');
+ assert_optional_number_field(stats, 'jitterBufferDelay');
+ assert_optional_unsigned_int_field(stats, 'jitterBufferEmittedCount');
+ assert_optional_unsigned_int_field(stats, 'totalSamplesReceived');
+ assert_optional_unsigned_int_field(stats, 'samplesDecodedWithSilk');
+ assert_optional_unsigned_int_field(stats, 'samplesDecodedWithCelt');
+ assert_optional_unsigned_int_field(stats, 'concealedSamples');
+ assert_optional_unsigned_int_field(stats, 'silentConcealedSamples');
+ assert_optional_unsigned_int_field(stats, 'concealmentEvents');
+ assert_optional_unsigned_int_field(stats, 'insertedSamplesForDeceleration');
+ assert_optional_unsigned_int_field(stats, 'removedSamplesForAcceleration');
+ assert_optional_number_field(stats, 'audioLevel');
+ assert_optional_number_field(stats, 'totalAudioEnergy');
+ assert_optional_number_field(stats, 'totalSamplesDuration');
+ assert_unsigned_int_field(stats, 'framesReceived');
+ assert_optional_string_field(stats, 'decoderImplementation');
+ assert_optional_boolean_field(stats, 'powerEfficientDecoder');
+}
+
+/*
+ [webrtc-stats]
+ 7.5. RTCRemoteInboundRtpStreamStats dictionary
+ dictionary RTCRemoteInboundRtpStreamStats : RTCReceivedRtpStreamStats {
+ DOMString localId;
+ double roundTripTime;
+ double totalRoundTripTime;
+ double fractionLost;
+ unsigned long long reportsReceived;
+ unsigned long long roundTripTimeMeasurements;
+ };
+
+ [webrtc-pc]
+ 8.6. Mandatory To Implement Stats
+ - RTCRemoteInboundRtpStreamStats, with all required attributes from its
+ inherited dictionaries, and also attributes localId, roundTripTime
+ */
+function validateRemoteInboundRtpStreamStats(statsReport, stats) {
+ validateReceivedRtpStreamStats(statsReport, stats);
+
+ validateIdField(statsReport, stats, 'localId', 'outbound-rtp');
+ assert_number_field(stats, 'roundTripTime');
+ assert_optional_number_field(stats, 'totalRoundTripTime');
+ assert_optional_number_field(stats, 'fractionLost');
+ assert_optional_unsigned_int_field(stats, 'reportsReceived');
+ assert_optional_unsigned_int_field(stats, 'roundTripTimeMeasurements');
+}
+
+/*
+ [webrtc-stats]
+ 7.6. RTCSentRtpStreamStats dictionary
+ dictionary RTCSentRtpStreamStats : RTCRtpStreamStats {
+ unsigned long packetsSent;
+ unsigned long long bytesSent;
+ };
+
+ [webrtc-pc]
+ 8.6. Mandatory To Implement Stats
+ - RTCSentRtpStreamStats, with all required attributes from its inherited
+ dictionaries, and also attributes packetsSent, bytesSent
+ */
+function validateSentRtpStreamStats(statsReport, stats) {
+ validateRtpStreamStats(statsReport, stats);
+
+ assert_unsigned_int_field(stats, 'packetsSent');
+ assert_unsigned_int_field(stats, 'bytesSent');
+}
+
+/*
+ [webrtc-stats]
+ 7.7. RTCOutboundRtpStreamStats dictionary
+ dictionary RTCOutboundRtpStreamStats : RTCSentRtpStreamStats {
+ DOMString mediaSourceId;
+ DOMString remoteId;
+ DOMString rid;
+ DOMHighResTimeStamp lastPacketSentTimestamp;
+ unsigned long long headerBytesSent;
+ unsigned long packetsDiscardedOnSend;
+ unsigned long long bytesDiscardedOnSend;
+ unsigned long fecPacketsSent;
+ unsigned long long retransmittedPacketsSent;
+ unsigned long long retransmittedBytesSent;
+ double targetBitrate;
+ unsigned long long totalEncodedBytesTarget;
+ unsigned long frameWidth;
+ unsigned long frameHeight;
+ unsigned long frameBitDepth;
+ double framesPerSecond;
+ unsigned long framesSent;
+ unsigned long hugeFramesSent;
+ unsigned long framesEncoded;
+ unsigned long keyFramesEncoded;
+ unsigned long framesDiscardedOnSend;
+ unsigned long long qpSum;
+ unsigned long long totalSamplesSent;
+ unsigned long long samplesEncodedWithSilk;
+ unsigned long long samplesEncodedWithCelt;
+ boolean voiceActivityFlag;
+ double totalEncodeTime;
+ double totalPacketSendDelay;
+ double averageRtcpInterval;
+ RTCQualityLimitationReason qualityLimitationReason;
+ record<DOMString, double> qualityLimitationDurations;
+ unsigned long qualityLimitationResolutionChanges;
+ record<USVString, unsigned long long> perDscpPacketsSent;
+ unsigned long nackCount;
+ unsigned long firCount;
+ unsigned long pliCount;
+ unsigned long sliCount;
+ DOMString encoderImplementation;
+ };
+ [webrtc-pc]
+ 8.6. Mandatory To Implement Stats
+ - RTCOutboundRtpStreamStats, with all required attributes from its
+ inherited dictionaries, and also attributes remoteId, framesEncoded, nackCount, framesSent
+ */
+function validateOutboundRtpStreamStats(statsReport, stats) {
+ validateSentRtpStreamStats(statsReport, stats)
+
+ validateOptionalIdField(statsReport, stats, 'mediaSourceId', 'media-source');
+ validateOptionalIdField(statsReport, stats, 'remoteId', 'remote-inbound-rtp');
+
+ assert_optional_string_field(stats, 'rid');
+
+ assert_optional_number_field(stats, 'lastPacketSentTimestamp');
+ assert_optional_unsigned_int_field(stats, 'headerBytesSent');
+ assert_optional_unsigned_int_field(stats, 'packetsDiscardedOnSend');
+ assert_optional_unsigned_int_field(stats, 'bytesDiscardedOnSend');
+ assert_optional_unsigned_int_field(stats, 'fecPacketsSent');
+ assert_optional_unsigned_int_field(stats, 'retransmittedPacketsSent');
+ assert_optional_unsigned_int_field(stats, 'retransmittedBytesSent');
+ assert_optional_number_field(stats, 'targetBitrate');
+ assert_optional_unsigned_int_field(stats, 'totalEncodedBytesTarget');
+ if (stats['kind'] === 'video') {
+ assert_optional_unsigned_int_field(stats, 'frameWidth');
+ assert_optional_unsigned_int_field(stats, 'frameHeight');
+ assert_optional_unsigned_int_field(stats, 'frameBitDepth');
+ assert_optional_number_field(stats, 'framesPerSecond');
+ assert_unsigned_int_field(stats, 'framesSent');
+ assert_optional_unsigned_int_field(stats, 'hugeFramesSent');
+ assert_unsigned_int_field(stats, 'framesEncoded');
+ assert_optional_unsigned_int_field(stats, 'keyFramesEncoded');
+ assert_optional_unsigned_int_field(stats, 'framesDiscardedOnSend');
+ assert_optional_unsigned_int_field(stats, 'qpSum');
+ } else if (stats['kind'] === 'audio') {
+ assert_optional_unsigned_int_field(stats, 'totalSamplesSent');
+ assert_optional_unsigned_int_field(stats, 'samplesEncodedWithSilk');
+ assert_optional_unsigned_int_field(stats, 'samplesEncodedWithCelt');
+ assert_optional_boolean_field(stats, 'voiceActivityFlag');
+ }
+ assert_optional_number_field(stats, 'totalEncodeTime');
+ assert_optional_number_field(stats, 'totalPacketSendDelay');
+ assert_optional_number_field(stats, 'averageRTCPInterval');
+
+ if (stats['kind'] === 'video') {
+ assert_optional_enum_field(stats, 'qualityLimitationReason', ['none', 'cpu', 'bandwidth', 'other']);
+
+ assert_optional_dict_field(stats, 'qualityLimitationDurations');
+ if (stats['qualityLimitationDurations']) {
+ Object.keys(stats['qualityLimitationDurations'])
+ .forEach(k =>
+ assert_equals(typeof k, 'string', 'Expect keys of qualityLimitationDurations to be strings')
+ );
+ Object.values(stats['qualityLimitationDurations'])
+ .forEach(v =>
+ assert_equals(typeof num, 'number', 'Expect values of qualityLimitationDurations to be numbers')
+ );
+ }
+
+ assert_optional_unsigned_int_field(stats, 'qualityLimitationResolutionChanges');
+ }
+ assert_unsigned_int_field(stats, 'nackCount');
+ assert_optional_dict_field(stats, 'perDscpPacketsSent');
+ if (stats['perDscpPacketsSent']) {
+ Object.keys(stats['perDscpPacketsSent'])
+ .forEach(k =>
+ assert_equals(typeof k, 'string', 'Expect keys of perDscpPacketsSent to be strings')
+ );
+ Object.values(stats['perDscpPacketsSent'])
+ .forEach(v =>
+ assert_true(Number.isInteger(v) && (v >= 0), 'Expect values of perDscpPacketsSent to be strings')
+ );
+ }
+
+ assert_optional_unsigned_int_field(stats, 'firCount');
+ assert_optional_unsigned_int_field(stats, 'pliCount');
+ assert_optional_unsigned_int_field(stats, 'sliCount');
+ assert_optional_string_field(stats, 'encoderImplementation');
+ assert_optional_boolean_field(stats, 'powerEfficientEncoder');
+ assert_optional_string_field(stats, 'scalabilityMode');
+}
+
+/*
+ [webrtc-stats]
+ 7.8. RTCRemoteOutboundRtpStreamStats dictionary
+ dictionary RTCRemoteOutboundRtpStreamStats : RTCSentRtpStreamStats {
+ DOMString localId;
+ DOMHighResTimeStamp remoteTimestamp;
+ unsigned long long reportsSent;
+ };
+
+ [webrtc-pc]
+ 8.6. Mandatory To Implement Stats
+ - RTCRemoteOutboundRtpStreamStats, with all required attributes from its
+ inherited dictionaries, and also attributes localId, remoteTimestamp
+ */
+function validateRemoteOutboundRtpStreamStats(statsReport, stats) {
+ validateSentRtpStreamStats(statsReport, stats);
+
+ validateIdField(statsReport, stats, 'localId', 'inbound-rtp');
+ assert_number_field(stats, 'remoteTimeStamp');
+ assert_optional_unsigned_int_field(stats, 'reportsSent');
+}
+
+/*
+ [webrtc-stats]
+ 7.11 RTCMediaSourceStats dictionary
+ dictionary RTCMediaSourceStats : RTCStats {
+ DOMString trackIdentifier;
+ DOMString kind;
+ };
+
+ dictionary RTCAudioSourceStats : RTCMediaSourceStats {
+ double audioLevel;
+ double totalAudioEnergy;
+ double totalSamplesDuration;
+ double echoReturnLoss;
+ double echoReturnLossEnhancement;
+ };
+
+ dictionary RTCVideoSourceStats : RTCMediaSourceStats {
+ unsigned long width;
+ unsigned long height;
+ unsigned long bitDepth;
+ unsigned long frames;
+ // see https://github.com/w3c/webrtc-stats/issues/540
+ double framesPerSecond;
+ };
+
+ [webrtc-pc]
+ 8.6. Mandatory To Implement Stats
+ RTCMediaSourceStats with attributes trackIdentifier, kind
+ RTCAudioSourceStats, with all required attributes from its inherited dictionaries and totalAudioEnergy, totalSamplesDuration
+ RTCVideoSourceStats, with all required attributes from its inherited dictionaries and width, height, framesPerSecond
+*/
+function validateMediaSourceStats(statsReport, stats) {
+ validateRtcStats(statsReport, stats);
+ assert_string_field(stats, 'trackIdentifier');
+ assert_enum_field(stats, 'kind', ['audio', 'video']);
+
+ if (stats.kind === 'audio') {
+ assert_optional_number_field(stats, 'audioLevel');
+ assert_number_field(stats, 'totalAudioEnergy');
+ assert_number_field(stats, 'totalSamplesDuration');
+ assert_optional_number_field(stats, 'echoReturnLoss');
+ assert_optional_number_field(stats, 'echoReturnLossEnhancement');
+ } else if (stats.kind === 'video') {
+ assert_unsigned_int_field(stats, 'width');
+ assert_unsigned_int_field(stats, 'height');
+ assert_optional_unsigned_int_field(stats, 'bitDpeth');
+ assert_optional_unsigned_int_field(stats, 'frames');
+ assert_number_field(stats, 'framesPerSecond');
+ }
+}
+
+/*
+ [webrtc-stats]
+ 7.9. RTCRTPContributingSourceStats
+ dictionary RTCRTPContributingSourceStats : RTCStats {
+ unsigned long contributorSsrc;
+ DOMString inboundRtpStreamId;
+ unsigned long packetsContributedTo;
+ double audioLevel;
+ };
+ */
+function validateContributingSourceStats(statsReport, stats) {
+ validateRtcStats(statsReport, stats);
+
+ assert_optional_unsigned_int_field(stats, 'contributorSsrc');
+
+ validateOptionalIdField(statsReport, stats, 'inboundRtpStreamId', 'inbound-rtp');
+ assert_optional_unsigned_int_field(stats, 'packetsContributedTo');
+ assert_optional_number_field(stats, 'audioLevel');
+}
+
+/*
+ [webrtc-stats]
+ 7.10. RTCPeerConnectionStats dictionary
+ dictionary RTCPeerConnectionStats : RTCStats {
+ unsigned long dataChannelsOpened;
+ unsigned long dataChannelsClosed;
+ unsigned long dataChannelsRequested;
+ unsigned long dataChannelsAccepted;
+ };
+
+ [webrtc-pc]
+ 8.6. Mandatory To Implement Stats
+ - RTCPeerConnectionStats, with attributes dataChannelsOpened, dataChannelsClosed
+ */
+function validatePeerConnectionStats(statsReport, stats) {
+ validateRtcStats(statsReport, stats);
+
+ assert_unsigned_int_field(stats, 'dataChannelsOpened');
+ assert_unsigned_int_field(stats, 'dataChannelsClosed');
+ assert_optional_unsigned_int_field(stats, 'dataChannelsRequested');
+ assert_optional_unsigned_int_field(stats, 'dataChannelsAccepted');
+}
+
+/*
+ [webrtc-stats]
+ 7.13. RTCDataChannelStats dictionary
+ dictionary RTCDataChannelStats : RTCStats {
+ DOMString label;
+ DOMString protocol;
+ // see https://github.com/w3c/webrtc-stats/issues/541
+ unsigned short dataChannelIdentifier;
+ DOMString transportId;
+ RTCDataChannelState state;
+ unsigned long messagesSent;
+ unsigned long long bytesSent;
+ unsigned long messagesReceived;
+ unsigned long long bytesReceived;
+ };
+
+ [webrtc-pc]
+ 6.2. RTCDataChannel
+ enum RTCDataChannelState {
+ "connecting",
+ "open",
+ "closing",
+ "closed"
+ };
+
+ 8.6. Mandatory To Implement Stats
+ - RTCDataChannelStats, with attributes label, protocol, datachannelIdentifier, state,
+ messagesSent, bytesSent, messagesReceived, bytesReceived
+ */
+function validateDataChannelStats(statsReport, stats) {
+ validateRtcStats(statsReport, stats);
+
+ assert_string_field(stats, 'label');
+ assert_string_field(stats, 'protocol');
+ assert_unsigned_int_field(stats, 'dataChannelIdentifier');
+
+ validateOptionalIdField(statsReport, stats, 'transportId', 'transport');
+
+ assert_enum_field(stats, 'state',
+ ['connecting', 'open', 'closing', 'closed']);
+
+ assert_unsigned_int_field(stats, 'messagesSent');
+ assert_unsigned_int_field(stats, 'bytesSent');
+ assert_unsigned_int_field(stats, 'messagesReceived');
+ assert_unsigned_int_field(stats, 'bytesReceived');
+}
+
+/*
+ [webrtc-stats]
+ 7.14. RTCTransportStats dictionary
+ dictionary RTCTransportStats : RTCStats {
+ unsigned long long packetsSent;
+ unsigned long long packetsReceived;
+ unsigned long long bytesSent;
+ unsigned long long bytesReceived;
+ DOMString rtcpTransportStatsId;
+ RTCIceRole iceRole;
+ RTCDtlsTransportState dtlsState;
+ DOMString selectedCandidatePairId;
+ DOMString localCertificateId;
+ DOMString remoteCertificateId;
+ DOMString tlsVersion;
+ DOMString dtlsCipher;
+ DOMString srtpCipher;
+ DOMString tlsGroup;
+ unsigned long selectedCandidatePairChanges;
+ };
+
+ [webrtc-pc]
+ 5.5. RTCDtlsTransportState Enum
+ enum RTCDtlsTransportState {
+ "new",
+ "connecting",
+ "connected",
+ "closed",
+ "failed"
+ };
+
+ 5.6. RTCIceRole Enum
+ enum RTCIceRole {
+ "unknown",
+ "controlling",
+ "controlled"
+ };
+
+ 8.6. Mandatory To Implement Stats
+ - RTCTransportStats, with attributes bytesSent, bytesReceived,
+ selectedCandidatePairId, localCertificateId,
+ remoteCertificateId
+ */
+function validateTransportStats(statsReport, stats) {
+ validateRtcStats(statsReport, stats);
+
+ assert_optional_unsigned_int_field(stats, 'packetsSent');
+ assert_optional_unsigned_int_field(stats, 'packetsReceived');
+ assert_unsigned_int_field(stats, 'bytesSent');
+ assert_unsigned_int_field(stats, 'bytesReceived');
+
+ validateOptionalIdField(statsReport, stats, 'rtcpTransportStatsId',
+ 'transport');
+
+ assert_optional_enum_field(stats, 'iceRole',
+ ['unknown', 'controlling', 'controlled']);
+
+ assert_optional_enum_field(stats, 'dtlsState',
+ ['new', 'connecting', 'connected', 'closed', 'failed']);
+
+ validateIdField(statsReport, stats, 'selectedCandidatePairId', 'candidate-pair');
+ validateIdField(statsReport, stats, 'localCertificateId', 'certificate');
+ validateIdField(statsReport, stats, 'remoteCertificateId', 'certificate');
+ assert_optional_string_field(stats, 'tlsVersion');
+ assert_optional_string_field(stats, 'dtlsCipher');
+ assert_optional_string_field(stats, 'srtpCipher');
+ assert_optional_string_field(stats, 'tlsGroup');
+ assert_optional_unsigned_int_field(stats, 'selectedCandidatePairChanges');
+}
+
+/*
+ [webrtc-stats]
+ 7.15. RTCIceCandidateStats dictionary
+ dictionary RTCIceCandidateStats : RTCStats {
+ required DOMString transportId;
+ DOMString? address;
+ long port;
+ DOMString protocol;
+ RTCIceCandidateType candidateType;
+ long priority;
+ DOMString url;
+ DOMString relayProtocol;
+ };
+
+ [webrtc-pc]
+ 4.8.1.3. RTCIceCandidateType Enum
+ enum RTCIceCandidateType {
+ "host",
+ "srflx",
+ "prflx",
+ "relay"
+ };
+
+ 8.6. Mandatory To Implement Stats
+ - RTCIceCandidateStats, with attributes address, port, protocol, candidateType, url
+ */
+function validateIceCandidateStats(statsReport, stats) {
+ validateRtcStats(statsReport, stats);
+
+ validateIdField(statsReport, stats, 'transportId', 'transport');
+ // The address is mandatory to implement, but is allowed to be null
+ // when hidden for privacy reasons.
+ if (stats.address != null) {
+ // Departure from strict spec reading:
+ // This field is populated in a racy manner in Chrome.
+ // We allow it to be present or not present for the time being.
+ // TODO(https://bugs.chromium.org/1092721): Become consistent.
+ assert_optional_string_field(stats, 'address');
+ }
+ assert_unsigned_int_field(stats, 'port');
+ assert_string_field(stats, 'protocol');
+
+ assert_enum_field(stats, 'candidateType',
+ ['host', 'srflx', 'prflx', 'relay']);
+
+ assert_optional_int_field(stats, 'priority');
+ // The url field is mandatory for local candidates gathered from
+ // a STUN or TURN server, and MUST NOT be present otherwise.
+ // TODO(hta): Improve checking.
+ assert_optional_string_field(stats, 'url');
+ assert_optional_string_field(stats, 'relayProtocol');
+}
+
+/*
+ [webrtc-stats]
+ 7.16. RTCIceCandidatePairStats dictionary
+ dictionary RTCIceCandidatePairStats : RTCStats {
+ DOMString transportId;
+ DOMString localCandidateId;
+ DOMString remoteCandidateId;
+ RTCStatsIceCandidatePairState state;
+ boolean nominated;
+ unsigned long packetsSent;
+ unsigned long packetsReceived;
+ unsigned long long bytesSent;
+ unsigned long long bytesReceived;
+ DOMHighResTimeStamp lastPacketSentTimestamp;
+ DOMHighResTimeStamp lastPacketReceivedTimestamp;
+ DOMHighResTimeStamp firstRequestTimestamp;
+ DOMHighResTimeStamp lastRequestTimestamp;
+ DOMHighResTimeStamp lastResponseTimestamp;
+ double totalRoundTripTime;
+ double currentRoundTripTime;
+ double availableOutgoingBitrate;
+ double availableIncomingBitrate;
+ unsigned long circuitBreakerTriggerCount;
+ unsigned long long requestsReceived;
+ unsigned long long requestsSent;
+ unsigned long long responsesReceived;
+ unsigned long long responsesSent;
+ unsigned long long retransmissionsReceived;
+ unsigned long long retransmissionsSent;
+ unsigned long long consentRequestsSent;
+ DOMHighResTimeStamp consentExpiredTimestamp;
+ unsigned long packetsDiscardedOnSend;
+ unsigned long long bytesDiscardedOnSend; };
+
+ enum RTCStatsIceCandidatePairState {
+ "frozen",
+ "waiting",
+ "in-progress",
+ "failed",
+ "succeeded"
+ };
+
+ [webrtc-pc]
+ 8.6. Mandatory To Implement Stats
+ - RTCIceCandidatePairStats, with attributes transportId, localCandidateId,
+ remoteCandidateId, state, nominated, bytesSent, bytesReceived, totalRoundTripTime, currentRoundTripTime
+ // not including priority per https://github.com/w3c/webrtc-pc/issues/2457
+ */
+function validateIceCandidatePairStats(statsReport, stats) {
+ validateRtcStats(statsReport, stats);
+
+ validateIdField(statsReport, stats, 'transportId', 'transport');
+ validateIdField(statsReport, stats, 'localCandidateId', 'local-candidate');
+ validateIdField(statsReport, stats, 'remoteCandidateId', 'remote-candidate');
+
+ assert_enum_field(stats, 'state',
+ ['frozen', 'waiting', 'in-progress', 'failed', 'succeeded']);
+
+ assert_boolean_field(stats, 'nominated');
+ assert_optional_unsigned_int_field(stats, 'packetsSent');
+ assert_optional_unsigned_int_field(stats, 'packetsReceived');
+ assert_unsigned_int_field(stats, 'bytesSent');
+ assert_unsigned_int_field(stats, 'bytesReceived');
+
+ assert_optional_number_field(stats, 'lastPacketSentTimestamp');
+ assert_optional_number_field(stats, 'lastPacketReceivedTimestamp');
+ assert_optional_number_field(stats, 'firstRequestTimestamp');
+ assert_optional_number_field(stats, 'lastRequestTimestamp');
+ assert_optional_number_field(stats, 'lastResponseTimestamp');
+
+ assert_number_field(stats, 'totalRoundTripTime');
+ assert_number_field(stats, 'currentRoundTripTime');
+
+ assert_optional_number_field(stats, 'availableOutgoingBitrate');
+ assert_optional_number_field(stats, 'availableIncomingBitrate');
+
+ assert_optional_unsigned_int_field(stats, 'circuitBreakerTriggerCount');
+ assert_optional_unsigned_int_field(stats, 'requestsReceived');
+ assert_optional_unsigned_int_field(stats, 'requestsSent');
+ assert_optional_unsigned_int_field(stats, 'responsesReceived');
+ assert_optional_unsigned_int_field(stats, 'responsesSent');
+ assert_optional_unsigned_int_field(stats, 'retransmissionsReceived');
+ assert_optional_unsigned_int_field(stats, 'retransmissionsSent');
+ assert_optional_unsigned_int_field(stats, 'consentRequestsSent');
+ assert_optional_number_field(stats, 'consentExpiredTimestamp');
+ assert_optional_unsigned_int_field(stats, 'packetsDiscardedOnSend');
+ assert_optional_unsigned_int_field(stats, 'bytesDiscardedOnSend');
+}
+
+/*
+ [webrtc-stats]
+ 7.17. RTCCertificateStats dictionary
+ dictionary RTCCertificateStats : RTCStats {
+ DOMString fingerprint;
+ DOMString fingerprintAlgorithm;
+ DOMString base64Certificate;
+ DOMString issuerCertificateId;
+ };
+
+ [webrtc-pc]
+ 8.6. Mandatory To Implement Stats
+ - RTCCertificateStats, with attributes fingerprint, fingerprintAlgorithm,
+ base64Certificate, issuerCertificateId
+ */
+function validateCertificateStats(statsReport, stats) {
+ validateRtcStats(statsReport, stats);
+
+ assert_string_field(stats, 'fingerprint');
+ assert_string_field(stats, 'fingerprintAlgorithm');
+ assert_string_field(stats, 'base64Certificate');
+ assert_optional_string_field(stats, 'issuerCertificateId');
+}
+
+/*
+ [webrtc-stats]
+ 7.30. RTCIceServerStats dictionary
+ dictionary RTCIceServerStats : RTCStats {
+ DOMString url;
+ long port;
+ DOMString protocol;
+ unsigned long totalRequestsSent;
+ unsigned long totalResponsesReceived;
+ double totalRoundTripTime;
+ };
+*/
+function validateIceServerStats(statsReport, stats) {
+ validateRtcStats(statsReport, stats);
+
+ assert_optional_string_field(stats, 'url');
+ assert_optional_int_field(stats, 'port');
+ assert_optional_string_field(stats, 'protocol');
+ assert_optional_unsigned_int_field(stats, 'totalRequestsSent');
+ assert_optional_unsigned_int_field(stats, 'totalResponsesReceived');
+ assert_optional_number_field(stats, 'totalRoundTripTime');
+}
diff --git a/testing/web-platform/tests/webrtc/RTCTrackEvent-constructor.html b/testing/web-platform/tests/webrtc/RTCTrackEvent-constructor.html
new file mode 100644
index 0000000000..c9105e693a
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCTrackEvent-constructor.html
@@ -0,0 +1,159 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCTrackEvent constructor</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+ 'use strict';
+
+ // Test is based on the following editor draft:
+ // https://w3c.github.io/webrtc-pc/archives/20170605/webrtc.html
+
+ /*
+ 5.7. RTCTrackEvent
+ [Constructor(DOMString type, RTCTrackEventInit eventInitDict)]
+ interface RTCTrackEvent : Event {
+ readonly attribute RTCRtpReceiver receiver;
+ readonly attribute MediaStreamTrack track;
+ [SameObject]
+ readonly attribute FrozenArray<MediaStream> streams;
+ readonly attribute RTCRtpTransceiver transceiver;
+ };
+
+ dictionary RTCTrackEventInit : EventInit {
+ required RTCRtpReceiver receiver;
+ required MediaStreamTrack track;
+ sequence<MediaStream> streams = [];
+ required RTCRtpTransceiver transceiver;
+ };
+ */
+
+ test(t => {
+ const pc = new RTCPeerConnection();
+ const transceiver = pc.addTransceiver('audio');
+ const { receiver } = transceiver;
+ const { track } = receiver;
+
+ const trackEvent = new RTCTrackEvent('track', {
+ receiver, track, transceiver
+ });
+
+ assert_equals(trackEvent.receiver, receiver);
+ assert_equals(trackEvent.track, track);
+ assert_array_equals(trackEvent.streams, []);
+ assert_equals(trackEvent.streams, trackEvent.streams, '[SameObject]');
+ assert_equals(trackEvent.transceiver, transceiver);
+
+ assert_equals(trackEvent.type, 'track');
+ assert_false(trackEvent.bubbles);
+ assert_false(trackEvent.cancelable);
+
+ }, `new RTCTrackEvent() with valid receiver, track, transceiver should succeed`);
+
+ test(t => {
+ const pc = new RTCPeerConnection();
+ const transceiver = pc.addTransceiver('audio');
+ const { receiver } = transceiver;
+ const { track } = receiver;
+
+ const stream = new MediaStream([track]);
+
+ const trackEvent = new RTCTrackEvent('track', {
+ receiver, track, transceiver,
+ streams: [stream]
+ });
+
+ assert_equals(trackEvent.receiver, receiver);
+ assert_equals(trackEvent.track, track);
+ assert_array_equals(trackEvent.streams, [stream]);
+ assert_equals(trackEvent.transceiver, transceiver);
+
+ }, `new RTCTrackEvent() with valid receiver, track, streams, transceiver should succeed`);
+
+ test(t => {
+ const pc = new RTCPeerConnection();
+ const transceiver = pc.addTransceiver('audio');
+ const { receiver } = transceiver;
+ const { track } = receiver;
+
+ const stream1 = new MediaStream([track]);
+ const stream2 = new MediaStream([track]);
+
+ const trackEvent = new RTCTrackEvent('track', {
+ receiver, track, transceiver,
+ streams: [stream1, stream2]
+ });
+
+ assert_equals(trackEvent.receiver, receiver);
+ assert_equals(trackEvent.track, track);
+ assert_array_equals(trackEvent.streams, [stream1, stream2]);
+ assert_equals(trackEvent.transceiver, transceiver);
+
+ }, `new RTCTrackEvent() with valid receiver, track, multiple streams, transceiver should succeed`);
+
+ test(t => {
+ const pc = new RTCPeerConnection();
+ const transceiver = pc.addTransceiver('audio');
+ const receiver = pc.addTransceiver('audio').receiver;
+ const track = pc.addTransceiver('audio').receiver.track;
+
+ const stream = new MediaStream();
+
+ const trackEvent = new RTCTrackEvent('track', {
+ receiver, track, transceiver,
+ streams: [stream]
+ });
+
+ assert_equals(trackEvent.receiver, receiver);
+ assert_equals(trackEvent.track, track);
+ assert_array_equals(trackEvent.streams, [stream]);
+ assert_equals(trackEvent.transceiver, transceiver);
+
+ }, `new RTCTrackEvent() with unrelated receiver, track, streams, transceiver should succeed`);
+
+ test(t => {
+ const pc = new RTCPeerConnection();
+ const transceiver = pc.addTransceiver('audio');
+ const { receiver } = transceiver;
+ const { track } = receiver;
+
+ assert_throws_js(TypeError, () =>
+ new RTCTrackEvent('track', {
+ receiver, track
+ }));
+
+ }, `new RTCTrackEvent() with no transceiver should throw TypeError`);
+
+ test(t => {
+ const pc = new RTCPeerConnection();
+ const transceiver = pc.addTransceiver('audio');
+ const { receiver } = transceiver;
+
+ assert_throws_js(TypeError, () =>
+ new RTCTrackEvent('track', {
+ receiver, transceiver
+ }));
+
+ }, `new RTCTrackEvent() with no track should throw TypeError`);
+
+ test(t => {
+ const pc = new RTCPeerConnection();
+ const transceiver = pc.addTransceiver('audio');
+ const { receiver } = transceiver;
+ const { track } = receiver;
+
+ assert_throws_js(TypeError, () =>
+ new RTCTrackEvent('track', {
+ track, transceiver
+ }));
+
+ }, `new RTCTrackEvent() with no receiver should throw TypeError`);
+
+ /*
+ Coverage Report
+ Interface tests are counted as 1 trivial test
+
+ Tested 1
+ Total 1
+ */
+</script>
diff --git a/testing/web-platform/tests/webrtc/RTCTrackEvent-fire.html b/testing/web-platform/tests/webrtc/RTCTrackEvent-fire.html
new file mode 100644
index 0000000000..9435d7b6e5
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCTrackEvent-fire.html
@@ -0,0 +1,168 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Change of msid in remote description should trigger related track events</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+const sdpBase =`v=0
+o=- 5511237691691746 2 IN IP4 127.0.0.1
+s=-
+t=0 0
+a=group:BUNDLE 0
+a=ice-options:trickle
+a=ice-lite
+a=msid-semantic:WMS *
+m=audio 9 UDP/TLS/RTP/SAVPF 111 103 9 102 0 8 105 13 110 113 126
+c=IN IP6 ::
+a=rtcp:9 IN IP6 ::
+a=rtcp-mux
+a=mid:0
+a=sendrecv
+a=ice-ufrag:z0i8R3C9C4hPRWls
+a=ice-pwd:O7bPpOFAqasqoidV4yxnFVbc
+a=ice-lite
+a=fingerprint:sha-256 B7:9C:0D:C9:D1:42:57:97:82:4D:F9:B7:93:75:49:C3:42:21:5A:DD:9C:B5:ED:53:53:F0:B4:C8:AE:88:7A:E7
+a=setup:actpass
+a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
+a=extmap:9 urn:ietf:params:rtp-hdrext:sdes:mid
+a=rtpmap:0 PCMU/8000`;
+
+const sdp0 = sdpBase + `
+`;
+
+const sdp1 = sdpBase + `
+a=msid:1 2
+a=ssrc:3 cname:4
+a=ssrc:3 msid:1 2
+`;
+
+const sdp2 = sdpBase + `
+a=ssrc:3 cname:4
+a=ssrc:3 msid:1 2
+`;
+
+const sdp3 = sdpBase + `
+a=msid:1 2
+a=ssrc:3 cname:4
+a=ssrc:3 msid:3 2
+`;
+
+const sdp4 = sdp1.replace('msid-semantic', 'unknownattr');
+
+const sdp5 = sdpBase + `
+a=msid:-
+`;
+
+const sdp6 = sdpBase + `
+a=msid:1 2
+a=msid:1 2
+`;
+
+async function applyRemoteDescriptionAndReturnRemoteTrackAndStreams(pc, sdp)
+{
+ const testTrackPromise = new Promise(resolve => {
+ pc.ontrack = (event) => { resolve([event.track, event.streams]); };
+ });
+ await pc.setRemoteDescription({type: 'offer', sdp: sdp});
+ return testTrackPromise;
+}
+
+promise_test(async test => {
+ const pc = new RTCPeerConnection();
+ test.add_cleanup(() => pc.close());
+
+ const [track, streams] = await applyRemoteDescriptionAndReturnRemoteTrackAndStreams(pc, sdp0);
+ assert_equals(streams.length, 1, "track event has a stream");
+}, "When a=msid is absent, the track should still be associated with a stream");
+
+promise_test(async test => {
+ const pc = new RTCPeerConnection();
+ test.add_cleanup(() => pc.close());
+
+ const [track, streams] = await applyRemoteDescriptionAndReturnRemoteTrackAndStreams(pc, sdp1);
+ assert_equals(streams.length, 1, "track event has a stream");
+ assert_equals(streams[0].id, "1", "msid should match");
+}, "Source-level msid should be ignored if media-level msid is present");
+
+promise_test(async test => {
+ const pc = new RTCPeerConnection();
+ test.add_cleanup(() => pc.close());
+
+ const [track, streams] = await applyRemoteDescriptionAndReturnRemoteTrackAndStreams(pc, sdp2);
+ assert_equals(streams.length, 1, "track event has a stream");
+ assert_equals(streams[0].id, "1", "msid should match");
+}, "Source-level msid should be parsed if media-level msid is absent");
+
+promise_test(async test => {
+ const pc = new RTCPeerConnection();
+ test.add_cleanup(() => pc.close());
+
+ let track;
+ let streams;
+ try {
+ [track, streams] = await applyRemoteDescriptionAndReturnRemoteTrackAndStreams(pc, sdp3);
+ } catch (e) {
+ return;
+ }
+ assert_equals(streams.length, 1, "track event has a stream");
+ assert_equals(streams[0].id, "1", "msid should match");
+}, "Source-level msid should be ignored, or an error should be thrown, if a different media-level msid is present");
+
+promise_test(async test => {
+ const pc = new RTCPeerConnection();
+ test.add_cleanup(() => pc.close());
+
+ const [track, streams] = await applyRemoteDescriptionAndReturnRemoteTrackAndStreams(pc, sdp4);
+ assert_equals(streams.length, 1, "track event has a stream");
+ assert_equals(streams[0].id, "1", "msid should match");
+}, "stream ids should be found even if msid-semantic is absent");
+
+promise_test(async test => {
+ const pc = new RTCPeerConnection();
+ test.add_cleanup(() => pc.close());
+
+ const [track, streams] = await applyRemoteDescriptionAndReturnRemoteTrackAndStreams(pc, sdp5);
+ assert_equals(streams.length, 0, "track event has no stream");
+}, "a=msid:- should result in a track event with no streams");
+
+promise_test(async test => {
+ const pc = new RTCPeerConnection();
+ test.add_cleanup(() => pc.close());
+
+ const [track, streams] = await applyRemoteDescriptionAndReturnRemoteTrackAndStreams(pc, sdp6);
+ assert_equals(streams.length, 1, "track event has one stream");
+}, "Duplicate a=msid should result in a track event with one stream");
+
+promise_test(async test => {
+ const pc = new RTCPeerConnection();
+ test.add_cleanup(() => pc.close());
+
+ const [track, streams] = await applyRemoteDescriptionAndReturnRemoteTrackAndStreams(pc, sdp1);
+ assert_equals(streams.length, 1, "track event has a stream");
+ assert_equals(streams[0].id, "1", "msid should match");
+ const stream = streams[0];
+
+ await pc.setLocalDescription(await pc.createAnswer());
+
+ const testTrackPromise = new Promise((resolve) => { stream.onremovetrack = resolve; });
+ await pc.setRemoteDescription({type: 'offer', 'sdp': sdp0});
+ await testTrackPromise;
+
+ assert_equals(stream.getAudioTracks().length, 0, "stream should be empty");
+}, "Applying a remote description with removed msid should trigger firing a removetrack event on the corresponding stream");
+
+promise_test(async test => {
+ const pc = new RTCPeerConnection();
+ test.add_cleanup(() => pc.close());
+
+ let [track0, streams0] = await applyRemoteDescriptionAndReturnRemoteTrackAndStreams(pc, sdp0);
+
+ await pc.setLocalDescription(await pc.createAnswer());
+
+ let [track1, streams1] = await applyRemoteDescriptionAndReturnRemoteTrackAndStreams(pc, sdp1);
+
+ assert_equals(streams1.length, 1, "track event has a stream");
+ assert_equals(streams1[0].id, "1", "msid should match");
+ assert_equals(streams1[0].getTracks()[0], track0, "track should match");
+}, "Applying a remote description with a new msid should trigger firing an event with populated streams");
+</script>
diff --git a/testing/web-platform/tests/webrtc/RollbackEvents.https.html b/testing/web-platform/tests/webrtc/RollbackEvents.https.html
new file mode 100644
index 0000000000..25c83842c9
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RollbackEvents.https.html
@@ -0,0 +1,231 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+'use strict';
+
+['audio', 'video'].forEach((kind) => {
+ // Make sure "ontrack" fires if a prevuously rolled back track is added back.
+ promise_test(async t => {
+ const constraints = {};
+ constraints[kind] = true;
+ const stream = await navigator.mediaDevices.getUserMedia(constraints);
+ const [track] = stream.getTracks();
+ t.add_cleanup(() => track.stop());
+
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ pc1.addTrack(track, stream);
+ pc2.addTrack(track, stream);
+ const [pc1Transceiver] = pc1.getTransceivers();
+ const [pc2Transceiver] = pc2.getTransceivers();
+
+ let remoteStreamViaOnTrackPromise = getRemoteStreamViaOnTrackPromise(pc2);
+
+ // Apply remote offer, but don't complete the entire exchange.
+ await pc1.setLocalDescription();
+ await pc2.setRemoteDescription(pc1.localDescription);
+ // The addTrack-transceiver gets associated, no need for a second
+ // transceiver.
+ assert_equals(pc2.getTransceivers().length, 1);
+ const remoteStream = await remoteStreamViaOnTrackPromise;
+ assert_equals(remoteStream.id, stream.id);
+
+ const onRemoveTrackPromise = new Promise(r => {
+ remoteStream.onremovetrack = () => { r(); };
+ });
+
+ // Cause track removal due to rollback.
+ await pc2.setRemoteDescription({type:'rollback'});
+ // The track was removed.
+ await onRemoveTrackPromise;
+
+ // Sanity check that ontrack still fires if we add it back again by applying
+ // the same remote offer.
+ remoteStreamViaOnTrackPromise = getRemoteStreamViaOnTrackPromise(pc2);
+ await pc2.setRemoteDescription(pc1.localDescription);
+ const revivedRemoteStream = await remoteStreamViaOnTrackPromise;
+ // This test only expects IDs to be the same. The same stream object should
+ // also be used, but this should be covered by separate tests.
+ // TODO(https://crbug.com/1321738): Add MediaStream identity tests.
+ assert_equals(remoteStream.id, revivedRemoteStream.id);
+ // No cheating, the same transciever should be used as before.
+ assert_equals(pc2.getTransceivers().length, 1);
+ }, `[${kind}] Track with stream: removal due to disassociation in rollback and then add it back again`);
+
+ // This is the same test as above, but this time without any remote streams.
+ // This test could fail if [[FiredDirection]] was not reset in a rollback but
+ // the above version of the test might still pass due to the track being
+ // re-added to its stream.
+ promise_test(async t => {
+ const constraints = {};
+ constraints[kind] = true;
+ const stream = await navigator.mediaDevices.getUserMedia(constraints);
+ const [track] = stream.getTracks();
+ t.add_cleanup(() => track.stop());
+
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ pc1.addTrack(track);
+ pc2.addTrack(track);
+ const [pc1Transceiver] = pc1.getTransceivers();
+ const [pc2Transceiver] = pc2.getTransceivers();
+
+ let remoteTrackPromise = getTrackViaOnTrackPromise(pc2);
+
+ // Apply remote offer, but don't complete the entire exchange.
+ await pc1.setLocalDescription();
+ await pc2.setRemoteDescription(pc1.localDescription);
+ // The addTrack-transceiver gets associated, no need for a second
+ // transceiver.
+ assert_equals(pc2.getTransceivers().length, 1);
+ const remoteTrack = await remoteTrackPromise;
+ assert_not_equals(remoteTrack, null);
+
+ // Cause track removal due to rollback.
+ await pc2.setRemoteDescription({type:'rollback'});
+ // There's nothing equivalent to stream.onremovetrack when you don't have a
+ // stream, but the track should become muted (if it isn't already).
+ if (!remoteTrack.muted) {
+ await new Promise(r => remoteTrack.onmute = () => { r(); });
+ }
+ assert_equals(remoteTrack.muted, true);
+
+ // Sanity check that ontrack still fires if we add it back again by applying
+ // the same remote offer.
+ remoteTrackPromise = getTrackViaOnTrackPromise(pc2);
+ await pc2.setRemoteDescription(pc1.localDescription);
+ const revivedRemoteTrack = await remoteTrackPromise;
+ // We can be sure the same track is used, because the same transceiver is
+ // used (and transciever.receiver.track has same lifetime as transceiver).
+ assert_equals(pc2.getTransceivers().length, 1);
+ assert_equals(remoteTrack, revivedRemoteTrack);
+ }, `[${kind}] Track without stream: removal due to disassociation in rollback and then add it back`);
+
+ // Make sure "ontrack" can fire in a rollback (undo making it inactive).
+ promise_test(async t => {
+ const constraints = {};
+ constraints[kind] = true;
+ const stream = await navigator.mediaDevices.getUserMedia(constraints);
+ const [track] = stream.getTracks();
+ t.add_cleanup(() => track.stop());
+
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ pc1.addTrack(track, stream);
+ const [pc1Transceiver] = pc1.getTransceivers();
+
+ let remoteStreamViaOnTrackPromise = getRemoteStreamViaOnTrackPromise(pc2);
+
+ // Complete O/A exchange such that the transceiver gets associated.
+ await pc1.setLocalDescription();
+ await pc2.setRemoteDescription(pc1.localDescription);
+ await pc2.setLocalDescription();
+ await pc1.setRemoteDescription(pc2.localDescription);
+ const [pc2Transceiver] = pc2.getTransceivers();
+ assert_equals(pc2Transceiver.direction, 'recvonly');
+ assert_equals(pc2Transceiver.currentDirection, 'recvonly');
+
+ const remoteStream = await remoteStreamViaOnTrackPromise;
+ assert_equals(remoteStream.id, stream.id);
+ const onRemoveTrackPromise = new Promise(r => {
+ remoteStream.onremovetrack = () => { r(); };
+ });
+
+ // Cause track removal.
+ pc1Transceiver.direction = 'inactive';
+ await pc1.setLocalDescription();
+ await pc2.setRemoteDescription(pc1.localDescription);
+ // The track was removed.
+ await onRemoveTrackPromise;
+
+ // Rolling back the offer revives the track, causing ontrack to fire again.
+ remoteStreamViaOnTrackPromise = getRemoteStreamViaOnTrackPromise(pc2);
+ await pc2.setRemoteDescription({type:'rollback'});
+ const revivedRemoteStream = await remoteStreamViaOnTrackPromise;
+ // This test only expects IDs to be the same. The same stream object should
+ // also be used, but this should be covered by separate tests.
+ // TODO(https://crbug.com/1321738): Add MediaStream identity tests.
+ assert_equals(remoteStream.id, revivedRemoteStream.id);
+ }, `[${kind}] Track with stream: removal due to direction changing and then add back using rollback`);
+
+ // Same test as above but without remote streams.
+ promise_test(async t => {
+ const constraints = {};
+ constraints[kind] = true;
+ const stream = await navigator.mediaDevices.getUserMedia(constraints);
+ const [track] = stream.getTracks();
+ t.add_cleanup(() => track.stop());
+
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ pc1.addTrack(track);
+ const [pc1Transceiver] = pc1.getTransceivers();
+
+ let remoteTrackPromise = getTrackViaOnTrackPromise(pc2);
+
+ // Complete O/A exchange such that the transceiver gets associated.
+ await pc1.setLocalDescription();
+ await pc2.setRemoteDescription(pc1.localDescription);
+ await pc2.setLocalDescription();
+ await pc1.setRemoteDescription(pc2.localDescription);
+ const [pc2Transceiver] = pc2.getTransceivers();
+ assert_equals(pc2Transceiver.direction, 'recvonly');
+ assert_equals(pc2Transceiver.currentDirection, 'recvonly');
+
+ const remoteTrack = await remoteTrackPromise;
+
+ // Cause track removal.
+ pc1Transceiver.direction = 'inactive';
+ await pc1.setLocalDescription();
+ await pc2.setRemoteDescription(pc1.localDescription);
+ // There's nothing equivalent to stream.onremovetrack when you don't have a
+ // stream, but the track should become muted (if it isn't already).
+ if (!remoteTrack.muted) {
+ await new Promise(r => remoteTrack.onmute = () => { r(); });
+ }
+ assert_equals(remoteTrack.muted, true);
+
+ // Rolling back the offer revives the track, causing ontrack to fire again.
+ remoteTrackPromise = getTrackViaOnTrackPromise(pc2);
+ await pc2.setRemoteDescription({type:'rollback'});
+ const revivedRemoteTrack = await remoteTrackPromise;
+ // We can be sure the same track is used, because the same transceiver is
+ // used (and transciever.receiver.track has same lifetime as transceiver).
+ assert_equals(pc2.getTransceivers().length, 1);
+ assert_equals(remoteTrack, revivedRemoteTrack);
+ }, `[${kind}] Track without stream: removal due to direction changing and then add back using rollback`);
+});
+
+function getTrackViaOnTrackPromise(pc) {
+ return new Promise(r => {
+ pc.ontrack = e => {
+ pc.ontrack = null;
+ r(e.track);
+ };
+ });
+}
+
+function getRemoteStreamViaOnTrackPromise(pc) {
+ return new Promise(r => {
+ pc.ontrack = e => {
+ pc.ontrack = null;
+ r(e.streams[0]);
+ };
+ });
+}
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/coverage/RTCDTMFSender.txt b/testing/web-platform/tests/webrtc/coverage/RTCDTMFSender.txt
new file mode 100644
index 0000000000..aa30021323
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/coverage/RTCDTMFSender.txt
@@ -0,0 +1,122 @@
+Coverage is based on the following editor draft:
+https://w3c.github.io/webrtc-pc/archives/20170605/webrtc.html
+
+7. insertDTMF
+
+ [Trivial]
+ - The tones parameter is treated as a series of characters.
+
+ [RTCDTMFSender-insertDTMF]
+ - The characters 0 through 9, A through D, #, and * generate the associated
+ DTMF tones.
+
+ [RTCDTMFSender-insertDTMF]
+ - The characters a to d MUST be normalized to uppercase on entry and are equivalent
+ to A to D.
+
+ [RTCDTMFSender-insertDTMF]
+ - As noted in [RTCWEB-AUDIO] Section 3, support for the characters 0 through 9,
+ A through D, #, and * are required.
+
+ [RTCDTMFSender-insertDTMF]
+ - The character ',' MUST be supported, and indicates a delay of 2 seconds before
+ processing the next character in the tones parameter.
+
+ [RTCDTMFSender-insertDTMF]
+ - All other characters (and only those other characters) MUST
+ be considered unrecognized.
+
+ [Trivial]
+ - The duration parameter indicates the duration in ms to use for each character passed
+ in the tones parameters.
+
+ [RTCDTMFSender-ontonechange]
+ - The duration cannot be more than 6000 ms or less than 40 ms.
+
+ [RTCDTMFSender-ontonechange]
+ - The default duration is 100 ms for each tone.
+
+ [RTCDTMFSender-ontonechange]
+ - The interToneGap parameter indicates the gap between tones in ms. The user agent
+ clamps it to at least 30 ms. The default value is 70 ms.
+
+ [Untestable]
+ - The browser MAY increase the duration and interToneGap times to cause the times
+ that DTMF start and stop to align with the boundaries of RTP packets but it MUST
+ not increase either of them by more than the duration of a single RTP audio packet.
+
+ [Trivial]
+ When the insertDTMF() method is invoked, the user agent MUST run the following steps:
+
+ [Trivial]
+ 1. let sender be the RTCRtpSender used to send DTMF.
+
+ [Trivial]
+ 2. Let transceiver be the RTCRtpTransceiver object associated with sender.
+
+ [RTCDTMFSender-insertDTMF]
+ 3. If transceiver.stopped is true, throw an InvalidStateError.
+
+ [RTCDTMFSender-insertDTMF]
+ 4. If transceiver.currentDirection is recvonly or inactive, throw an
+ InvalidStateError.
+
+ [Trivial]
+ 5. Let tones be the method's first argument.
+
+ [RTCDTMFSender-insertDTMF]
+ 6. If tones contains any unrecognized characters, throw an InvalidCharacterError.
+
+ [RTCDTMFSender-insertDTMF]
+ 7. Set the object's toneBuffer attribute to tones.
+
+ [RTCDTMFSender-ontonechange]
+ 8. If the value of the duration parameter is less than 40, set it to 40.
+
+ [RTCDTMFSender-ontonechange-long]
+ If, on the other hand, the value is greater than 6000, set it to 6000.
+
+ [RTCDTMFSender-ontonechange]
+ 9. If the value of the interToneGap parameter is less than 30, set it to 30.
+
+ [RTCDTMFSender-ontonechange]
+ 10. If toneBuffer is an empty string, abort these steps.
+
+ [RTCDTMFSender-ontonechange]
+ 11. If a Playout task is scheduled to be run; abort these steps;
+
+ [RTCDTMFSender-ontonechange]
+ otherwise queue a task that runs the following steps (Playout task):
+
+ [RTCDTMFSender-ontonechange]
+ 1. If transceiver.stopped is true, abort these steps.
+
+ [RTCDTMFSender-ontonechange]
+ 2. If transceiver.currentDirection is recvonly or inactive, abort these steps.
+
+ [RTCDTMFSender-ontonechange]
+ 3. If toneBuffer is an empty string, fire an event named tonechange with an
+ empty string at the RTCDTMFSender object and abort these steps.
+
+ [RTCDTMFSender-ontonechange]
+ 4. Remove the first character from toneBuffer and let that character be tone.
+
+ [Untestable]
+ 5. Start playout of tone for duration ms on the associated RTP media stream,
+ using the appropriate codec.
+
+ [RTCDTMFSender-ontonechange]
+ 6. Queue a task to be executed in duration + interToneGap ms from now that
+ runs the steps labelled Playout task.
+
+ [RTCDTMFSender-ontonechange]
+ 7. Fire an event named tonechange with a string consisting of tone at the
+ RTCDTMFSender object.
+
+Coverage Report
+
+ Tested 31
+ Not Tested 0
+ Untestable 1
+
+ Total 32
diff --git a/testing/web-platform/tests/webrtc/coverage/identity.txt b/testing/web-platform/tests/webrtc/coverage/identity.txt
new file mode 100644
index 0000000000..0d1bcca7ed
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/coverage/identity.txt
@@ -0,0 +1,220 @@
+Coverage is based on the following editor draft:
+https://w3c.github.io/webrtc-pc/archives/20170605/webrtc.html
+
+9.3 Requesting Identity Assertions
+
+ [Trivial]
+ The identity assertion request process is triggered by a call to createOffer,
+ createAnswer, or getIdentityAssertion. When these calls are invoked and an
+ identity provider has been set, the following steps are executed:
+
+ [RTCPeerConnection-getIdentityAssertion]
+ 1. The RTCPeerConnection instantiates an IdP as described in Identity Provider
+ Selection and Registering an IdP Proxy. If the IdP cannot be loaded, instantiated,
+ or the IdP proxy is not registered, this process fails.
+
+ [RTCPeerConnection-getIdentityAssertion]
+ 2. The RTCPeerConnection invokes the generateAssertion method on the
+ RTCIdentityProvider methods registered by the IdP.
+
+ [RTCPeerConnection-getIdentityAssertion]
+ The RTCPeerConnection generates the contents parameter to this method as
+ described in [RTCWEB-SECURITY-ARCH]. The value of contents includes the
+ fingerprint of the certificate that was selected or generated during the
+ construction of the RTCPeerConnection. The origin parameter contains the
+ origin of the script that calls the RTCPeerConnection method that triggers
+ this behavior. The usernameHint value is the same value that is provided
+ to setIdentityProvider, if any such value was provided.
+
+ [RTCPeerConnection-getIdentityAssertion]
+ 3. The IdP proxy returns a Promise to the RTCPeerConnection. The IdP proxy is
+ expected to generate the identity assertion asynchronously.
+
+ [RTCPeerConnection-getIdentityAssertion]
+ If the user has been authenticated by the IdP, and the IdP is able to generate
+ an identity assertion, the IdP resolves the promise with an identity assertion
+ in the form of an RTCIdentityAssertionResult .
+
+ [RTCPeerConnection-getIdentityAssertion]
+ This step depends entirely on the IdP. The methods by which an IdP authenticates
+ users or generates assertions is not specified, though they could involve
+ interacting with the IdP server or other servers.
+
+ [RTCPeerConnection-getIdentityAssertion]
+ 4. If the IdP proxy produces an error or returns a promise that does not resolve
+ to a valid RTCIdentityValidationResult (see 9.5 IdP Error Handling), then
+ identity validation fails.
+
+ [Untestable]
+ 5. The RTCPeerConnection MAY store the identity assertion for use with future
+ offers or answers. If a fresh identity assertion is needed for any reason,
+ applications can create a new RTCPeerConnection.
+
+ [RTCPeerConnection-getIdentityAssertion]
+ 6. If the identity request was triggered by a createOffer() or createAnswer(),
+ then the assertion is converted to a JSON string, base64-encoded and inserted
+ into an a=identity attribute in the session description.
+
+ [RTCPeerConnection-getIdentityAssertion]
+ If assertion generation fails, then the promise for the corresponding function call
+ is rejected with a newly created OperationError.
+
+9.3.1 User Login Procedure
+ [RTCPeerConnection-getIdentityAssertion]
+ An IdP MAY reject an attempt to generate an identity assertion if it is unable to
+ verify that a user is authenticated. This might be due to the IdP not having the
+ necessary authentication information available to it (such as cookies).
+
+ [RTCPeerConnection-getIdentityAssertion]
+ Rejecting the promise returned by generateAssertion will cause the error to propagate
+ to the application. Login errors are indicated by rejecting the promise with an RTCError
+ with errorDetail set to "idp-need-login".
+
+ [RTCPeerConnection-getIdentityAssertion]
+ The URL to login at will be passed to the application in the idpLoginUrl attribute of
+ the RTCPeerConnection.
+
+ [Out of Scope]
+ An application can load the login URL in an IFRAME or popup window; the resulting page
+ then SHOULD provide the user with an opportunity to enter any information necessary to
+ complete the authorization process.
+
+ [Out of Scope]
+ Once the authorization process is complete, the page loaded in the IFRAME or popup sends
+ a message using postMessage [webmessaging] to the page that loaded it (through the
+ window.opener attribute for popups, or through window.parent for pages loaded in an IFRAME).
+ The message MUST consist of the DOMString "LOGINDONE". This message informs the application
+ that another attempt at generating an identity assertion is likely to be successful.
+
+9.4. Verifying Identity Assertions
+ The identity assertion request process involves the following asynchronous steps:
+
+ [TODO]
+ 1. The RTCPeerConnection awaits any prior identity validation. Only one identity
+ validation can run at a time for an RTCPeerConnection. This can happen because
+ the resolution of setRemoteDescription is not blocked by identity validation
+ unless there is a target peer identity.
+
+ [RTCPeerConnection-peerIdentity]
+ 2. The RTCPeerConnection loads the identity assertion from the session description
+ and decodes the base64 value, then parses the resulting JSON. The idp parameter
+ of the resulting dictionary contains a domain and an optional protocol value
+ that identifies the IdP, as described in [RTCWEB-SECURITY-ARCH].
+
+ [RTCPeerConnection-peerIdentity]
+ 3. The RTCPeerConnection instantiates the identified IdP as described in 9.1.1
+ Identity Provider Selection and 9.2 Registering an IdP Proxy. If the IdP
+ cannot be loaded, instantiated or the IdP proxy is not registered, this
+ process fails.
+
+ [RTCPeerConnection-peerIdentity]
+ 4. The RTCPeerConnection invokes the validateAssertion method registered by the IdP.
+
+ [RTCPeerConnection-peerIdentity]
+ The assertion parameter is taken from the decoded identity assertion. The origin
+ parameter contains the origin of the script that calls the RTCPeerConnection
+ method that triggers this behavior.
+
+ [RTCPeerConnection-peerIdentity]
+ 5. The IdP proxy returns a promise and performs the validation process asynchronously.
+
+ [Out of Scope]
+ The IdP proxy verifies the identity assertion using whatever means necessary.
+ Depending on the authentication protocol this could involve interacting with the
+ IdP server.
+
+ [RTCPeerConnection-peerIdentity]
+ 6. If the IdP proxy produces an error or returns a promise that does not resolve
+ to a valid RTCIdentityValidationResult (see 9.5 IdP Error Handling), then
+ identity validation fails.
+
+ [RTCPeerConnection-peerIdentity]
+ 7. Once the assertion is successfully verified, the IdP proxy resolves the promise
+ with an RTCIdentityValidationResult containing the validated identity and the
+ original contents that are the payload of the assertion.
+
+ [RTCPeerConnection-peerIdentity]
+ 8. The RTCPeerConnection decodes the contents and validates that it contains a
+ fingerprint value for every a=fingerprint attribute in the session description.
+ This ensures that the certificate used by the remote peer for communications
+ is covered by the identity assertion.
+
+ [RTCPeerConnection-peerIdentity]
+ 9. The RTCPeerConnection validates that the domain portion of the identity matches
+ the domain of the IdP as described in [RTCWEB-SECURITY-ARCH]. If this check fails
+ then the identity validation fails.
+
+ [RTCPeerConnection-peerIdentity]
+ 10. The RTCPeerConnection resolves the peerIdentity attribute with a new instance
+ of RTCIdentityAssertion that includes the IdP domain and peer identity.
+
+ [Out of Scope]
+ 11. The user agent MAY display identity information to a user in its UI. Any user
+ identity information that is displayed in this fashion MUST use a mechanism that
+ cannot be spoofed by content.
+
+ [RTCPeerConnection-peerIdentity]
+ If identity validation fails, the peerIdentity promise is rejected with a newly
+ created OperationError.
+
+ [RTCPeerConnection-peerIdentity]
+ If identity validation fails and there is a target peer identity for the
+ RTCPeerConnection, the promise returned by setRemoteDescription MUST be rejected
+ with the same DOMException.
+
+9.5. IdP Error Handling
+ [RTCPeerConnection-getIdentityAssertion]
+ - A RTCPeerConnection might be configured with an identity provider, but loading of
+ the IdP URI fails. Any procedure that attempts to invoke such an identity provider
+ and cannot load the URI fails with an RTCError with errorDetail set to
+ "idp-load-failure" and the httpRequestStatusCode attribute of the error set to the
+ HTTP status code of the response.
+
+ [Untestable]
+ - If the IdP loads fails due to the TLS certificate used for the HTTPS connection not
+ being trusted, it fails with an RTCError with errorDetail set to "idp-tls-failure".
+ This typically happens when the IdP uses certificate pinning and an intermediary
+ such as an enterprise firewall has intercepted the TLS connection.
+
+ [RTCPeerConnection-getIdentityAssertion]
+ - If the script loaded from the identity provider is not valid JavaScript or does not
+ implement the correct interfaces, it causes an IdP failure with an RTCError with
+ errorDetail set to "idp-bad-script-failure".
+
+ [TODO]
+ - An apparently valid identity provider might fail in several ways.
+
+ If the IdP token has expired, then the IdP MUST fail with an RTCError with
+ errorDetail set to "idp-token-expired".
+
+ If the IdP token is not valid, then the IdP MUST fail with an RTCError with
+ errorDetail set to "idp-token-invalid".
+
+ [Untestable]
+ - The user agent SHOULD limit the time that it allows for an IdP to 15 seconds.
+ This includes both the loading of the IdP proxy and the identity assertion
+ generation or validation. Failure to do so potentially causes the corresponding
+ operation to take an indefinite amount of time. This timer can be cancelled when
+ the IdP proxy produces a response. Expiration of this timer cases an IdP failure
+ with an RTCError with errorDetail set to "idp-timeout".
+
+ [RTCPeerConnection-getIdentityAssertion]
+ - If the identity provider requires the user to login, the operation will fail
+ RTCError with errorDetail set to "idp-need-login" and the idpLoginUrl attribute
+ of the error set to the URL that can be used to login.
+
+ [RTCPeerConnection-peerIdentity]
+ - Even when the IdP proxy produces a positive result, the procedure that uses this
+ information might still fail. Additional validation of a RTCIdentityValidationResult
+ value is still necessary. The procedure for validation of identity assertions
+ describes additional steps that are required to successfully validate the output
+ of the IdP proxy.
+
+
+Coverage Report
+
+ Tested 29
+ Not Tested 2
+ Untestable 4
+
+ Total 35
diff --git a/testing/web-platform/tests/webrtc/coverage/set-session-description.txt b/testing/web-platform/tests/webrtc/coverage/set-session-description.txt
new file mode 100644
index 0000000000..f2bb422703
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/coverage/set-session-description.txt
@@ -0,0 +1,240 @@
+Coverage Report is based on the following editor draft:
+https://w3c.github.io/webrtc-pc/archives/20170605/webrtc.html
+
+4.3.1.6 Set the RTCSessionSessionDescription
+
+ [Trivial]
+ 1. Let p be a new promise.
+
+ [Trivial]
+ 2. In parallel, start the process to apply description as described in [JSEP]
+ (section 5.5. and section 5.6.).
+
+ [Trivial]
+ 1. If the process to apply description fails for any reason, then user agent
+ MUST queue a task that runs the following steps:
+
+ [Untestable]
+ 1. If connection's [[IsClosed]] slot is true, then abort these steps.
+
+ [Untestable]
+ 2. If elements of the SDP were modified, then reject p with a newly created
+ InvalidModificationError and abort these steps.
+
+ [RTCPeerConnection-setLocalDescription-answer]
+ [RTCPeerConnection-setRemoteDescription-offer]
+ [RTCPeerConnection-setRemoteDescription-answer]
+ 3. If the description's type is invalid for the current signaling state of
+ connection as described in [JSEP] (section 5.5. and section 5.6.), then
+ reject p with a newly created InvalidStateError and abort these steps.
+
+ [RTCPeerConnection-setRemoteDescription-offer]
+ 4. If the content of description is not valid SDP syntax, then reject p
+ with an RTCError (with errorDetail set to "sdp-syntax-error" and the
+ sdpLineNumber attribute set to the line number in the SDP where the
+ syntax error was detected) and abort these steps.
+
+ [Untestable]
+ 5. If the content of description is invalid, then reject p with a newly
+ created InvalidAccessError and abort these steps.
+
+ [Untestable]
+ 6. For all other errors, for example if description cannot be applied at
+ the media layer, reject p with a newly created OperationError.
+
+ [Trivial]
+ 2. If description is applied successfully, the user agent MUST queue a task
+ that runs the following steps:
+
+ [Untestable]
+ 1. If connection's [[isClosed]] slot is true, then abort these steps.
+
+ [RTCPeerConnection-setLocalDescription]
+ 2. If description is set as a local description, then run one of the
+ following steps:
+
+ [RTCPeerConnection-setLocalDescription-offer]
+ - If description is of type "offer", set connection.pendingLocalDescription
+ to description and signaling state to have-local-offer.
+
+ [RTCPeerConnection-setLocalDescription-answer]
+ - If description is of type "answer", then this completes an offer answer
+ negotiation.
+
+ Set connection's currentLocalDescription to description and
+ currentRemoteDescription to the value of pendingRemoteDescription.
+
+ Set both pendingRemoteDescription and pendingLocalDescription to null.
+ Finally set connection's signaling state to stable
+
+ [RTCPeerConnection-setLocalDescription-rollback]
+ - If description is of type "rollback", then this is a rollback. Set
+ connection.pendingLocalDescription to null and signaling state to stable.
+
+ [RTCPeerConnection-setLocalDescription-pranswer]
+ - If description is of type "pranswer", then set
+ connection.pendingLocalDescription to description and signaling state to
+ have-local-pranswer.
+
+ [RTCPeerConnection-setRemoteDescription]
+ 3. Otherwise, if description is set as a remote description, then run one of the
+ following steps:
+
+ [RTCPeerConnection-setRemoteDescription-offer]
+ - If description is of type "offer", set connection.pendingRemoteDescription
+ attribute to description and signaling state to have-remote-offer.
+
+ [RTCPeerConnection-setRemoteDescription-answer]
+ - If description is of type "answer", then this completes an offer answer
+ negotiation.
+
+ Set connection's currentRemoteDescription to description and
+ currentLocalDescription to the value of pendingLocalDescription.
+
+ Set both pendingRemoteDescription and pendingLocalDescription to null.
+
+ Finally setconnection's signaling state to stable
+
+ [RTCPeerConnection-setRemoteDescription-rollback]
+ - If description is of type "rollback", then this is a rollback.
+ Set connection.pendingRemoteDescription to null and signaling state to stable.
+
+ [RTCPeerConnection-setRemoteDescription-rollback]
+ - If description is of type "pranswer", then set
+ connection.pendingRemoteDescription to description and signaling state
+ to have-remote-pranswer.
+
+ [RTCPeerConnection-setLocalDescription]
+ [RTCPeerConnection-setRemoteDescription]
+ 4. If connection's signaling state changed above, fire a simple event named
+ signalingstatechange at connection.
+
+ [TODO]
+ 5. If description is of type "answer", and it initiates the closure of an existing
+ SCTP association, as defined in [SCTP-SDP], Sections 10.3 and 10.4, set the value
+ of connection's [[sctpTransport]] internal slot to null.
+
+ [RTCSctpTransport]
+ 6. If description is of type "answer" or "pranswer", then run the following steps:
+
+ [RTCSctpTransport]
+ 1. If description initiates the establishment of a new SCTP association,
+ as defined in [SCTP-SDP], Sections 10.3 and 10.4, set the value of connection's
+ [[sctpTransport]] internal slot to a newly created RTCSctpTransport.
+
+ [TODO]
+ 2. If description negotiates the DTLS role of the SCTP transport, and there is an
+ RTCDataChannel with a null id, then generate an ID according to
+ [RTCWEB-DATA-PROTOCOL].
+
+ [Untestable]
+ If no available ID could be generated, then run the following steps:
+
+ [Untestable]
+ 1. Let channel be the RTCDataChannel object for which an ID could not be
+ generated.
+
+ [Untestable]
+ 2. Set channel's readyState attribute to closed.
+
+ [Untestable]
+ 3. Fire an event named error with a ResourceInUse exception at channel.
+
+ [Untestable]
+ 4. Fire a simple event named close at channel.
+
+ [TODO RTCPeerConnection-setDescription-transceiver]
+ 7. If description is set as a local description, then run the following steps for
+ each media description in description that is not yet associated with an
+ RTCRtpTransceiver object:
+
+ [TODO RTCPeerConnection-setDescription-transceiver]
+ 1. Let transceiver be the RTCRtpTransceiver used to create the media
+ description.
+
+ [TODO RTCPeerConnection-setDescription-transceiver]
+ 2. Set transceiver's mid value to the mid of the corresponding media
+ description.
+
+ [RTCPeerConnection-ontrack]
+ 8. If description is set as a remote description, then run the following steps
+ for each media description in description:
+
+ [TODO RTCPeerConnection-setDescription-transceiver]
+ 1. As described by [JSEP] (section 5.9.), attempt to find an existing
+ RTCRtpTransceiver object, transceiver, to represent the media description.
+
+ [RTCPeerConnection-ontrack]
+ 2. If no suitable transceiver is found (transceiver is unset), run the following
+ steps:
+
+ [RTCPeerConnection-ontrack]
+ 1. Create an RTCRtpSender, sender, from the media description.
+
+ [RTCPeerConnection-ontrack]
+ 2. Create an RTCRtpReceiver, receiver, from the media description.
+
+ [RTCPeerConnection-ontrack]
+ 3. Create an RTCRtpTransceiver with sender, receiver and direction, and let
+ transceiver be the result.
+
+ [RTCPeerConnection-ontrack]
+ 3. Set transceiver's mid value to the mid of the corresponding media description.
+ If the media description has no MID, and transceiver's mid is unset, generate
+ a random value as described in [JSEP] (section 5.9.).
+
+ [RTCPeerConnection-ontrack]
+ 4. If the direction of the media description is sendrecv or sendonly, and
+ transceiver.receiver.track has not yet been fired in a track event, process
+ the remote track for the media description, given transceiver.
+
+ [TODO RTCPeerConnection-setDescription-transceiver]
+ 5. If the media description is rejected, and transceiver is not already stopped,
+ stop the RTCRtpTransceiver transceiver.
+
+
+ [TODO RTCPeerConnection-setDescription-transceiver]
+ 9. If description is of type "rollback", then run the following steps:
+
+ [TODO RTCPeerConnection-setDescription-transceiver]
+ 1. If the mid value of an RTCRtpTransceiver was set to a non-null value by
+ the RTCSessionDescription that is being rolled back, set the mid value
+ of that transceiver to null, as described by [JSEP] (section 4.1.8.2.).
+
+ [TODO RTCPeerConnection-setDescription-transceiver]
+ 2. If an RTCRtpTransceiver was created by applying the RTCSessionDescription
+ that is being rolled back, and a track has not been attached to it via
+ addTrack, remove that transceiver from connection's set of transceivers,
+ as described by [JSEP] (section 4.1.8.2.).
+
+ [TODO RTCPeerConnection-setDescription-transceiver]
+ 3. Restore the value of connection's [[SctpTransport]] internal slot to its
+ value at the last stable signaling state.
+
+ [RTCPeerConnection-onnegotiationneeded]
+ 10. If connection's signaling state is now stable, update the negotiation-needed
+ flag. If connection's [[NegotiationNeeded]] slot was true both before and after
+ this update, queue a task that runs the following steps:
+
+ [Untestable]
+ 1. If connection's [[IsClosed]] slot is true, abort these steps.
+
+ [RTCPeerConnection-onnegotiationneeded]
+ 2. If connection's [[NegotiationNeeded]] slot is false, abort these steps.
+
+ [RTCPeerConnection-onnegotiationneeded]
+ 3. Fire a simple event named negotiationneeded at connection.
+
+ [Trivial]
+ 11. Resolve p with undefined.
+
+ [Trivial]
+ 3. Return p.
+
+
+Coverage Report
+
+ Tested 35
+ Not Tested 15
+ Untestable 8
+ Total 58
diff --git a/testing/web-platform/tests/webrtc/dictionary-helper.js b/testing/web-platform/tests/webrtc/dictionary-helper.js
new file mode 100644
index 0000000000..dab7e49fad
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/dictionary-helper.js
@@ -0,0 +1,101 @@
+'use strict';
+
+// Helper assertion functions to validate dictionary fields
+// on dictionary objects returned from APIs
+
+function assert_unsigned_int_field(object, field) {
+ const num = object[field];
+ assert_true(Number.isInteger(num) && (num >= 0),
+ `Expect dictionary.${field} to be unsigned integer`);
+}
+
+function assert_int_field(object, field) {
+ const num = object[field];
+ assert_true(Number.isInteger(num),
+ `Expect dictionary.${field} to be integer`);
+}
+
+function assert_string_field(object, field) {
+ const str = object[field];
+ assert_equals(typeof str, 'string',
+ `Expect dictionary.${field} to be string`);
+}
+
+function assert_number_field(object, field) {
+ const num = object[field];
+ assert_equals(typeof num, 'number',
+ `Expect dictionary.${field} to be number`);
+}
+
+function assert_boolean_field(object, field) {
+ const bool = object[field];
+ assert_equals(typeof bool, 'boolean',
+ `Expect dictionary.${field} to be boolean`);
+}
+
+function assert_array_field(object, field) {
+ assert_true(Array.isArray(object[field]),
+ `Expect dictionary.${field} to be array`);
+}
+
+function assert_dict_field(object, field) {
+ assert_equals(typeof object[field], 'object',
+ `Expect dictionary.${field} to be plain object`);
+
+ assert_not_equals(object[field], null,
+ `Expect dictionary.${field} to not be null`);
+}
+
+function assert_enum_field(object, field, validValues) {
+ assert_string_field(object, field);
+ assert_true(validValues.includes(object[field]),
+ `Expect dictionary.${field} to have one of the valid enum values: ${validValues}`);
+}
+
+function assert_optional_unsigned_int_field(object, field) {
+ if(object[field] !== undefined) {
+ assert_unsigned_int_field(object, field);
+ }
+}
+
+function assert_optional_int_field(object, field) {
+ if(object[field] !== undefined) {
+ assert_int_field(object, field);
+ }
+}
+
+function assert_optional_string_field(object, field) {
+ if(object[field] !== undefined) {
+ assert_string_field(object, field);
+ }
+}
+
+function assert_optional_number_field(object, field) {
+ if(object[field] !== undefined) {
+ assert_number_field(object, field);
+ }
+}
+
+function assert_optional_boolean_field(object, field) {
+ if(object[field] !== undefined) {
+ assert_boolean_field(object, field);
+ }
+}
+
+function assert_optional_array_field(object, field) {
+ if(object[field] !== undefined) {
+ assert_array_field(object, field);
+ }
+}
+
+function assert_optional_dict_field(object, field) {
+ if(object[field] !== undefined) {
+ assert_dict_field(object, field);
+ }
+}
+
+function assert_optional_enum_field(object, field, validValues) {
+ if(object[field] !== undefined) {
+ assert_enum_field(object, field, validValues);
+ }
+}
diff --git a/testing/web-platform/tests/webrtc/getstats.html b/testing/web-platform/tests/webrtc/getstats.html
new file mode 100644
index 0000000000..d6a692bb78
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/getstats.html
@@ -0,0 +1,130 @@
+<!doctype html>
+<!--
+This test uses data only, and thus does not require fake media devices.
+-->
+
+<html>
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+ <title>RTCPeerConnection GetStats</title>
+</head>
+<body>
+ <div id="log"></div>
+ <h2>Retrieved stats info</h2>
+ <pre>
+ <input type="button" onclick="showStats()" value="Show stats"></input>
+ <div id="stats">
+ </div>
+ </pre>
+
+ <!-- These files are in place when executing on W3C. -->
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script type="text/javascript">
+ var test = async_test('Can get stats from a basic WebRTC call.');
+ var statsToShow;
+ var gFirstConnection = null;
+ var gSecondConnection = null;
+
+ var onIceCandidateToFirst = test.step_func(function(event) {
+ gSecondConnection.addIceCandidate(event.candidate);
+ });
+
+ var onIceCandidateToSecond = test.step_func(function(event) {
+ gFirstConnection.addIceCandidate(event.candidate);
+ });
+
+ var getStatsRecordByType = function(stats, type) {
+ for (let stat of stats.values()) {
+ if (stat.type == type) {
+ return stat;
+ }
+ }
+ return null;
+ }
+
+ var onIceConnectionStateChange = test.step_func(function(event) {
+ // Wait until connection is established.
+ // Note - not all browsers reach 'completed' state, so we're
+ // checking for 'connected' state instead.
+ if (gFirstConnection.iceConnectionState != 'connected') {
+ return;
+ }
+ gFirstConnection.getStats()
+ .then(function(report) {
+ let reportDictionary = {};
+ for (let stats of report.values()) {
+ reportDictionary[stats.id] = stats;
+ }
+ statsToShow = JSON.stringify(reportDictionary, null, 2);
+ // Check the stats properties.
+ assert_not_equals(report, null, 'No report');
+ let sessionStat = getStatsRecordByType(report, 'peer-connection');
+ assert_not_equals(sessionStat, null, 'Did not find peer-connection stats');
+ assert_own_property(sessionStat, 'dataChannelsOpened', 'no dataChannelsOpened stat');
+ // Once every 4000 or so tests, the datachannel won't be opened when the getStats
+ // function is done, so allow both 0 and 1 datachannels.
+ assert_true(sessionStat.dataChannelsOpened == 1 || sessionStat.dataChannelsOpened == 0,
+ 'dataChannelsOpened count wrong');
+ test.done();
+ })
+ .catch(test.step_func(function(e) {
+ assert_unreached(e.name + ': ' + e.message + ': ');
+ }));
+ });
+
+ // This function starts the test.
+ test.step(function() {
+ gFirstConnection = new RTCPeerConnection(null);
+ test.add_cleanup(() => gFirstConnection.close());
+ gFirstConnection.onicecandidate = onIceCandidateToFirst;
+ gFirstConnection.oniceconnectionstatechange = onIceConnectionStateChange;
+
+ gSecondConnection = new RTCPeerConnection(null);
+ test.add_cleanup(() => gSecondConnection.close());
+ gSecondConnection.onicecandidate = onIceCandidateToSecond;
+
+ // The createDataChannel is necessary and sufficient to make
+ // sure the ICE connection be attempted.
+ gFirstConnection.createDataChannel('channel');
+ var atStep = 'Create offer';
+
+ gFirstConnection.createOffer()
+ .then(function(offer) {
+ atStep = 'Set local description at first';
+ return gFirstConnection.setLocalDescription(offer);
+ })
+ .then(function() {
+ atStep = 'Set remote description at second';
+ return gSecondConnection.setRemoteDescription(
+ gFirstConnection.localDescription);
+ })
+ .then(function() {
+ atStep = 'Create answer';
+ return gSecondConnection.createAnswer();
+ })
+ .then(function(answer) {
+ atStep = 'Set local description at second';
+ return gSecondConnection.setLocalDescription(answer);
+ })
+ .then(function() {
+ atStep = 'Set remote description at first';
+ return gFirstConnection.setRemoteDescription(
+ gSecondConnection.localDescription);
+ })
+ .catch(test.step_func(function(e) {
+ assert_unreached('Error ' + e.name + ': ' + e.message +
+ ' happened at step ' + atStep);
+ }));
+ });
+
+ function showStats() {
+ // Show the retrieved stats info
+ var showStats = document.getElementById('stats');
+ showStats.innerHTML = statsToShow;
+ }
+
+</script>
+
+</body>
+</html>
diff --git a/testing/web-platform/tests/webrtc/historical.html b/testing/web-platform/tests/webrtc/historical.html
new file mode 100644
index 0000000000..ae7a29dec0
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/historical.html
@@ -0,0 +1,51 @@
+<!doctype html>
+<title>Historical WebRTC features</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+[
+ 'reliable',
+ 'maxRetransmitTime',
+].forEach((member) => {
+ test(() => {
+ assert_false(member in RTCDataChannel.prototype);
+ }, `RTCDataChannel member ${member} should not exist`);
+});
+
+[
+ "addStream",
+ "createDTMFSender",
+ "getLocalStreams",
+ "getRemoteStreams",
+ "getStreamById",
+ "onaddstream",
+ "onremovestream",
+ "removeStream",
+ "updateIce",
+].forEach(function(name) {
+ test(function() {
+ assert_false(name in RTCPeerConnection.prototype);
+ }, "RTCPeerConnection member " + name + " should not exist");
+});
+
+[
+ "setDirection",
+].forEach(function(name) {
+ test(function() {
+ assert_false(name in RTCRtpTransceiver.prototype);
+ }, "RTCRtpTransceiver member " + name + " should not exist");
+});
+
+[
+ "DataChannel",
+ "mozRTCIceCandidate",
+ "mozRTCPeerConnection",
+ "mozRTCSessionDescription",
+ "webkitRTCPeerConnection",
+].forEach(function(name) {
+ test(function() {
+ assert_false(name in window);
+ }, name + " interface should not exist");
+});
+</script>
diff --git a/testing/web-platform/tests/webrtc/idlharness.https.window.js b/testing/web-platform/tests/webrtc/idlharness.https.window.js
new file mode 100644
index 0000000000..98685f1cd1
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/idlharness.https.window.js
@@ -0,0 +1,146 @@
+// META: script=/resources/WebIDLParser.js
+// META: script=/resources/idlharness.js
+// META: script=./RTCPeerConnection-helper.js
+// META: timeout=long
+
+'use strict';
+
+// The following helper functions are called from RTCPeerConnection-helper.js:
+// generateAnswer()
+// getNoiseStream()
+
+// Put the global IDL test objects under a parent object.
+// This allows easier search for the test cases when
+// viewing the web page
+const idlTestObjects = {};
+
+// Helper function to create RTCTrackEvent object
+function initTrackEvent() {
+ const pc = new RTCPeerConnection();
+ const transceiver = pc.addTransceiver('audio');
+ const { sender, receiver } = transceiver;
+ const { track } = receiver;
+ return new RTCTrackEvent('track', {
+ receiver, track, transceiver
+ });
+}
+
+// List of async test driver functions
+const asyncInitTasks = [
+ asyncInitCertificate,
+ asyncInitTransports,
+ asyncInitMediaStreamTrack,
+];
+
+// Asynchronously generate an RTCCertificate
+function asyncInitCertificate() {
+ return RTCPeerConnection.generateCertificate({
+ name: 'RSASSA-PKCS1-v1_5',
+ modulusLength: 2048,
+ publicExponent: new Uint8Array([1, 0, 1]),
+ hash: 'SHA-256'
+ }).then(cert => {
+ idlTestObjects.certificate = cert;
+ });
+}
+
+// Asynchronously generate instances of
+// RTCSctpTransport, RTCDtlsTransport,
+// and RTCIceTransport
+function asyncInitTransports() {
+ const pc = new RTCPeerConnection();
+ pc.createDataChannel('test');
+
+ // setting answer description initializes pc.sctp
+ return pc.createOffer()
+ .then(offer =>
+ pc.setLocalDescription(offer)
+ .then(() => generateAnswer(offer)))
+ .then(answer => pc.setRemoteDescription(answer))
+ .then(() => {
+ const sctpTransport = pc.sctp;
+ assert_true(sctpTransport instanceof RTCSctpTransport,
+ 'Expect pc.sctp to be instance of RTCSctpTransport');
+ idlTestObjects.sctpTransport = sctpTransport;
+
+ const dtlsTransport = sctpTransport.transport;
+ assert_true(dtlsTransport instanceof RTCDtlsTransport,
+ 'Expect sctpTransport.transport to be instance of RTCDtlsTransport');
+ idlTestObjects.dtlsTransport = dtlsTransport;
+
+ const iceTransport = dtlsTransport.iceTransport;
+ assert_true(iceTransport instanceof RTCIceTransport,
+ 'Expect sctpTransport.transport to be instance of RTCDtlsTransport');
+ idlTestObjects.iceTransport = iceTransport;
+ });
+}
+
+// Asynchoronously generate MediaStreamTrack from getUserMedia
+function asyncInitMediaStreamTrack() {
+ return getNoiseStream({ audio: true })
+ .then(mediaStream => {
+ idlTestObjects.mediaStreamTrack = mediaStream.getTracks()[0];
+ });
+}
+
+// Run all async test drivers, report and swallow any error
+// thrown/rejected. Proper test for correct initialization
+// of the objects are done in their respective test files.
+function asyncInit() {
+ return Promise.all(asyncInitTasks.map(
+ task => {
+ const t = async_test(`Test driver for ${task.name}`);
+ let promise;
+ t.step(() => {
+ promise = task().then(
+ t.step_func_done(),
+ t.step_func(err =>
+ assert_unreached(`Failed to run ${task.name}: ${err}`)));
+ });
+ return promise;
+ }));
+}
+
+idl_test(
+ ['webrtc'],
+ ['webidl', 'mediacapture-streams', 'hr-time', 'dom', 'html'],
+ async idlArray => {
+ idlArray.add_objects({
+ RTCPeerConnection: [`new RTCPeerConnection()`],
+ RTCSessionDescription: [`new RTCSessionDescription({ type: 'offer' })`],
+ RTCIceCandidate: [`new RTCIceCandidate({ sdpMid: 1 })`],
+ RTCDataChannel: [`new RTCPeerConnection().createDataChannel('')`],
+ RTCRtpTransceiver: [`new RTCPeerConnection().addTransceiver('audio')`],
+ RTCRtpSender: [`new RTCPeerConnection().addTransceiver('audio').sender`],
+ RTCRtpReceiver: [`new RTCPeerConnection().addTransceiver('audio').receiver`],
+ RTCPeerConnectionIceEvent: [`new RTCPeerConnectionIceEvent('ice')`],
+ RTCPeerConnectionIceErrorEvent: [
+ `new RTCPeerConnectionIceErrorEvent('ice-error', { port: 0, errorCode: 701 });`
+ ],
+ RTCTrackEvent: [`initTrackEvent()`],
+ RTCErrorEvent: [`new RTCErrorEvent('error')`],
+ RTCDataChannelEvent: [
+ `new RTCDataChannelEvent('channel', {
+ channel: new RTCPeerConnection().createDataChannel('')
+ })`
+ ],
+ // Async initialized objects below
+ RTCCertificate: ['idlTestObjects.certificate'],
+ RTCSctpTransport: ['idlTestObjects.sctpTransport'],
+ RTCDtlsTransport: ['idlTestObjects.dtlsTransport'],
+ RTCIceTransport: ['idlTestObjects.iceTransport'],
+ MediaStreamTrack: ['idlTestObjects.mediaStreamTrack'],
+ });
+ /*
+ TODO
+ RTCRtpContributingSource
+ RTCRtpSynchronizationSource
+ RTCDTMFSender
+ RTCDTMFToneChangeEvent
+ RTCIdentityProviderRegistrar
+ RTCIdentityAssertion
+ */
+
+ await asyncInit();
+ }
+);
diff --git a/testing/web-platform/tests/webrtc/legacy/README.txt b/testing/web-platform/tests/webrtc/legacy/README.txt
new file mode 100644
index 0000000000..8adbf6aa17
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/legacy/README.txt
@@ -0,0 +1,2 @@
+This directory contains files that test for behavior relevant to webrtc,
+particularly defined in https://w3c.github.io/webrtc-pc/#legacy-interface-extensions
diff --git a/testing/web-platform/tests/webrtc/legacy/RTCPeerConnection-createOffer-offerToReceive.html b/testing/web-platform/tests/webrtc/legacy/RTCPeerConnection-createOffer-offerToReceive.html
new file mode 100644
index 0000000000..f710498e75
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/legacy/RTCPeerConnection-createOffer-offerToReceive.html
@@ -0,0 +1,274 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Test legacy offerToReceiveAudio/Video options</title>
+<link rel="help" href="https://w3c.github.io/webrtc-pc/#legacy-configuration-extensions">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../RTCPeerConnection-helper.js"></script>
+<script>
+ 'use strict';
+
+ /*
+ * 4.3.3.2 Configuration data extensions
+ * partial dictionary RTCOfferOptions
+ */
+
+ /*
+ * offerToReceiveAudio of type boolean
+ * When this is given a non-false value, no outgoing track of type
+ * "audio" is attached to the PeerConnection, and the existing
+ * localDescription (if any) doesn't contain any sendrecv or recv
+ * audio media sections, createOffer() will behave as if
+ * addTransceiver("audio") had been called once prior to the createOffer() call.
+ */
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+
+ t.add_cleanup(() => pc.close());
+
+ return pc.createOffer({ offerToReceiveAudio: true })
+ .then(offer1 => {
+ assert_equals(countAudioLine(offer1.sdp), 1,
+ 'Expect created offer to have audio line');
+
+ // The first createOffer implicitly calls addTransceiver('audio'),
+ // so all following offers will also have audio media section
+ // in their SDP.
+ return pc.createOffer({ offerToReceiveAudio: false })
+ .then(offer2 => {
+ assert_equals(countAudioLine(offer2.sdp), 1,
+ 'Expect audio line to remain in created offer');
+ })
+ });
+ }, 'createOffer() with offerToReceiveAudio should add audio line to all subsequent created offers');
+
+ /*
+ * offerToReceiveVideo of type boolean
+ * When this is given a non-false value, and no outgoing track
+ * of type "video" is attached to the PeerConnection, and the
+ * existing localDescription (if any) doesn't contain any sendecv
+ * or recv video media sections, createOffer() will behave as if
+ * addTransceiver("video") had been called prior to the createOffer() call.
+ */
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+
+ t.add_cleanup(() => pc.close());
+
+ return pc.createOffer({ offerToReceiveVideo: true })
+ .then(offer1 => {
+ assert_equals(countVideoLine(offer1.sdp), 1,
+ 'Expect created offer to have video line');
+
+ return pc.createOffer({ offerToReceiveVideo: false })
+ .then(offer2 => {
+ assert_equals(countVideoLine(offer2.sdp), 1,
+ 'Expect video line to remain in created offer');
+ })
+ });
+ }, 'createOffer() with offerToReceiveVideo should add video line to all subsequent created offers');
+
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+
+ t.add_cleanup(() => pc.close());
+
+ return pc.createOffer({
+ offerToReceiveAudio: true,
+ offerToReceiveVideo: false
+ }).then(offer1 => {
+ assert_equals(countAudioLine(offer1.sdp), 1,
+ 'Expect audio line to be found in created offer');
+
+ assert_equals(countVideoLine(offer1.sdp), 0,
+ 'Expect video line to not be found in create offer');
+
+ return pc.createOffer({
+ offerToReceiveAudio: false,
+ offerToReceiveVideo: true
+ }).then(offer2 => {
+ assert_equals(countAudioLine(offer2.sdp), 1,
+ 'Expect audio line to remain in created offer');
+
+ assert_equals(countVideoLine(offer2.sdp), 1,
+ 'Expect video line to be found in create offer');
+ })
+ });
+ }, 'createOffer() with offerToReceiveAudio:true, then with offerToReceiveVideo:true, should have result offer with both audio and video line');
+
+
+ // Run some tests for both audio and video kinds
+ ['audio', 'video'].forEach((kind) => {
+ const capsKind = kind[0].toUpperCase() + kind.slice(1);
+
+ const offerToReceiveTrue = {};
+ offerToReceiveTrue[`offerToReceive${capsKind}`] = true;
+
+ const offerToReceiveFalse = {};
+ offerToReceiveFalse[`offerToReceive${capsKind}`] = false;
+
+ // Start testing
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const dummy = pc.createDataChannel('foo'); // Just to have something to offer
+
+ return pc.createOffer(offerToReceiveFalse)
+ .then(() => {
+ assert_equals(pc.getTransceivers().length, 0,
+ 'Expect pc to have no transceivers');
+ });
+ }, `createOffer() with offerToReceive${capsKind} set to false should not create a transceiver`);
+
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+
+ t.add_cleanup(() => pc.close());
+
+ return pc.createOffer(offerToReceiveTrue)
+ .then(() => {
+ assert_equals(pc.getTransceivers().length, 1,
+ 'Expect pc to have one transceiver');
+
+ const transceiver = pc.getTransceivers()[0];
+ assert_equals(transceiver.direction, 'recvonly',
+ 'Expect transceiver to have "recvonly" direction');
+ });
+ }, `createOffer() with offerToReceive${capsKind} should create a "recvonly" transceiver`);
+
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+
+ t.add_cleanup(() => pc.close());
+
+ return pc.createOffer(offerToReceiveTrue)
+ .then(() => {
+ assert_equals(pc.getTransceivers().length, 1,
+ 'Expect pc to have one transceiver');
+
+ const transceiver = pc.getTransceivers()[0];
+ assert_equals(transceiver.direction, 'recvonly',
+ 'Expect transceiver to have "recvonly" direction');
+ })
+ .then(() => pc.createOffer(offerToReceiveTrue))
+ .then(() => {
+ assert_equals(pc.getTransceivers().length, 1,
+ 'Expect pc to still have only one transceiver');
+ })
+ ;
+ }, `offerToReceive${capsKind} option should be ignored if a non-stopped "recvonly" transceiver exists`);
+
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+
+ t.add_cleanup(() => pc.close());
+
+ return getTrackFromUserMedia(kind)
+ .then(([track, stream]) => {
+ pc.addTrack(track, stream);
+ return pc.createOffer();
+ })
+ .then(() => {
+ assert_equals(pc.getTransceivers().length, 1,
+ 'Expect pc to have one transceiver');
+
+ const transceiver = pc.getTransceivers()[0];
+ assert_equals(transceiver.direction, 'sendrecv',
+ 'Expect transceiver to have "sendrecv" direction');
+ })
+ .then(() => pc.createOffer(offerToReceiveTrue))
+ .then(() => {
+ assert_equals(pc.getTransceivers().length, 1,
+ 'Expect pc to still have only one transceiver');
+ })
+ ;
+ }, `offerToReceive${capsKind} option should be ignored if a non-stopped "sendrecv" transceiver exists`);
+
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+
+ t.add_cleanup(() => pc.close());
+
+ return getTrackFromUserMedia(kind)
+ .then(([track, stream]) => {
+ pc.addTrack(track, stream);
+ return pc.createOffer(offerToReceiveFalse);
+ })
+ .then(() => {
+ assert_equals(pc.getTransceivers().length, 1,
+ 'Expect pc to have one transceiver');
+
+ const transceiver = pc.getTransceivers()[0];
+ assert_equals(transceiver.direction, 'sendonly',
+ 'Expect transceiver to have "sendonly" direction');
+ })
+ ;
+ }, `offerToReceive${capsKind} set to false with a track should create a "sendonly" transceiver`);
+
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+
+ t.add_cleanup(() => pc.close());
+
+ pc.addTransceiver(kind, {direction: 'recvonly'});
+
+ return pc.createOffer(offerToReceiveFalse)
+ .then(() => {
+ assert_equals(pc.getTransceivers().length, 1,
+ 'Expect pc to have one transceiver');
+
+ const transceiver = pc.getTransceivers()[0];
+ assert_equals(transceiver.direction, 'inactive',
+ 'Expect transceiver to have "inactive" direction');
+ })
+ ;
+ }, `offerToReceive${capsKind} set to false with a "recvonly" transceiver should change the direction to "inactive"`);
+
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const pc2 = new RTCPeerConnection();
+
+ t.add_cleanup(() => pc2.close());
+
+ return getTrackFromUserMedia(kind)
+ .then(([track, stream]) => {
+ pc.addTrack(track, stream);
+ return pc.createOffer();
+ })
+ .then((offer) => pc.setLocalDescription(offer))
+ .then(() => pc2.setRemoteDescription(pc.localDescription))
+ .then(() => pc2.createAnswer())
+ .then((answer) => pc2.setLocalDescription(answer))
+ .then(() => pc.setRemoteDescription(pc2.localDescription))
+ .then(() => pc.createOffer(offerToReceiveFalse))
+ .then((offer) => {
+ assert_equals(pc.getTransceivers().length, 1,
+ 'Expect pc to have one transceiver');
+
+ const transceiver = pc.getTransceivers()[0];
+ assert_equals(transceiver.direction, 'sendonly',
+ 'Expect transceiver to have "sendonly" direction');
+ })
+ ;
+ }, `subsequent offerToReceive${capsKind} set to false with a track should change the direction to "sendonly"`);
+ });
+
+ promise_test(t => {
+ const pc = new RTCPeerConnection();
+
+ t.add_cleanup(() => pc.close());
+
+ return pc.createOffer({ offerToReceiveAudio: true, offerToReceiveVideo: true })
+ .then(() => {
+ assert_equals(pc.getTransceivers().length, 2,
+ 'Expect pc to have two transceivers');
+
+ assert_equals(pc.getTransceivers()[0].direction, 'recvonly',
+ 'Expect first transceiver to have "recvonly" direction');
+ assert_equals(pc.getTransceivers()[1].direction, 'recvonly',
+ 'Expect second transceiver to have "recvonly" direction');
+ });
+ }, 'offerToReceiveAudio and Video should create two "recvonly" transceivers');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/legacy/RTCRtpTransceiver-with-OfferToReceive-options.https.html b/testing/web-platform/tests/webrtc/legacy/RTCRtpTransceiver-with-OfferToReceive-options.https.html
new file mode 100644
index 0000000000..65a4d7e393
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/legacy/RTCRtpTransceiver-with-OfferToReceive-options.https.html
@@ -0,0 +1,172 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCRtpTransceiver with OfferToReceive legacy options</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src=/resources/testdriver.js></script>
+<script src=/resources/testdriver-vendor.js></script>
+<script src='/mediacapture-streams/permission-helper.js'></script>
+<script src="../RTCPeerConnection-helper.js"></script>
+<script>
+ 'use strict';
+
+ const stopTracks = (...streams) => {
+ streams.forEach(stream => stream.getTracks().forEach(track => track.stop()));
+ };
+
+ // comparable() - produces copy of object that is JSON comparable.
+ // o = original object (required)
+ // t = template of what to examine. Useful if o is non-enumerable (optional)
+
+ const comparable = (o, t = o) => {
+ if (typeof o != 'object' || !o) {
+ return o;
+ }
+ if (Array.isArray(t) && Array.isArray(o)) {
+ return o.map((n, i) => comparable(n, t[i]));
+ }
+ return Object.keys(t).sort()
+ .reduce((r, key) => (r[key] = comparable(o[key], t[key]), r), {});
+ };
+
+ const stripKeyQuotes = s => s.replace(/"(\w+)":/g, "$1:");
+
+ const hasProps = (observed, expected) => {
+ const observable = comparable(observed, expected);
+ assert_equals(stripKeyQuotes(JSON.stringify(observable)),
+ stripKeyQuotes(JSON.stringify(comparable(expected))));
+ };
+
+ const checkAddTransceiverWithStream = async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ await setMediaPermission();
+ const audioStream = await navigator.mediaDevices.getUserMedia({audio: true});
+ const videoStream = await navigator.mediaDevices.getUserMedia({video: true});
+ t.add_cleanup(() => stopTracks(audioStream, videoStream));
+
+ const audio = audioStream.getAudioTracks()[0];
+ const video = videoStream.getVideoTracks()[0];
+
+ pc.addTransceiver(audio, {streams: [audioStream]});
+ pc.addTransceiver(video, {streams: [videoStream]});
+
+ hasProps(pc.getTransceivers(),
+ [
+ {
+ receiver: {track: {kind: "audio"}},
+ sender: {track: audio},
+ direction: "sendrecv",
+ mid: null,
+ currentDirection: null,
+ stopped: false
+ },
+ {
+ receiver: {track: {kind: "video"}},
+ sender: {track: video},
+ direction: "sendrecv",
+ mid: null,
+ currentDirection: null,
+ stopped: false
+ }
+ ]);
+
+ const offer = await pc.createOffer();
+ assert_true(offer.sdp.includes("a=msid:" + audioStream.id),
+ "offer contains the expected audio msid");
+ assert_true(offer.sdp.includes("a=msid:" + videoStream.id),
+ "offer contains the expected video msid");
+ };
+
+ const checkAddTransceiverWithOfferToReceive = async (t, kinds) => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const propsToSet = kinds.map(kind => {
+ if (kind == "audio") {
+ return "offerToReceiveAudio";
+ } else if (kind == "video") {
+ return "offerToReceiveVideo";
+ }
+ });
+
+ const options = {};
+
+ for (const prop of propsToSet) {
+ options[prop] = true;
+ }
+
+ let offer = await pc.createOffer(options);
+
+ const expected = [];
+
+ if (options.offerToReceiveAudio) {
+ expected.push(
+ {
+ receiver: {track: {kind: "audio"}},
+ sender: {track: null},
+ direction: "recvonly",
+ mid: null,
+ currentDirection: null,
+ stopped: false
+ });
+ }
+
+ if (options.offerToReceiveVideo) {
+ expected.push(
+ {
+ receiver: {track: {kind: "video"}},
+ sender: {track: null},
+ direction: "recvonly",
+ mid: null,
+ currentDirection: null,
+ stopped: false
+ });
+ }
+
+ hasProps(pc.getTransceivers(), expected);
+
+ // Test offerToReceive: false
+ for (const prop of propsToSet) {
+ options[prop] = false;
+ }
+
+ // Check that sendrecv goes to sendonly
+ for (const transceiver of pc.getTransceivers()) {
+ transceiver.direction = "sendrecv";
+ }
+
+ for (const transceiverCheck of expected) {
+ transceiverCheck.direction = "sendonly";
+ }
+
+ offer = await pc.createOffer(options);
+ hasProps(pc.getTransceivers(), expected);
+
+ // Check that recvonly goes to inactive
+ for (const transceiver of pc.getTransceivers()) {
+ transceiver.direction = "recvonly";
+ }
+
+ for (const transceiverCheck of expected) {
+ transceiverCheck.direction = "inactive";
+ }
+
+ offer = await pc.createOffer(options);
+ hasProps(pc.getTransceivers(), expected);
+ };
+
+const tests = [
+ checkAddTransceiverWithStream,
+ function checkAddTransceiverWithOfferToReceiveAudio(t) {
+ return checkAddTransceiverWithOfferToReceive(t, ["audio"]);
+ },
+ function checkAddTransceiverWithOfferToReceiveVideo(t) {
+ return checkAddTransceiverWithOfferToReceive(t, ["video"]);
+ },
+ function checkAddTransceiverWithOfferToReceiveBoth(t) {
+ return checkAddTransceiverWithOfferToReceive(t, ["audio", "video"]);
+ }
+].forEach(test => promise_test(test, test.name));
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/legacy/onaddstream.https.html b/testing/web-platform/tests/webrtc/legacy/onaddstream.https.html
new file mode 100644
index 0000000000..b5e8a402b8
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/legacy/onaddstream.https.html
@@ -0,0 +1,157 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>onaddstream tests</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src=/resources/testdriver.js></script>
+<script src=/resources/testdriver-vendor.js></script>
+<script src='/mediacapture-streams/permission-helper.js'></script>
+<script>
+ 'use strict';
+
+ const stopTracks = (...streams) => {
+ streams.forEach(stream => stream.getTracks().forEach(track => track.stop()));
+ };
+
+ const collectEvents = (target, name, check) => {
+ const events = [];
+ const handler = e => {
+ check(e);
+ events.push(e);
+ };
+
+ target.addEventListener(name, handler);
+
+ const finishCollecting = () => {
+ target.removeEventListener(name, handler);
+ return events;
+ };
+
+ return {finish: finishCollecting};
+ };
+
+ const collectAddTrackEvents = stream => {
+ const checkEvent = e => {
+ assert_true(e.track instanceof MediaStreamTrack, "Track is set on event");
+ assert_true(stream.getTracks().includes(e.track),
+ "track in addtrack event is in the stream");
+ };
+ return collectEvents(stream, "addtrack", checkEvent);
+ };
+
+ const collectRemoveTrackEvents = stream => {
+ const checkEvent = e => {
+ assert_true(e.track instanceof MediaStreamTrack, "Track is set on event");
+ assert_true(!stream.getTracks().includes(e.track),
+ "track in removetrack event is not in the stream");
+ };
+ return collectEvents(stream, "removetrack", checkEvent);
+ };
+
+ const collectTrackEvents = pc => {
+ const checkEvent = e => {
+ assert_true(e.track instanceof MediaStreamTrack, "Track is set on event");
+ assert_true(e.receiver instanceof RTCRtpReceiver, "Receiver is set on event");
+ assert_true(e.transceiver instanceof RTCRtpTransceiver, "Transceiver is set on event");
+ assert_true(Array.isArray(e.streams), "Streams is set on event");
+ e.streams.forEach(stream => {
+ assert_true(stream.getTracks().includes(e.track),
+ "Each stream in event contains the track");
+ });
+ assert_equals(e.receiver, e.transceiver.receiver,
+ "Receiver belongs to transceiver");
+ assert_equals(e.track, e.receiver.track,
+ "Track belongs to receiver");
+ };
+
+ return collectEvents(pc, "track", checkEvent);
+ };
+
+ // comparable() - produces copy of object that is JSON comparable.
+ // o = original object (required)
+ // t = template of what to examine. Useful if o is non-enumerable (optional)
+
+ const comparable = (o, t = o) => {
+ if (typeof o != 'object' || !o) {
+ return o;
+ }
+ if (Array.isArray(t) && Array.isArray(o)) {
+ return o.map((n, i) => comparable(n, t[i]));
+ }
+ return Object.keys(t).sort()
+ .reduce((r, key) => (r[key] = comparable(o[key], t[key]), r), {});
+ };
+
+ const stripKeyQuotes = s => s.replace(/"(\w+)":/g, "$1:");
+
+ const hasProps = (observed, expected) => {
+ const observable = comparable(observed, expected);
+ assert_equals(stripKeyQuotes(JSON.stringify(observable)),
+ stripKeyQuotes(JSON.stringify(comparable(expected))));
+ };
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ await setMediaPermission();
+ const stream1 = await navigator.mediaDevices.getUserMedia({audio: true, video: true});
+ t.add_cleanup(() => stopTracks(stream1));
+ const audio1 = stream1.getAudioTracks()[0];
+ pc1.addTrack(audio1, stream1);
+ const video1 = stream1.getVideoTracks()[0];
+ pc1.addTrack(video1, stream1);
+
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+ const stream2 = await navigator.mediaDevices.getUserMedia({audio: true, video: true});
+ t.add_cleanup(() => stopTracks(stream2));
+ const audio2 = stream2.getAudioTracks()[0];
+ pc2.addTrack(audio2, stream2);
+ const video2 = stream2.getVideoTracks()[0];
+ pc2.addTrack(video2, stream2);
+
+ const offer = await pc1.createOffer();
+
+ let trackEventCollector = collectTrackEvents(pc2);
+ let addstreamEventCollector = collectEvents(pc2, "addstream", e => {
+ hasProps(e, {stream: {id: stream1.id}});
+ assert_equals(e.stream.getAudioTracks().length, 1, "One audio track");
+ assert_equals(e.stream.getVideoTracks().length, 1, "One video track");
+ });
+
+ await pc2.setRemoteDescription(offer);
+
+ let addstreamEvents = addstreamEventCollector.finish();
+ assert_equals(addstreamEvents.length, 1, "Should have 1 addstream event");
+
+ let trackEvents = trackEventCollector.finish();
+
+ hasProps(trackEvents,
+ [
+ {streams: [addstreamEvents[0].stream]},
+ {streams: [addstreamEvents[0].stream]}
+ ]);
+
+ await pc1.setLocalDescription(offer);
+ const answer = await pc2.createAnswer();
+
+ trackEventCollector = collectTrackEvents(pc1);
+ addstreamEventCollector = collectEvents(pc1, "addstream", e => {
+ hasProps(e, {stream: {id: stream2.id}});
+ assert_equals(e.stream.getAudioTracks().length, 1, "One audio track");
+ assert_equals(e.stream.getVideoTracks().length, 1, "One video track");
+ });
+
+ await pc1.setRemoteDescription(answer);
+ addstreamEvents = addstreamEventCollector.finish();
+ assert_equals(addstreamEvents.length, 1, "Should have 1 addstream event");
+
+ trackEvents = trackEventCollector.finish();
+
+ hasProps(trackEvents,
+ [
+ {streams: [addstreamEvents[0].stream]},
+ {streams: [addstreamEvents[0].stream]}
+ ]);
+ },"Check onaddstream");
+</script>
diff --git a/testing/web-platform/tests/webrtc/no-media-call.html b/testing/web-platform/tests/webrtc/no-media-call.html
new file mode 100644
index 0000000000..dba0b1d2df
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/no-media-call.html
@@ -0,0 +1,100 @@
+<!doctype html>
+
+<html>
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+ <title>RTCPeerConnection No-Media Connection Test</title>
+</head>
+<body>
+ <div id="log"></div>
+ <h2>iceConnectionState info</h2>
+ <div id="stateinfo">
+ </div>
+
+ <!-- These files are in place when executing on W3C. -->
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="RTCPeerConnection-helper.js"></script>
+ <script type="text/javascript">
+ let gFirstConnection = null;
+ let gSecondConnection = null;
+
+ function onIceCandidate(otherConnction, event, reject) {
+ try {
+ otherConnction.addIceCandidate(event.candidate);
+ } catch(e) {
+ reject(e);
+ }
+ };
+
+ function onIceConnectionStateChange(done, failed, event) {
+ try {
+ assert_equals(event.type, 'iceconnectionstatechange');
+ assert_not_equals(gFirstConnection.iceConnectionState, "failed",
+ "iceConnectionState of first connection");
+ assert_not_equals(gSecondConnection.iceConnectionState, "failed",
+ "iceConnectionState of second connection");
+ const stateinfo = document.getElementById('stateinfo');
+ stateinfo.innerHTML = 'First: ' + gFirstConnection.iceConnectionState
+ + '<br>Second: ' + gSecondConnection.iceConnectionState;
+ // Note: All these combinations are legal states indicating that the
+ // call has connected. All browsers should end up in completed/completed,
+ // but as of this moment, we've chosen to terminate the test early.
+ // TODO: Revise test to ensure completed/completed is reached.
+ const allowedStates = [ 'connected', 'completed'];
+ if (allowedStates.includes(gFirstConnection.iceConnectionState) &&
+ allowedStates.includes(gSecondConnection.iceConnectionState)) {
+ done();
+ }
+ } catch(e) {
+ failed(e);
+ }
+ };
+
+ // This function starts the test.
+ promise_test((test) => {
+ return new Promise(async (resolve, reject) => {
+ gFirstConnection = new RTCPeerConnection(null);
+ test.add_cleanup(() => gFirstConnection.close());
+ gFirstConnection.onicecandidate =
+ (event) => onIceCandidate(gSecondConnection, event, reject);
+ gFirstConnection.oniceconnectionstatechange =
+ (event) => onIceConnectionStateChange(resolve, reject, event);
+
+ gSecondConnection = new RTCPeerConnection(null);
+ test.add_cleanup(() => gSecondConnection.close());
+ gSecondConnection.onicecandidate =
+ (event) => onIceCandidate(gFirstConnection, event, reject);
+ gSecondConnection.oniceconnectionstatechange =
+ (event) => onIceConnectionStateChange(resolve, reject, event);
+
+ const offer = await generateVideoReceiveOnlyOffer(gFirstConnection);
+
+ await gFirstConnection.setLocalDescription(offer);
+
+ // This would normally go across the application's signaling solution.
+ // In our case, the "signaling" is to call this function.
+
+ await gSecondConnection.setRemoteDescription({ type: 'offer',
+ sdp: offer.sdp });
+
+ const answer = await gSecondConnection.createAnswer();
+
+ await gSecondConnection.setLocalDescription(answer);
+
+ assert_equals(gSecondConnection.getSenders().length, 1);
+ assert_not_equals(gSecondConnection.getSenders()[0], null);
+ assert_not_equals(gSecondConnection.getSenders()[0].transport, null);
+
+ // Similarly, this would go over the application's signaling solution.
+ await gFirstConnection.setRemoteDescription({ type: 'answer',
+ sdp: answer.sdp });
+
+ // The test is terminated by onIceConnectionStateChange() calling resolve
+ // once both connections are connected.
+ })
+ });
+</script>
+
+</body>
+</html>
diff --git a/testing/web-platform/tests/webrtc/promises-call.html b/testing/web-platform/tests/webrtc/promises-call.html
new file mode 100644
index 0000000000..ee64b463ee
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/promises-call.html
@@ -0,0 +1,113 @@
+<!doctype html>
+<!--
+This test uses data only, and thus does not require fake media devices.
+-->
+
+<html>
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+ <title>RTCPeerConnection Data-Only Connection Test with Promises</title>
+</head>
+<body>
+ <div id="log"></div>
+ <h2>iceConnectionState info</h2>
+ <div id="stateinfo">
+ </div>
+
+ <!-- These files are in place when executing on W3C. -->
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script type="text/javascript">
+ var test = async_test('Can set up a basic WebRTC call with only data using promises.');
+
+ var gFirstConnection = null;
+ var gSecondConnection = null;
+
+ var onIceCandidateToFirst = test.step_func(function(event) {
+ gSecondConnection.addIceCandidate(event.candidate);
+ });
+
+ var onIceCandidateToSecond = test.step_func(function(event) {
+ gFirstConnection.addIceCandidate(event.candidate);
+ });
+
+ var onIceConnectionStateChange = test.step_func(function(event) {
+ assert_equals(event.type, 'iceconnectionstatechange');
+ var stateinfo = document.getElementById('stateinfo');
+ stateinfo.innerHTML = 'First: ' + gFirstConnection.iceConnectionState
+ + '<br>Second: ' + gSecondConnection.iceConnectionState;
+ // Note: All these combinations are legal states indicating that the
+ // call has connected. All browsers should end up in completed/completed,
+ // but as of this moment, we've chosen to terminate the test early.
+ // TODO: Revise test to ensure completed/completed is reached.
+ if (gFirstConnection.iceConnectionState == 'connected' &&
+ gSecondConnection.iceConnectionState == 'connected') {
+ test.done()
+ }
+ if (gFirstConnection.iceConnectionState == 'connected' &&
+ gSecondConnection.iceConnectionState == 'completed') {
+ test.done()
+ }
+ if (gFirstConnection.iceConnectionState == 'completed' &&
+ gSecondConnection.iceConnectionState == 'connected') {
+ test.done()
+ }
+ if (gFirstConnection.iceConnectionState == 'completed' &&
+ gSecondConnection.iceConnectionState == 'completed') {
+ test.done()
+ }
+ });
+
+ // This function starts the test.
+ test.step(function() {
+ gFirstConnection = new RTCPeerConnection(null);
+ test.add_cleanup(() => gFirstConnection.close());
+ gFirstConnection.onicecandidate = onIceCandidateToFirst;
+ gFirstConnection.oniceconnectionstatechange = onIceConnectionStateChange;
+
+ gSecondConnection = new RTCPeerConnection(null);
+ test.add_cleanup(() => gSecondConnection.close());
+ gSecondConnection.onicecandidate = onIceCandidateToSecond;
+ gSecondConnection.oniceconnectionstatechange = onIceConnectionStateChange;
+
+ // The createDataChannel is necessary and sufficient to make
+ // sure the ICE connection be attempted.
+ gFirstConnection.createDataChannel('channel');
+
+ var atStep = 'Create offer';
+
+ gFirstConnection.createOffer()
+ .then(function(offer) {
+ atStep = 'Set local description at first';
+ return gFirstConnection.setLocalDescription(offer);
+ })
+ .then(function() {
+ atStep = 'Set remote description at second';
+ return gSecondConnection.setRemoteDescription(
+ gFirstConnection.localDescription);
+ })
+ .then(function() {
+ atStep = 'Create answer';
+ return gSecondConnection.createAnswer();
+ })
+ .then(function(answer) {
+ atStep = 'Set local description at second';
+ return gSecondConnection.setLocalDescription(answer);
+ })
+ .then(function() {
+ atStep = 'Set remote description at first';
+ return gFirstConnection.setRemoteDescription(
+ gSecondConnection.localDescription);
+ })
+ .then(function() {
+ atStep = 'Negotiation completed';
+ })
+ .catch(test.step_func(function(e) {
+ assert_unreached('Error ' + e.name + ': ' + e.message +
+ ' happened at step ' + atStep);
+ }));
+ });
+</script>
+
+</body>
+</html>
diff --git a/testing/web-platform/tests/webrtc/protocol/README.txt b/testing/web-platform/tests/webrtc/protocol/README.txt
new file mode 100644
index 0000000000..5e17fbf9c3
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/protocol/README.txt
@@ -0,0 +1,22 @@
+This directory contains files that test for behavior relevant to webrtc,
+but which is specified in protocol specifications from the IETF, not in
+API recommendations from the W3C.
+
+The main specifications are given in the following RFCs:
+
+- RFC 7742, "WebRTC Video Processing and Codec Requirements"
+- RFC 7874, "WebRTC Audio Codec and Processing Requirements"
+- RFC 8825 (draft-ietf-rtcweb-overview)
+- RFC 8826 (draft-ietf-rtcweb-security)
+- RFC 8827 (draft-ietf-rtcweb-security-arch)
+- RFC 8828 (draft-ietf-rtcweb-ip-handling)
+- RFC 8829 (draft-ietf-rtcweb-jsep)
+- RFC 8831 (draft-ietf-rtcweb-data-channel)
+- RFC 8832 (draft-ietf-rtcweb-data-protocol)
+- RFC 8834 (draft-ietf-rtcweb-rtp-usage)
+- RFC 8835 (draft-ietf-rtcweb-transports)
+- RFC 8851 (draft-ietf-mmusic-rid)
+- RFC 8853 (draft-ietf-mmusic-sdp-simulcast)
+- RFC 8854 (draft-ietf-rtcweb-fec)
+
+This list is incomplete.
diff --git a/testing/web-platform/tests/webrtc/protocol/RTCPeerConnection-payloadTypes.html b/testing/web-platform/tests/webrtc/protocol/RTCPeerConnection-payloadTypes.html
new file mode 100644
index 0000000000..066fc2e085
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/protocol/RTCPeerConnection-payloadTypes.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>RTCPeerConnection RTP payload types</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+<script>
+
+// Test that when creating an offer we do not run out of valid payload types.
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+
+ pc1.addTransceiver('audio', { direction: 'recvonly' });
+ pc1.addTransceiver('video', { direction: 'recvonly' });
+ const offer = await pc1.createOffer();
+
+ // Extract all payload types from the m= lines.
+ const payloadTypes = offer.sdp.split('\n')
+ .map(line => line.trim())
+ .filter(line => line.startsWith('m='))
+ .map(line => line.split(' ').slice(3).join(' '))
+ .join(' ')
+ .split(' ')
+ .map(payloadType => parseInt(payloadType, 10));
+
+ // The list of allowed payload types is taken from here
+ // https://www.iana.org/assignments/rtp-parameters/rtp-parameters.xhtml#rtp-parameters-1.
+ const forbiddenPayloadTypes = payloadTypes
+ .filter(payloadType => {
+ if (payloadType >= 96 && payloadType <= 127) {
+ return false;
+ }
+ if (payloadType >= 72 && payloadType < 96) {
+ return true;
+ }
+ if (payloadType >= 35 && payloadType < 72) {
+ return false;
+ }
+ // TODO: Check against static payload type list.
+ return false;
+ });
+ assert_equals(forbiddenPayloadTypes.length, 0)
+}, 'createOffer with the maximum set of codecs does not generate invalid payload types');
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/webrtc/protocol/bundle.https.html b/testing/web-platform/tests/webrtc/protocol/bundle.https.html
new file mode 100644
index 0000000000..3d2b835baf
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/protocol/bundle.https.html
@@ -0,0 +1,150 @@
+<!doctype html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<title>RTCPeerConnection BUNDLE</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../RTCPeerConnection-helper.js"></script>
+<script>
+'use strict';
+
+promise_test(async t => {
+ const caller = new RTCPeerConnection();
+ t.add_cleanup(() => caller.close());
+ const callee = new RTCPeerConnection();
+ t.add_cleanup(() => callee.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));
+
+
+ exchangeIceCandidates(caller, callee);
+ const offer = await caller.createOffer();
+ // remove the a=group:BUNDLE from the SDP when signaling.
+ const sdp = offer.sdp.replace(/a=group:BUNDLE (.*)\r\n/, '');
+ const ontrack = new Promise(r => callee.ontrack = r);
+
+ await callee.setRemoteDescription({type: 'offer', sdp});
+ await caller.setLocalDescription(offer);
+
+ const answer = await callee.createAnswer();
+ await caller.setRemoteDescription(answer);
+ await callee.setLocalDescription(answer);
+
+ const {streams: [recvStream]} = await ontrack;
+ assert_equals(recvStream.getTracks().length, 2, "Tracks should be added to the stream before sRD resolves.");
+ const v = document.createElement('video');
+ v.autoplay = true;
+ v.srcObject = recvStream;
+ v.id = recvStream.id;
+ await new Promise(r => v.onloadedmetadata = r);
+
+ const senders = caller.getSenders();
+ const dtlsTransports = senders.map(s => s.transport);
+ assert_equals(dtlsTransports.length, 2);
+ assert_not_equals(dtlsTransports[0], dtlsTransports[1]);
+
+ const iceTransports = dtlsTransports.map(t => t.iceTransport);
+ assert_equals(iceTransports.length, 2);
+ assert_not_equals(iceTransports[0], iceTransports[1]);
+}, 'not negotiating BUNDLE creates two separate ice and dtls transports');
+
+promise_test(async t => {
+ const caller = new RTCPeerConnection();
+ t.add_cleanup(() => caller.close());
+ const callee = new RTCPeerConnection();
+ t.add_cleanup(() => callee.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));
+
+ exchangeIceCandidates(caller, callee);
+ const offer = await caller.createOffer();
+ const ontrack = new Promise(r => callee.ontrack = r);
+ await callee.setRemoteDescription(offer);
+ await caller.setLocalDescription(offer);
+ const secondTransport = caller.getSenders()[1].transport; // Save a reference to this transport.
+
+ const answer = await callee.createAnswer();
+ await caller.setRemoteDescription(answer);
+ await callee.setLocalDescription(answer);
+
+ const {streams: [recvStream]} = await ontrack;
+ assert_equals(recvStream.getTracks().length, 2, "Tracks should be added to the stream before sRD resolves.");
+ const v = document.createElement('video');
+ v.autoplay = true;
+ v.srcObject = recvStream;
+ v.id = recvStream.id;
+ await new Promise(r => v.onloadedmetadata = r);
+
+ const senders = caller.getSenders();
+ const dtlsTransports = senders.map(s => s.transport);
+ assert_equals(dtlsTransports.length, 2);
+ assert_equals(dtlsTransports[0], dtlsTransports[1]);
+ assert_not_equals(dtlsTransports[1], secondTransport);
+ assert_equals(secondTransport.state, 'closed');
+}, 'bundles on the first transport and closes the second');
+
+promise_test(async t => {
+ const sdp = `v=0
+o=- 0 3 IN IP4 127.0.0.1
+s=-
+t=0 0
+a=fingerprint:sha-256 A7:24:72:CA:6E:02:55:39:BA:66:DF:6E:CC:4C:D8:B0:1A:BF:1A:56:65:7D:F4:03:AD:7E:77:43:2A:29:EC:93
+a=ice-ufrag:ETEn
+a=ice-pwd:OtSK0WpNtpUjkY4+86js7Z/l
+m=audio 9 UDP/TLS/RTP/SAVPF 111
+c=IN IP4 0.0.0.0
+a=rtcp-mux
+a=sendonly
+a=mid:audio
+a=rtpmap:111 opus/48000/2
+a=setup:actpass
+m=video 9 UDP/TLS/RTP/SAVPF 100
+c=IN IP4 0.0.0.0
+a=rtcp-mux
+a=sendonly
+a=mid:video
+a=rtpmap:100 VP8/90000
+a=fmtp:100 max-fr=30;max-fs=3600
+a=setup:actpass
+`;
+ const pc = new RTCPeerConnection({ bundlePolicy: 'max-bundle' });
+ t.add_cleanup(() => pc.close());
+ await pc.setRemoteDescription({ type: 'offer', sdp });
+ await pc.setLocalDescription();
+ const transceivers = pc.getTransceivers();
+ assert_equals(transceivers.length, 2);
+ assert_false(transceivers[0].stopped);
+ assert_true(transceivers[1].stopped);
+}, 'max-bundle with an offer without bundle only negotiates the first m-line');
+
+promise_test(async t => {
+ const sdp = `v=0
+o=- 0 3 IN IP4 127.0.0.1
+s=-
+t=0 0
+a=group:BUNDLE audio video
+m=audio 9 UDP/TLS/RTP/SAVPF 111
+c=IN IP4 0.0.0.0
+a=fingerprint:sha-256 A7:24:72:CA:6E:02:55:39:BA:66:DF:6E:CC:4C:D8:B0:1A:BF:1A:56:65:7D:F4:03:AD:7E:77:43:2A:29:EC:93
+a=ice-ufrag:ETEn
+a=ice-pwd:OtSK0WpNtpUjkY4+86js7Z/l
+a=rtcp-mux
+a=sendonly
+a=mid:audio
+a=rtpmap:111 opus/48000/2
+a=setup:actpass
+m=video 9 UDP/TLS/RTP/SAVPF 100
+c=IN IP4 0.0.0.0
+a=bundle-only
+a=sendonly
+a=mid:video
+a=rtpmap:100 VP8/90000
+a=fmtp:100 max-fr=30;max-fs=3600
+`;
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ await pc.setRemoteDescription({ type: 'offer', sdp });
+}, 'sRD(offer) works with no transport attributes in a bundle-only m-section');
+</script>
diff --git a/testing/web-platform/tests/webrtc/protocol/candidate-exchange.https.html b/testing/web-platform/tests/webrtc/protocol/candidate-exchange.https.html
new file mode 100644
index 0000000000..c54f26e6d8
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/protocol/candidate-exchange.https.html
@@ -0,0 +1,218 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Candidate exchange</title>
+<meta name=timeout content=long>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../RTCPeerConnection-helper.js"></script>
+</head>
+<body>
+<script>
+
+class StateLogger {
+ constructor(source, eventname, field) {
+ source.addEventListener(eventname, event => {
+ this.events.push(source[field]);
+ });
+ this.events = [source[field]];
+ }
+}
+
+class IceStateLogger extends StateLogger {
+ constructor(source) {
+ super(source, 'iceconnectionstatechange', 'iceConnectionState');
+ }
+}
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+ pc1.createDataChannel('datachannel');
+ pc1IceStates = new IceStateLogger(pc1);
+ pc2IceStates = new IceStateLogger(pc1);
+ exchangeIceCandidates(pc1, pc2);
+ await exchangeOfferAnswer(pc1, pc2);
+ // Note - it's been claimed that this state sometimes jumps straight
+ // to "completed". If so, this test should be flaky.
+ await waitForIceStateChange(pc1, ['connected']);
+ assert_array_equals(pc1IceStates.events, ['new', 'checking', 'connected']);
+ assert_array_equals(pc2IceStates.events, ['new', 'checking', 'connected']);
+}, 'Two way ICE exchange works');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+ pc1IceStates = new IceStateLogger(pc1);
+ pc2IceStates = new IceStateLogger(pc1);
+ let candidates = [];
+ pc1.createDataChannel('datachannel');
+ pc1.onicecandidate = e => {
+ candidates.push(e.candidate);
+ }
+ // Candidates from PC2 are not delivered to pc1, so pc1 will use
+ // peer-reflexive candidates.
+ await exchangeOfferAnswer(pc1, pc2);
+ const waiter = waitForIceGatheringState(pc1, ['complete']);
+ await waiter;
+ for (const candidate of candidates) {
+ if (candidate) {
+ pc2.addIceCandidate(candidate);
+ }
+ }
+ await Promise.all([waitForIceStateChange(pc1, ['connected', 'completed']),
+ waitForIceStateChange(pc2, ['connected', 'completed'])]);
+ const candidate_pair = pc1.sctp.transport.iceTransport.getSelectedCandidatePair();
+ assert_equals(candidate_pair.local.type, 'host');
+ assert_equals(candidate_pair.remote.type, 'prflx');
+ assert_array_equals(pc1IceStates.events, ['new', 'checking', 'connected']);
+ assert_array_equals(pc2IceStates.events, ['new', 'checking', 'connected']);
+}, 'Adding only caller -> callee candidates gives a connection');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+ pc1IceStates = new IceStateLogger(pc1);
+ pc2IceStates = new IceStateLogger(pc1);
+ let candidates = [];
+ pc1.createDataChannel('datachannel');
+ pc2.onicecandidate = e => {
+ candidates.push(e.candidate);
+ }
+ // Candidates from pc1 are not delivered to pc2. so pc2 will use
+ // peer-reflexive candidates.
+ await exchangeOfferAnswer(pc1, pc2);
+ const waiter = waitForIceGatheringState(pc2, ['complete']);
+ await waiter;
+ for (const candidate of candidates) {
+ if (candidate) {
+ pc1.addIceCandidate(candidate);
+ }
+ }
+ await Promise.all([waitForIceStateChange(pc1, ['connected', 'completed']),
+ waitForIceStateChange(pc2, ['connected', 'completed'])]);
+ const candidate_pair = pc2.sctp.transport.iceTransport.getSelectedCandidatePair();
+ assert_equals(candidate_pair.local.type, 'host');
+ assert_equals(candidate_pair.remote.type, 'prflx');
+ assert_array_equals(pc1IceStates.events, ['new', 'checking', 'connected']);
+ assert_array_equals(pc2IceStates.events, ['new', 'checking', 'connected']);
+}, 'Adding only callee -> caller candidates gives a connection');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+ pc1IceStates = new IceStateLogger(pc1);
+ pc2IceStates = new IceStateLogger(pc1);
+ let pc2ToPc1Candidates = [];
+ pc1.createDataChannel('datachannel');
+ pc2.onicecandidate = e => {
+ pc2ToPc1Candidates.push(e.candidate);
+ // This particular test verifies that candidates work
+ // properly if added from the pc2 onicecandidate event.
+ if (!e.candidate) {
+ for (const candidate of pc2ToPc1Candidates) {
+ if (candidate) {
+ pc1.addIceCandidate(candidate);
+ }
+ }
+ }
+ }
+ // Candidates from |pc1| are not delivered to |pc2|. |pc2| will use
+ // peer-reflexive candidates.
+ await exchangeOfferAnswer(pc1, pc2);
+ await Promise.all([waitForIceStateChange(pc1, ['connected', 'completed']),
+ waitForIceStateChange(pc2, ['connected', 'completed'])]);
+ const candidate_pair = pc2.sctp.transport.iceTransport.getSelectedCandidatePair();
+ assert_equals(candidate_pair.local.type, 'host');
+ assert_equals(candidate_pair.remote.type, 'prflx');
+ assert_array_equals(pc1IceStates.events, ['new', 'checking', 'connected']);
+ assert_array_equals(pc2IceStates.events, ['new', 'checking', 'connected']);
+}, 'Adding callee -> caller candidates from end-of-candidates gives a connection');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+ pc1IceStates = new IceStateLogger(pc1);
+ pc2IceStates = new IceStateLogger(pc1);
+ let pc1ToPc2Candidates = [];
+ let pc2ToPc1Candidates = [];
+ pc1.createDataChannel('datachannel');
+ pc1.onicecandidate = e => {
+ pc1ToPc2Candidates.push(e.candidate);
+ }
+ pc2.onicecandidate = e => {
+ pc2ToPc1Candidates.push(e.candidate);
+ }
+ const offer = await pc1.createOffer();
+ await Promise.all([pc1.setLocalDescription(offer),
+ pc2.setRemoteDescription(offer)]);
+ const answer = await pc2.createAnswer();
+ await waitForIceGatheringState(pc1, ['complete']);
+ await pc2.setLocalDescription(answer).then(() => {
+ for (const candidate of pc1ToPc2Candidates) {
+ if (candidate) {
+ pc2.addIceCandidate(candidate);
+ }
+ }
+ });
+ await waitForIceGatheringState(pc2, ['complete']);
+ pc1.setRemoteDescription(answer).then(async () => {
+ for (const candidate of pc2ToPc1Candidates) {
+ if (candidate) {
+ await pc1.addIceCandidate(candidate);
+ }
+ }
+ });
+ await Promise.all([waitForIceStateChange(pc1, ['connected', 'completed']),
+ waitForIceStateChange(pc2, ['connected', 'completed'])]);
+ const candidate_pair =
+ pc1.sctp.transport.iceTransport.getSelectedCandidatePair();
+ assert_equals(candidate_pair.local.type, 'host');
+ // When we supply remote candidates, we expect a jump to the 'host' candidate,
+ // but it might also remain as 'prflx'.
+ assert_true(candidate_pair.remote.type == 'host' ||
+ candidate_pair.remote.type == 'prflx');
+ assert_array_equals(pc1IceStates.events, ['new', 'checking', 'connected']);
+ assert_array_equals(pc2IceStates.events, ['new', 'checking', 'connected']);
+}, 'Explicit offer/answer exchange gives a connection');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ pc1.createDataChannel('datachannel');
+ pc1.onicecandidate = assert_unreached;
+ const offer = await pc1.createOffer();
+ await pc1.setLocalDescription(offer);
+ await new Promise(resolve => {
+ pc1.onicecandidate = resolve;
+ });
+}, 'Candidates always arrive after setLocalDescription(offer) resolves');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+ pc1.createDataChannel('datachannel');
+ pc2.onicecandidate = assert_unreached;
+ const offer = await pc1.createOffer();
+ await pc2.setRemoteDescription(offer);
+ await pc2.setLocalDescription(await pc2.createAnswer());
+ await new Promise(resolve => {
+ pc2.onicecandidate = resolve;
+ });
+}, 'Candidates always arrive after setLocalDescription(answer) resolves');
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/webrtc/protocol/crypto-suite.https.html b/testing/web-platform/tests/webrtc/protocol/crypto-suite.https.html
new file mode 100644
index 0000000000..f13f221b88
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/protocol/crypto-suite.https.html
@@ -0,0 +1,85 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerConnection.prototype.createOffer</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../RTCPeerConnection-helper.js"></script>
+<script src="../RTCStats-helper.js"></script>
+<script>
+'use strict';
+
+// draft-ietf-rtcweb-security-20 section 6.5
+//
+// All Implementations MUST support DTLS 1.2 with the
+// TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 cipher suite and the P-256
+// curve [FIPS186].
+// ....... The DTLS-SRTP protection profile
+// SRTP_AES128_CM_HMAC_SHA1_80 MUST be supported for SRTP.
+// Implementations MUST favor cipher suites which support (Perfect
+// Forward Secrecy) PFS over non-PFS cipher suites and SHOULD favor AEAD
+// over non-AEAD cipher suites.
+
+const acceptableTlsVersions = new Set([
+ 'FEFD', // DTLS 1.2 - RFC 6437 section 4.1
+ '0304', // TLS 1.3 - RFC 8446 section 5.1
+]);
+
+const acceptableDtlsCiphersuites = new Set([
+ 'TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256',
+]);
+
+const acceptableSrtpCiphersuites = new Set([
+ 'SRTP_AES128_CM_HMAC_SHA1_80',
+ 'AES_CM_128_HMAC_SHA1_80',
+]);
+
+const acceptableTlsGroups = new Set([
+ 'P-256',
+]);
+
+const acceptableValues = {
+ 'tlsVersion': acceptableTlsVersions,
+ 'dtlsCipher': acceptableDtlsCiphersuites,
+ 'srtpCipher': acceptableSrtpCiphersuites,
+ 'tlsGroup': acceptableTlsGroups,
+};
+
+function verifyStat(name, transportStats) {
+ assert_not_equals(typeof transportStats, 'undefined');
+ assert_true(name in transportStats, 'Value present:');
+ assert_true(acceptableValues[name].has(transportStats[name]));
+}
+
+for (const name of Object.keys(acceptableValues)) {
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+ pc1.createDataChannel('foo');
+ exchangeIceCandidates(pc1, pc2);
+ await exchangeOfferAnswer(pc1, pc2);
+ await waitForState(pc1.sctp.transport, 'connected');
+ const statsReport = await pc1.getStats();
+ const transportStats = findStatsFromReport(statsReport,
+ stats => stats.type === 'transport')
+ verifyStat(name, transportStats);
+ }, name + ' is acceptable on data-only');
+
+ promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+ const transceiver = pc1.addTransceiver('video');
+
+ exchangeIceCandidates(pc1, pc2);
+ await exchangeOfferAnswer(pc1, pc2);
+ await waitForState(transceiver.sender.transport, 'connected');
+ const statsReport = await pc1.getStats();
+ const transportStats = findStatsFromReport(statsReport,
+ stats => stats.type === 'transport')
+ verifyStat(name, transportStats);
+ }, name + ' is acceptable on video-only');
+}
+</script>
diff --git a/testing/web-platform/tests/webrtc/protocol/dtls-certificates.html b/testing/web-platform/tests/webrtc/protocol/dtls-certificates.html
new file mode 100644
index 0000000000..bc4794cbc1
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/protocol/dtls-certificates.html
@@ -0,0 +1,42 @@
+<!doctype html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<title>RTCPeerConnection DTLS certifcate interop</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../RTCPeerConnection-helper.js"></script>
+<script>
+// https://w3c.github.io/webrtc-pc/#dom-rtcpeerconnection-generatecertificate
+const certificateParameters = {
+ ecdsa: {
+ name: 'ECDSA',
+ namedCurve: 'P-256',
+ },
+ rsa: {
+ name: 'RSASSA-PKCS1-v1_5',
+ modulusLength: 2048,
+ publicExponent: new Uint8Array([1, 0, 1]),
+ hash: 'SHA-256',
+ },
+};
+
+Object.keys(certificateParameters).forEach(async localType => {
+ Object.keys(certificateParameters).forEach(async remoteType => {
+ promise_test(async t => {
+ const localParameters = certificateParameters[localType];
+ const remoteParameters = certificateParameters[remoteType];
+ const firstCertificate = await RTCPeerConnection.generateCertificate(localParameters);
+ const secondCertificate = await RTCPeerConnection.generateCertificate(remoteParameters);
+ const pc1 = new RTCPeerConnection({certificates: [firstCertificate]});
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection({certificates: [secondCertificate]});
+ t.add_cleanup(() => pc2.close());
+ pc1.createDataChannel('test');
+ exchangeIceCandidates(pc1, pc2);
+ await exchangeOfferAnswer(pc1, pc2);
+ await waitForConnectionStateChange(pc1, ['connected']);
+ }, `RTCPeerConnection establishes using ${localType} and ${remoteType} certificates`);
+ });
+});
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/protocol/dtls-fingerprint-validation.html b/testing/web-platform/tests/webrtc/protocol/dtls-fingerprint-validation.html
new file mode 100644
index 0000000000..0ddc8488ae
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/protocol/dtls-fingerprint-validation.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>DTLS fingerprint validation</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../RTCPeerConnection-helper.js"></script>
+</head>
+<body>
+<script>
+
+// Tests that an invalid fingerprint leads to a connectionState 'failed'.
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+ pc1.createDataChannel('datachannel');
+ exchangeIceCandidates(pc1, pc2);
+ const offer = await pc1.createOffer();
+ await pc2.setRemoteDescription(offer);
+ await pc1.setLocalDescription(offer);
+ const answer = await pc2.createAnswer();
+ await pc1.setRemoteDescription(new RTCSessionDescription({
+ type: answer.type,
+ sdp: answer.sdp.replace(/a=fingerprint:sha-256 .*/g,
+ 'a=fingerprint:sha-256 00:00:00:00:00:00:00:00:00:00:00:00:00:' +
+ '00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00'),
+ }));
+ await pc2.setLocalDescription(answer);
+
+ await waitForConnectionStateChange(pc1, ['failed']);
+ await waitForConnectionStateChange(pc2, ['failed']);
+}, 'Connection fails if one side provides a wrong DTLS fingerprint');
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/webrtc/protocol/dtls-setup.https.html b/testing/web-platform/tests/webrtc/protocol/dtls-setup.https.html
new file mode 100644
index 0000000000..892e6db413
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/protocol/dtls-setup.https.html
@@ -0,0 +1,135 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerConnection a=setup SDP parameter test</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+'use strict';
+
+// Tests for correct behavior of DTLS a=setup parameter.
+
+// SDP copied from JSEP Example 7.1
+// It contains two media streams with different ufrags, and bundle
+// turned on.
+const kSdp = `v=0
+o=- 4962303333179871722 1 IN IP4 0.0.0.0
+s=-
+t=0 0
+a=ice-options:trickle
+a=group:BUNDLE a1 v1
+a=group:LS a1 v1
+m=audio 10100 UDP/TLS/RTP/SAVPF 96 0 8 97 98
+c=IN IP4 203.0.113.100
+a=mid:a1
+a=sendrecv
+a=rtpmap:96 opus/48000/2
+a=rtpmap:0 PCMU/8000
+a=rtpmap:8 PCMA/8000
+a=rtpmap:97 telephone-event/8000
+a=rtpmap:98 telephone-event/48000
+a=maxptime:120
+a=extmap:1 urn:ietf:params:rtp-hdrext:sdes:mid
+a=extmap:2 urn:ietf:params:rtp-hdrext:ssrc-audio-level
+a=msid:47017fee-b6c1-4162-929c-a25110252400 f83006c5-a0ff-4e0a-9ed9-d3e6747be7d9
+a=ice-ufrag:ETEn
+a=ice-pwd:OtSK0WpNtpUjkY4+86js7ZQl
+a=fingerprint:sha-256 19:E2:1C:3B:4B:9F:81:E6:B8:5C:F4:A5:A8:D8:73:04:BB:05:2F:70:9F:04:A9:0E:05:E9:26:33:E8:70:88:A2
+a=setup:actpass
+a=dtls-id:1
+a=rtcp:10101 IN IP4 203.0.113.100
+a=rtcp-mux
+a=rtcp-rsize
+m=video 10102 UDP/TLS/RTP/SAVPF 100 101
+c=IN IP4 203.0.113.100
+a=mid:v1
+a=sendrecv
+a=rtpmap:100 VP8/90000
+a=rtpmap:101 rtx/90000
+a=fmtp:101 apt=100
+a=extmap:1 urn:ietf:params:rtp-hdrext:sdes:mid
+a=rtcp-fb:100 ccm fir
+a=rtcp-fb:100 nack
+a=rtcp-fb:100 nack pli
+a=msid:47017fee-b6c1-4162-929c-a25110252400 f30bdb4a-5db8-49b5-bcdc-e0c9a23172e0
+a=ice-ufrag:BGKk
+a=ice-pwd:mqyWsAjvtKwTGnvhPztQ9mIf
+a=fingerprint:sha-256 19:E2:1C:3B:4B:9F:81:E6:B8:5C:F4:A5:A8:D8:73:04:BB:05:2F:70:9F:04:A9:0E:05:E9:26:33:E8:70:88:A2
+a=setup:actpass
+a=dtls-id:1
+a=rtcp:10103 IN IP4 203.0.113.100
+a=rtcp-mux
+a=rtcp-rsize
+`;
+
+for (let setup of ['actpass', 'active', 'passive']) {
+ promise_test(async t => {
+ const sdp = kSdp.replace(/a=setup:actpass/g,
+ 'a=setup:' + setup);
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ await pc1.setRemoteDescription({type: 'offer', sdp: sdp});
+ const answer = await pc1.createAnswer();
+ const resultingSetup = answer.sdp.match(/a=setup:\S+/);
+ if (setup === 'active') {
+ assert_equals(resultingSetup[0], 'a=setup:passive');
+ } else if (setup === 'passive') {
+ assert_equals(resultingSetup[0], 'a=setup:active');
+ } else if (setup === 'actpass') {
+ // For actpass, either active or passive are legal, although
+ // active is RECOMMENDED by RFC 5763 / 8842.
+ assert_in_array(resultingSetup[0], ['a=setup:active', 'a=setup:passive']);
+ }
+ await pc1.setLocalDescription(answer);
+ }, 'PC should accept initial offer with setup=' + setup);
+}
+
+for (let setup of ['actpass', 'active', 'passive']) {
+ const roleMap = {
+ actpass: 'client',
+ active: 'server',
+ passive: 'client',
+ };
+ promise_test(async t => {
+ const sdp = kSdp.replace(/a=setup:actpass/g,
+ 'a=setup:' + setup);
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ await pc1.setRemoteDescription({type: 'offer', sdp: sdp});
+ const answer = await pc1.createAnswer();
+ const resultingSetup = answer.sdp.match(/a=setup:\S+/);
+ if (setup === 'active') {
+ assert_equals(resultingSetup[0], 'a=setup:passive');
+ } else if (setup === 'passive') {
+ assert_equals(resultingSetup[0], 'a=setup:active');
+ } else if (setup === 'actpass') {
+ // For actpass, either active or passive are legal, although
+ // active is RECOMMENDED by RFC 5763 / 8842.
+ assert_in_array(resultingSetup[0], ['a=setup:active', 'a=setup:passive']);
+ }
+ await pc1.setLocalDescription(answer);
+ const stats = await pc1.getStats();
+ let transportStats;
+ stats.forEach(report => {
+ if (report.type === 'transport' && report.dtlsRole) {
+ transportStats = report;
+ }
+ });
+ assert_equals(transportStats.dtlsRole, roleMap[setup]);
+ }, 'PC with setup=' + setup + ' should have a dtlsRole of ' + roleMap[setup]);
+}
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ pc1.createDataChannel("wpt");
+ await pc1.setLocalDescription();
+ const stats = await pc1.getStats();
+ let transportStats;
+ stats.forEach(report => {
+ if (report.type === 'transport' && report.dtlsRole) {
+ transportStats = report;
+ }
+ });
+ assert_equals(transportStats.dtlsRole, 'unknown');
+}, 'dtlsRole is `unknown` before negotiation of the DTLS handshake');
+</script>
diff --git a/testing/web-platform/tests/webrtc/protocol/h264-profile-levels.https.html b/testing/web-platform/tests/webrtc/protocol/h264-profile-levels.https.html
new file mode 100644
index 0000000000..cb0b581c30
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/protocol/h264-profile-levels.https.html
@@ -0,0 +1,115 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerConnection H.264 profile levels</title>
+<meta name="timeout" content="long">
+<script src="../third_party/sdp/sdp.js"></script>
+<script src="simulcast.js"></script>
+<script src="../RTCPeerConnection-helper.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+
+function mungeLevel(sdp, level) {
+ level_hex = Math.round(level * 10).toString(16);
+ return {
+ type: sdp.type,
+ sdp: sdp.sdp.replace(/(profile-level-id=....)(..)/g,
+ "$1" + level_hex)
+ }
+}
+
+// Numbers taken from
+// https://en.wikipedia.org/wiki/Advanced_Video_Coding#Levels
+let levelTable = {
+ 1: {mbs: 1485, fs: 99},
+ 1.1: {mbs: 3000, fs: 396},
+ 1.2: {mbs: 6000, fs: 396},
+ 1.3: {mbs: 11880, fs: 396},
+ 2: {mbs: 11880, fs: 396},
+ 2.1: {mbs: 19800, fs: 792},
+ 2.2: {mbs: 20250, fs: 1620},
+ 3: {mbs: 40500, fs: 1620},
+ 3.1: {mbs: 108000, fs: 3600},
+ 3.2: {mbs: 216000, fs: 5120},
+ 4: {mbs: 245760, fs: 8192},
+ 4.1: {mbs: 245760, fs: 8192},
+ 4.2: {mbs: 522240, fs: 8704},
+ 5: {mbs: 589824, fs: 22800},
+ 5.1: {mbs: 983040, fs: 36864},
+ 5.2: {mbs: 2073600, fs: 36864},
+ 6: {mbs: 4177920, fs: 139264},
+ 6.1: {mbs: 8355840, fs: 139264},
+ 6.2: {mbs: 16711680, fs: 139264},
+};
+
+function sizeFitsLevel(width, height, fps, level) {
+ const frameSizeMacroblocks = width * height / 256;
+ const macroblocksPerSecond = frameSizeMacroblocks * fps;
+ assert_less_than_equal(frameSizeMacroblocks,
+ levelTable[level].fs, 'frame size');
+ assert_less_than_equal(macroblocksPerSecond,
+ levelTable[level].mbs, 'macroblocks/second');
+}
+
+// Constant for now, may be variable later.
+const framesPerSecond = 30;
+
+for (let level of Object.keys(levelTable)) {
+ promise_test(async t => {
+ assert_implements('getCapabilities' in RTCRtpSender, 'RTCRtpSender.getCapabilities not supported');
+ assert_implements(RTCRtpSender.getCapabilities('video').codecs.find(c => c.mimeType === 'video/H264'), 'H264 not supported');
+
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+ const v = document.createElement('video');
+
+ // Generate the largest video we can get from the attached device.
+ // This means platform inconsistency.
+ // The fake video in Chrome WPT tests is 3840x2160.
+ const stream = await navigator.mediaDevices.getUserMedia(
+ {video: {width: 12800, height: 7200, frameRate: framesPerSecond}});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const transceiver = pc1.addTransceiver(stream.getVideoTracks()[0], {
+ streams: [stream],
+ });
+ preferCodec(transceiver, 'video/H264');
+
+ exchangeIceCandidates(pc1, pc2);
+ const trackEvent = new Promise(r => pc2.ontrack = r);
+
+ const offer = await pc1.createOffer();
+ await pc1.setLocalDescription(offer),
+ await pc2.setRemoteDescription(offer);
+ const answer = await pc2.createAnswer();
+ await pc2.setLocalDescription(answer);
+ await pc1.setRemoteDescription(mungeLevel(answer, level));
+
+ v.srcObject = new MediaStream([(await trackEvent).track]);
+ let metadataLoaded = new Promise((resolve) => {
+ v.autoplay = true;
+ v.id = stream.id
+ v.addEventListener('loadedmetadata', () => {
+ resolve();
+ });
+ });
+ await metadataLoaded;
+ // Ensure that H.264 is in fact used.
+ const statsReport = await transceiver.sender.getStats();
+ for (const stats of statsReport.values()) {
+ if (stats.type === 'outbound-rtp') {
+ const activeCodec = stats.codecId;
+ const codecStats = statsReport.get(activeCodec);
+ assert_implements_optional(codecStats.mimeType ==='video/H264',
+ 'Level ' + level + ' H264 video is not supported');
+ }
+ }
+ // TODO(hta): This will not catch situations where the initial size is
+ // within the permitted bounds, but resolution or framerate changes to
+ // outside the permitted bounds after a while. Should be addressed.
+ sizeFitsLevel(v.videoWidth, v.videoHeight, framesPerSecond, level);
+ }, 'Level ' + level + ' H264 video is appropriately constrained');
+
+}
+</script>
diff --git a/testing/web-platform/tests/webrtc/protocol/handover-datachannel.html b/testing/web-platform/tests/webrtc/protocol/handover-datachannel.html
new file mode 100644
index 0000000000..8f224f822a
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/protocol/handover-datachannel.html
@@ -0,0 +1,61 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerConnection Handovers</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../RTCPeerConnection-helper.js"></script>
+<script>
+'use strict';
+
+promise_test(async t => {
+ const offerPc = new RTCPeerConnection();
+ const answerPcFirst = new RTCPeerConnection();
+ const answerPcSecond = new RTCPeerConnection();
+ t.add_cleanup(() => {
+ offerPc.close();
+ answerPcFirst.close();
+ answerPcSecond.close();
+ });
+ const offerDatachannel1 = offerPc.createDataChannel('initial');
+ exchangeIceCandidates(offerPc, answerPcFirst);
+
+ // Negotiate connection with PC 1
+ const offer1 = await offerPc.createOffer();
+ await offerPc.setLocalDescription(offer1);
+ await answerPcFirst.setRemoteDescription(offer1);
+ const answer1 = await answerPcFirst.createAnswer();
+ await offerPc.setRemoteDescription(answer1);
+ await answerPcFirst.setLocalDescription(answer1);
+ const datachannelAtAnswerPcFirst = await new Promise(
+ r => answerPcFirst.ondatachannel = ({channel}) => r(channel));
+ const iceTransport = offerPc.sctp.transport;
+ // Check that messages get through.
+ datachannelAtAnswerPcFirst.send('hello');
+ const message1 = await awaitMessage(offerDatachannel1);
+ assert_equals(message1, 'hello');
+
+ // Renegotiate with PC 2
+ // Note - ICE candidates will also be sent to answerPc1, but that shouldn't matter.
+ exchangeIceCandidates(offerPc, answerPcSecond);
+ const offer2 = await offerPc.createOffer();
+ await offerPc.setLocalDescription(offer2);
+ await answerPcSecond.setRemoteDescription(offer2);
+ const answer2 = await answerPcSecond.createAnswer();
+ await offerPc.setRemoteDescription(answer2);
+ await answerPcSecond.setLocalDescription(answer2);
+
+ // Kill the first PC. This should not affect anything, but leaving it may cause untoward events.
+ answerPcFirst.close();
+
+ const answerDataChannel2 = answerPcSecond.createDataChannel('second back');
+
+ const datachannelAtOfferPcSecond = await new Promise(r => offerPc.ondatachannel = ({channel}) => r(channel));
+
+ await new Promise(r => datachannelAtOfferPcSecond.onopen = r);
+
+ datachannelAtOfferPcSecond.send('hello again');
+ const message2 = await awaitMessage(answerDataChannel2);
+ assert_equals(message2, 'hello again');
+}, 'Handover with datachannel reinitiated from new callee completes');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/protocol/handover.html b/testing/web-platform/tests/webrtc/protocol/handover.html
new file mode 100644
index 0000000000..748cbeff8d
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/protocol/handover.html
@@ -0,0 +1,72 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerConnection Handovers</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../RTCPeerConnection-helper.js"></script>
+<script>
+'use strict';
+
+promise_test(async t => {
+ const offerPc = new RTCPeerConnection();
+ const answerPcFirst = new RTCPeerConnection();
+ const answerPcSecond = new RTCPeerConnection();
+ t.add_cleanup(() => {
+ offerPc.close();
+ answerPcFirst.close();
+ answerPcSecond.close();
+ });
+ offerPc.addTransceiver('audio');
+ // Negotiate connection with PC 1
+ const offer1 = await offerPc.createOffer();
+ await offerPc.setLocalDescription(offer1);
+ await answerPcFirst.setRemoteDescription(offer1);
+ const answer1 = await answerPcFirst.createAnswer();
+ await offerPc.setRemoteDescription(answer1);
+ await answerPcFirst.setLocalDescription(answer1);
+ // Renegotiate with PC 2
+ const offer2 = await offerPc.createOffer();
+ await offerPc.setLocalDescription(offer2);
+ await answerPcSecond.setRemoteDescription(offer2);
+ const answer2 = await answerPcSecond.createAnswer();
+ await offerPc.setRemoteDescription(answer2);
+ await answerPcSecond.setLocalDescription(answer2);
+}, 'Negotiation of handover initiated at caller works');
+
+promise_test(async t => {
+ const offerPc = new RTCPeerConnection();
+ const answerPcFirst = new RTCPeerConnection();
+ const answerPcSecond = new RTCPeerConnection();
+ t.add_cleanup(() => {
+ offerPc.close();
+ answerPcFirst.close();
+ answerPcSecond.close();
+ });
+ offerPc.addTransceiver('audio');
+ // Negotiate connection with PC 1
+ const offer1 = await offerPc.createOffer();
+ await offerPc.setLocalDescription(offer1);
+ await answerPcFirst.setRemoteDescription(offer1);
+ const answer1 = await answerPcFirst.createAnswer();
+ await offerPc.setRemoteDescription(answer1);
+ await answerPcFirst.setLocalDescription(answer1);
+ // Renegotiate with PC 2
+ // The offer from PC 2 needs to be consistent on at least the following:
+ // - Number, type and order of media sections
+ // - MID values
+ // - Payload type values
+ // Do a "fake" offer/answer using the original offer against PC2 to achieve this.
+ await answerPcSecond.setRemoteDescription(offer1);
+ // Discard the output of this round.
+ await answerPcSecond.setLocalDescription(await answerPcSecond.createAnswer());
+
+ // Now we can initiate an offer from the new PC.
+ const offer2 = await answerPcSecond.createOffer();
+ await answerPcSecond.setLocalDescription(offer2);
+ await offerPc.setRemoteDescription(offer2);
+ const answer2 = await offerPc.createAnswer();
+ await answerPcSecond.setRemoteDescription(answer2);
+ await offerPc.setLocalDescription(answer2);
+}, 'Negotiation of handover initiated at callee works');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/protocol/ice-state.https.html b/testing/web-platform/tests/webrtc/protocol/ice-state.https.html
new file mode 100644
index 0000000000..becce59509
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/protocol/ice-state.https.html
@@ -0,0 +1,130 @@
+<!doctype html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<title>RTCPeerConnection Failed State</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../RTCPeerConnection-helper.js"></script>
+<script>
+'use strict';
+
+// Tests for correct behavior of ICE state.
+
+// SDP copied from JSEP Example 7.1
+// It contains two media streams with different ufrags, and bundle
+// turned on.
+const kSdp = `v=0
+o=- 4962303333179871722 1 IN IP4 0.0.0.0
+s=-
+t=0 0
+a=ice-options:trickle
+a=group:BUNDLE a1 v1
+a=group:LS a1 v1
+m=audio 10100 UDP/TLS/RTP/SAVPF 96 0 8 97 98
+c=IN IP4 203.0.113.100
+a=mid:a1
+a=sendrecv
+a=rtpmap:96 opus/48000/2
+a=rtpmap:0 PCMU/8000
+a=rtpmap:8 PCMA/8000
+a=rtpmap:97 telephone-event/8000
+a=rtpmap:98 telephone-event/48000
+a=maxptime:120
+a=extmap:1 urn:ietf:params:rtp-hdrext:sdes:mid
+a=extmap:2 urn:ietf:params:rtp-hdrext:ssrc-audio-level
+a=msid:47017fee-b6c1-4162-929c-a25110252400 f83006c5-a0ff-4e0a-9ed9-d3e6747be7d9
+a=ice-ufrag:ETEn
+a=ice-pwd:OtSK0WpNtpUjkY4+86js7ZQl
+a=fingerprint:sha-256 19:E2:1C:3B:4B:9F:81:E6:B8:5C:F4:A5:A8:D8:73:04:BB:05:2F:70:9F:04:A9:0E:05:E9:26:33:E8:70:88:A2
+a=setup:actpass
+a=dtls-id:1
+a=rtcp:10101 IN IP4 203.0.113.100
+a=rtcp-mux
+a=rtcp-rsize
+m=video 10102 UDP/TLS/RTP/SAVPF 100 101
+c=IN IP4 203.0.113.100
+a=mid:v1
+a=sendrecv
+a=rtpmap:100 VP8/90000
+a=rtpmap:101 rtx/90000
+a=fmtp:101 apt=100
+a=extmap:1 urn:ietf:params:rtp-hdrext:sdes:mid
+a=rtcp-fb:100 ccm fir
+a=rtcp-fb:100 nack
+a=rtcp-fb:100 nack pli
+a=msid:47017fee-b6c1-4162-929c-a25110252400 f30bdb4a-5db8-49b5-bcdc-e0c9a23172e0
+a=ice-ufrag:BGKk
+a=ice-pwd:mqyWsAjvtKwTGnvhPztQ9mIf
+a=fingerprint:sha-256 19:E2:1C:3B:4B:9F:81:E6:B8:5C:F4:A5:A8:D8:73:04:BB:05:2F:70:9F:04:A9:0E:05:E9:26:33:E8:70:88:A2
+a=setup:actpass
+a=dtls-id:1
+a=rtcp:10103 IN IP4 203.0.113.100
+a=rtcp-mux
+a=rtcp-rsize
+`;
+
+// Returns a promise that resolves when |pc.iceConnectionState| is in one of the
+// wanted states, and rejects if it is in one of the unwanted states.
+// This is a variant of the function in RTCPeerConnection-helper.js.
+function waitForIceStateChange(pc, wantedStates, unwantedStates=[]) {
+ return new Promise((resolve, reject) => {
+ if (wantedStates.includes(pc.iceConnectionState)) {
+ resolve();
+ return;
+ } else if (unwantedStates.includes(pc.iceConnectionState)) {
+ reject('Unexpected state encountered: ' + pc.iceConnectionState);
+ return;
+ }
+ pc.addEventListener('iceconnectionstatechange', () => {
+ if (wantedStates.includes(pc.iceConnectionState)) {
+ resolve();
+ } else if (unwantedStates.includes(pc.iceConnectionState)) {
+ reject('Unexpected state encountered: ' + pc.iceConnectionState);
+ }
+ });
+ });
+}
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+ let [track, streams] = await getTrackFromUserMedia('video');
+ const sender = pc1.addTrack(track);
+ exchangeIceCandidates(pc1, pc2);
+ await exchangeOfferAnswer(pc1, pc2);
+ await waitForIceStateChange(pc1, ['connected', 'completed']);
+}, 'PC should enter connected (or completed) state when candidates are sent');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ let [track, streams] = await getTrackFromUserMedia('video');
+ const sender = pc1.addTrack(track);
+ const offer = await pc1.createOffer();
+ assert_greater_than_equal(offer.sdp.search('a=ice-options:trickle'), 0);
+}, 'PC should generate offer with a=ice-options:trickle');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ await pc1.setRemoteDescription({type: 'offer', sdp: kSdp});
+ const answer = await pc1.createAnswer();
+ await pc1.setLocalDescription(answer);
+ assert_greater_than_equal(answer.sdp.search('a=ice-options:trickle'), 0);
+ // When we use trickle ICE, and don't signal end-of-caniddates, we
+ // expect failure to result in 'disconnected' state rather than 'failed'.
+ const stateWaiter = waitForIceStateChange(pc1, ['disconnected'],
+ ['failed']);
+ // Add a bogus candidate. The candidate is drawn from the
+ // IANA "test-net-3" pool (RFC5737), so is guaranteed not to respond.
+ const candidateStr1 =
+ 'candidate:1 1 udp 2113929471 203.0.113.100 10100 typ host';
+ await pc1.addIceCandidate({candidate: candidateStr1,
+ sdpMid: 'a1',
+ usernameFragment: 'ETEn'});
+ await stateWaiter;
+}, 'PC should enter disconnected state when a failing candidate is sent');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/protocol/ice-ufragpwd.html b/testing/web-platform/tests/webrtc/protocol/ice-ufragpwd.html
new file mode 100644
index 0000000000..bd151284cb
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/protocol/ice-ufragpwd.html
@@ -0,0 +1,55 @@
+<!doctype html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<title>RTCPeerConnection Failed State</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../RTCPeerConnection-helper.js"></script>
+<script>
+'use strict';
+
+// Tests for validating ice-ufrag and ice-pwd syntax defined in
+// https://tools.ietf.org/html/rfc5245#section-15.4
+// Alphanumeric, '+' and '/' are allowed.
+
+const preamble = `v=0
+o=- 0 3 IN IP4 127.0.0.1
+s=-
+t=0 0
+a=fingerprint:sha-256 A7:24:72:CA:6E:02:55:39:BA:66:DF:6E:CC:4C:D8:B0:1A:BF:1A:56:65:7D:F4:03:AD:7E:77:43:2A:29:EC:93
+m=video 1 RTP/SAVPF 100
+c=IN IP4 0.0.0.0
+a=rtcp-mux
+a=sendonly
+a=mid:video
+a=rtpmap:100 VP8/30
+a=setup:actpass
+`;
+const valid_ufrag = 'a=ice-ufrag:ETEn\r\n';
+const valid_pwd = 'a=ice-pwd:OtSK0WpNtpUjkY4+86js7Z/l\r\n';
+const not_ice_char = '$'; // A snowman emoji would be cool but is not interoperable.
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const sdp = preamble +
+ valid_ufrag.replace('ETEn', 'E' + not_ice_char + 'En') +
+ valid_pwd;
+
+ return promise_rejects_dom(t, 'InvalidAccessError',
+ pc.setRemoteDescription({type: 'offer', sdp}));
+}, 'setRemoteDescription with a ice-ufrag containing a non-ice-char fails');
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ const sdp = preamble +
+ valid_ufrag +
+ valid_pwd.replace('K0Wp', 'K' + not_ice_char + 'Wp');
+
+ return promise_rejects_dom(t, 'InvalidAccessError',
+ pc.setRemoteDescription({type: 'offer', sdp}));
+}, 'setRemoteDescription with a ice-pwd containing a non-ice-char fails');
+</script>
diff --git a/testing/web-platform/tests/webrtc/protocol/jsep-initial-offer.https.html b/testing/web-platform/tests/webrtc/protocol/jsep-initial-offer.https.html
new file mode 100644
index 0000000000..50527f88df
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/protocol/jsep-initial-offer.https.html
@@ -0,0 +1,41 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerConnection.prototype.createOffer</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../RTCPeerConnection-helper.js"></script>
+<script>
+ 'use strict';
+
+ // Tests for the construction of initial offers according to
+ // draft-ietf-rtcweb-jsep-24 section 5.2.1
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ const offer = await generateVideoReceiveOnlyOffer(pc);
+ let offer_lines = offer.sdp.split('\r\n');
+ // The first 3 lines are dictated by JSEP.
+ assert_equals(offer_lines[0], "v=0");
+ assert_equals(offer_lines[1].slice(0, 2), "o=");
+
+ assert_regexp_match(offer_lines[1], /^o=\S+ \d+ \d+ IN IP4 \S+$/);
+ const fields = RegExp(/^o=\S+ (\d+) (\d+) IN IP4 (\S+)/).exec(offer_lines[1]);
+ // Per RFC 3264, the sess-id should be representable in an uint64
+ // Note: JSEP -24 has this wrong - see bug:
+ // https://github.com/rtcweb-wg/jsep/issues/855
+ assert_less_than(Number(fields[1]), 2**64);
+ // Per RFC 3264, the version should be less than 2^62 to avoid overflow
+ assert_less_than(Number(fields[2]), 2**62);
+ // JSEP says that the address part SHOULD be a meaningless address
+ // "such as" IN IP4 0.0.0.0. This is to prevent unintentional disclosure
+ // of IP addresses, so this is important enough to verify. Right now we
+ // allow 127.0.0.1 and 0.0.0.0, but there are other things we could allow.
+ // Maybe 0.0.0.0/8, 127.0.0.0/8, 192.0.2.0/24, 198.51.100.0/24, 203.0.113.0/24?
+ // (See RFC 3330, RFC 5737)
+ assert_true(fields[3] == "0.0.0.0" || fields[3] == "127.0.0.1",
+ fields[3] + " must be a meaningless IPV4 address")
+
+ assert_regexp_match(offer_lines[2], /^s=\S+$/);
+ // After this, the order is not dictated by JSEP.
+ // TODO: Check lines subsequent to the s= line.
+ }, 'Offer conforms to basic SDP requirements');
+</script>
diff --git a/testing/web-platform/tests/webrtc/protocol/missing-fields.html b/testing/web-platform/tests/webrtc/protocol/missing-fields.html
new file mode 100644
index 0000000000..d5aafd230e
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/protocol/missing-fields.html
@@ -0,0 +1,47 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerconnection SDP parse tests</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../RTCPeerConnection-helper.js"></script>
+<script>
+'use strict';
+
+function removeSdpLines(description, toRemove) {
+ const edited = description.sdp.split('\n').filter(function(line) {
+ return (!line.startsWith(toRemove));
+ }).join('\n');
+ return {type: description.type, sdp: edited};
+}
+
+promise_test(async t => {
+ const caller = new RTCPeerConnection();
+ const callee = new RTCPeerConnection();
+ t.add_cleanup(() => caller.close());
+ t.add_cleanup(() => callee.close());
+ caller.addTrack(trackFactories.audio());
+ const offer = await caller.createOffer();
+ await caller.setLocalDescription(offer);
+ let remote_offer = removeSdpLines(offer, 'a=mid:');
+ remote_offer = removeSdpLines(remote_offer, 'a=group:');
+ await callee.setRemoteDescription(remote_offer);
+ const answer = await callee.createAnswer();
+ await caller.setRemoteDescription(answer);
+}, 'Offer description with no mid is accepted');
+
+promise_test(async t => {
+ const caller = new RTCPeerConnection();
+ const callee = new RTCPeerConnection();
+ t.add_cleanup(() => caller.close());
+ t.add_cleanup(() => callee.close());
+ caller.addTrack(trackFactories.audio());
+ const offer = await caller.createOffer();
+ await caller.setLocalDescription(offer);
+ await callee.setRemoteDescription(offer);
+ const answer = await callee.createAnswer();
+ let remote_answer = removeSdpLines(answer, 'a=mid:');
+ remote_answer = removeSdpLines(remote_answer, 'a=group:');
+ await caller.setRemoteDescription(remote_answer);
+}, 'Answer description with no mid is accepted');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/protocol/msid-generate.html b/testing/web-platform/tests/webrtc/protocol/msid-generate.html
new file mode 100644
index 0000000000..29226c704e
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/protocol/msid-generate.html
@@ -0,0 +1,160 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerconnection MSID generation</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../RTCPeerConnection-helper.js"></script>
+<script src="../third_party/sdp/sdp.js"></script>
+<script>
+
+function msidLines(desc) {
+ const sections = SDPUtils.splitSections(desc.sdp);
+ return SDPUtils.matchPrefix(sections[1], 'a=msid:');
+}
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const dc = pc.createDataChannel('foo');
+ const desc = await pc.createOffer();
+ assert_equals(msidLines(desc).length, 0);
+}, 'No media track produces no MSID');
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const stream1 = await getNoiseStream({audio: true});
+ pc.addTrack(stream1.getTracks()[0]);
+ const desc = await pc.createOffer();
+ const msid_lines = msidLines(desc);
+ assert_equals(msid_lines.length, 1);
+ assert_regexp_match(msid_lines[0], /^a=msid:-/);
+}, 'AddTrack without a stream produces MSID with no stream ID');
+
+// token-char from RFC 4566
+// This is printable characters except whitespace, and ["(),/:;<=>?@[\]]
+const token_char = '\\x21\\x23-\\x27\\x2A-\\x2B\\x2D-\\x2E\\x30-\\x39\\x41-\\x5A\\x5E-\\x7E';
+
+// msid-value from RFC 8830
+const msid_attr = RegExp(`^a=msid:[${token_char}]{1,64}( [${token_char}]{1,64})?$`);
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const stream1 = await getNoiseStream({audio: true});
+ pc.addTrack(stream1.getTracks()[0], stream1);
+ const desc = await pc.createOffer();
+ const msid_lines = msidLines(desc);
+ assert_equals(msid_lines.length, 1);
+ assert_regexp_match(msid_lines[0], msid_attr);
+}, 'AddTrack with a stream produces MSID with a stream ID');
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const stream1 = await getNoiseStream({audio: true});
+ const stream2 = new MediaStream(stream1.getTracks());
+ pc.addTrack(stream1.getTracks()[0], stream1, stream2);
+ const desc = await pc.createOffer();
+ const msid_lines = msidLines(desc);
+ assert_equals(msid_lines.length, 2);
+ assert_regexp_match(msid_lines[0], msid_attr);
+ assert_regexp_match(msid_lines[1], msid_attr);
+}, 'AddTrack with two streams produces two MSID lines');
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const stream1 = await getNoiseStream({audio: true});
+ pc.addTrack(stream1.getTracks()[0], stream1, stream1);
+ const desc = await pc.createOffer();
+ const msid_lines = msidLines(desc);
+ assert_equals(msid_lines.length, 1);
+ assert_regexp_match(msid_lines[0], msid_attr);
+}, 'AddTrack with the stream twice produces single MSID with a stream ID');
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const stream1 = await getNoiseStream({audio: true});
+ pc.addTransceiver(stream1.getTracks()[0]);
+ const desc = await pc.createOffer();
+ const msid_lines = msidLines(desc);
+ assert_equals(msid_lines.length, 1);
+ assert_regexp_match(msid_lines[0], /^a=msid:-/);
+}, 'AddTransceiver without a stream produces MSID with no stream ID');
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const stream1 = await getNoiseStream({audio: true});
+ pc.addTransceiver(stream1.getTracks()[0], {streams: [stream1]});
+ const desc = await pc.createOffer();
+ const msid_lines = msidLines(desc);
+ assert_equals(msid_lines.length, 1);
+ assert_regexp_match(msid_lines[0], msid_attr);
+}, 'AddTransceiver with a stream produces MSID with a stream ID');
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const stream1 = await getNoiseStream({audio: true});
+ const stream2 = new MediaStream(stream1.getTracks());
+ pc.addTransceiver(stream1.getTracks()[0], {streams: [stream1, stream2]});
+ const desc = await pc.createOffer();
+ const msid_lines = msidLines(desc);
+ assert_equals(msid_lines.length, 2);
+ assert_regexp_match(msid_lines[0], msid_attr);
+ assert_regexp_match(msid_lines[1], msid_attr);
+}, 'AddTransceiver with two streams produces two MSID lines');
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const stream1 = await getNoiseStream({audio: true});
+ pc.addTransceiver(stream1.getTracks()[0], {streams: [stream1, stream1]});
+const desc = await pc.createOffer();
+ const msid_lines = msidLines(desc);
+ assert_equals(msid_lines.length, 1);
+ assert_regexp_match(msid_lines[0], msid_attr);
+}, 'AddTransceiver with the stream twice produces single MSID with a stream ID');
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const stream1 = await getNoiseStream({audio: true});
+ const {sender} = pc.addTransceiver(stream1.getTracks()[0]);
+ sender.setStreams(stream1);
+ const desc = await pc.createOffer();
+ const msid_lines = msidLines(desc);
+ assert_equals(msid_lines.length, 1);
+ assert_regexp_match(msid_lines[0], msid_attr);
+}, 'SetStreams with a stream produces MSID with a stream ID');
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const stream1 = await getNoiseStream({audio: true});
+ const stream2 = new MediaStream(stream1.getTracks());
+ const {sender} = pc.addTransceiver(stream1.getTracks()[0]);
+ sender.setStreams(stream1, stream2);
+ const desc = await pc.createOffer();
+ const msid_lines = msidLines(desc);
+ assert_equals(msid_lines.length, 2);
+ assert_regexp_match(msid_lines[0], msid_attr);
+ assert_regexp_match(msid_lines[1], msid_attr);
+}, 'SetStreams with two streams produces two MSID lines');
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const stream1 = await getNoiseStream({audio: true});
+ const {sender} = pc.addTransceiver(stream1.getTracks()[0]);
+ sender.setStreams(stream1, stream1);
+ const desc = await pc.createOffer();
+ const msid_lines = msidLines(desc);
+ assert_equals(msid_lines.length, 1);
+ assert_regexp_match(msid_lines[0], msid_attr);
+}, 'SetStreams with the stream twice produces single MSID with a stream ID');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/protocol/msid-parse.html b/testing/web-platform/tests/webrtc/protocol/msid-parse.html
new file mode 100644
index 0000000000..5596446e00
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/protocol/msid-parse.html
@@ -0,0 +1,83 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerconnection MSID parsing</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../RTCPeerConnection-helper.js"></script>
+<script>
+'use strict';
+const preamble = `v=0
+o=- 0 3 IN IP4 127.0.0.1
+s=-
+t=0 0
+a=fingerprint:sha-256 A7:24:72:CA:6E:02:55:39:BA:66:DF:6E:CC:4C:D8:B0:1A:BF:1A:56:65:7D:F4:03:AD:7E:77:43:2A:29:EC:93
+a=ice-ufrag:6HHHdzzeIhkE0CKj
+a=ice-pwd:XYDGVpfvklQIEnZ6YnyLsAew
+m=video 1 RTP/SAVPF 100
+c=IN IP4 0.0.0.0
+a=rtcp-mux
+a=sendonly
+a=mid:video
+a=rtpmap:100 VP8/30
+a=setup:actpass
+`;
+
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const ontrackPromise = addEventListenerPromise(t, pc, 'track');
+ await pc.setRemoteDescription({type: 'offer', sdp: preamble});
+ const trackevent = await ontrackPromise;
+ assert_equals(pc.getReceivers().length, 1);
+ assert_equals(trackevent.streams.length, 1, 'Stream count');
+}, 'Description with no msid produces a track with a stream');
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const ontrackPromise = addEventListenerPromise(t, pc, 'track');
+ await pc.setRemoteDescription({type: 'offer',
+ sdp: preamble + 'a=msid:- foobar\n'});
+ const trackevent = await ontrackPromise;
+ assert_equals(pc.getReceivers().length, 1);
+ assert_equals(trackevent.streams.length, 0);
+}, 'Description with msid:- appid produces a track with no stream');
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const ontrackPromise = addEventListenerPromise(t, pc, 'track');
+ await pc.setRemoteDescription({type: 'offer',
+ sdp: preamble + 'a=msid:foo bar\n'});
+ const trackevent = await ontrackPromise;
+ assert_equals(pc.getReceivers().length, 1);
+ assert_equals(trackevent.streams.length, 1);
+ assert_equals(trackevent.streams[0].id, 'foo');
+}, 'Description with msid:foo bar produces a stream with id foo');
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const ontrackPromise = addEventListenerPromise(t, pc, 'track');
+ await pc.setRemoteDescription({type: 'offer',
+ sdp: preamble + 'a=msid:foo bar\n'
+ + 'a=msid:baz bar\n'});
+ const trackevent = await ontrackPromise;
+ assert_equals(pc.getReceivers().length, 1);
+ assert_equals(trackevent.streams.length, 2);
+}, 'Description with two msid produces two streams');
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const ontrackPromise = addEventListenerPromise(t, pc, 'track');
+ await pc.setRemoteDescription({type: 'offer',
+ sdp: preamble + 'a=msid:foo\n'});
+ const trackevent = await ontrackPromise;
+ assert_equals(pc.getReceivers().length, 1);
+ assert_equals(trackevent.streams.length, 1);
+ assert_equals(trackevent.streams[0].id, 'foo');
+}, 'Description with msid foo but no track id is accepted');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/protocol/rtp-clockrate.html b/testing/web-platform/tests/webrtc/protocol/rtp-clockrate.html
new file mode 100644
index 0000000000..4177420050
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/protocol/rtp-clockrate.html
@@ -0,0 +1,40 @@
+<!doctype html>
+<meta charset=utf-8>
+<!-- This file contains a test that waits for two seconds. -->
+<meta name="timeout" content="long">
+<title>RTP clockrate</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../RTCPeerConnection-helper.js"></script>
+<script>
+'use strict';
+
+async function initiateSingleTrackCallAndReturnReceiver(t, kind) {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const stream = await getNoiseStream({[kind]:true});
+ const [track] = stream.getTracks();
+ t.add_cleanup(() => track.stop());
+ pc1.addTrack(track, stream);
+
+ exchangeIceCandidates(pc1, pc2);
+ const trackEvent = await exchangeOfferAndListenToOntrack(t, pc1, pc2);
+ await exchangeAnswer(pc1, pc2);
+ await waitForConnectionStateChange(pc2, ['connected']);
+ return trackEvent.receiver;
+}
+
+promise_test(async t => {
+ // the getSynchronizationSources API exposes the rtp timestamp.
+ const receiver = await initiateSingleTrackCallAndReturnReceiver(t, 'video');
+ const first = await listenForSSRCs(t, receiver);
+ await new Promise(resolve => t.step_timeout(resolve, 2000));
+ const second = await listenForSSRCs(t, receiver);
+ // rtpTimestamp may wrap at 0xffffffff, take care of that.
+ const actualClockRate = ((second[0].rtpTimestamp - first[0].rtpTimestamp + 0xffffffff) % 0xffffffff) / (second[0].timestamp - first[0].timestamp) * 1000;
+ assert_approx_equals(actualClockRate, 90000, 9000, 'Video clockrate is approximately 90000');
+}, 'video rtp timestamps increase by approximately 90000 per second');
+</script>
diff --git a/testing/web-platform/tests/webrtc/protocol/rtp-demuxing.html b/testing/web-platform/tests/webrtc/protocol/rtp-demuxing.html
new file mode 100644
index 0000000000..de08b2197f
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/protocol/rtp-demuxing.html
@@ -0,0 +1,109 @@
+<!doctype html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<title>RTCPeerConnection payload type demuxing</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../RTCPeerConnection-helper.js"></script>
+<script>
+'use strict';
+promise_test(async t => {
+ const caller = new RTCPeerConnection();
+ t.add_cleanup(() => caller.close());
+ const callee = new RTCPeerConnection();
+ t.add_cleanup(() => callee.close());
+ exchangeIceCandidates(caller, callee);
+
+ const stream = await getNoiseStream({video: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ stream.getTracks().forEach(track => caller.addTrack(track, stream));
+ stream.getTracks().forEach(track => caller.addTrack(track.clone(), stream.clone()));
+
+ let callCount = 0;
+ let metadataToBeLoaded = new Promise(resolve => {
+ callee.ontrack = (e) => {
+ const stream = e.streams[0];
+ const v = document.createElement('video');
+ v.autoplay = true;
+ v.srcObject = stream;
+ v.id = stream.id
+ v.addEventListener('loadedmetadata', () => {
+ if (++callCount === 2) {
+ resolve();
+ }
+ });
+ };
+ });
+
+ // Restrict first transceiver to VP8, second to H264.
+ const {codecs} = RTCRtpSender.getCapabilities('video');
+ const vp8 = codecs.find(c => c.mimeType === 'video/VP8');
+ const h264 = codecs.find(c => c.mimeType === 'video/H264');
+ caller.getTransceivers()[0].setCodecPreferences([vp8]);
+ caller.getTransceivers()[1].setCodecPreferences([h264]);
+
+ const offer = await caller.createOffer();
+ // Replace the mid header extension and all ssrc lines
+ // with bogus. The receiver will be forced to do payload type demuxing.
+ const sdp = offer.sdp
+ .replace(/rtp-hdrext:sdes/g, 'rtp-hdrext:something')
+ .replace(/a=ssrc:/g, 'a=notssrc');
+
+ await callee.setRemoteDescription({type: 'offer', sdp});
+ await caller.setLocalDescription(offer);
+
+ const answer = await callee.createAnswer();
+ await caller.setRemoteDescription(answer);
+ await callee.setLocalDescription(answer);
+
+ await metadataToBeLoaded;
+}, 'Can demux two video tracks with different payload types on a bundled connection');
+
+promise_test(async t => {
+ const caller = new RTCPeerConnection({bundlePolicy: 'max-compat'});
+ t.add_cleanup(() => caller.close());
+ const callee = new RTCPeerConnection();
+ t.add_cleanup(() => callee.close());
+ exchangeIceCandidates(caller, callee);
+
+ const stream = await getNoiseStream({video: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ stream.getTracks().forEach(track => caller.addTrack(track, stream));
+ stream.getTracks().forEach(track => caller.addTrack(track.clone(), stream.clone()));
+
+ let callCount = 0;
+ let metadataToBeLoaded = new Promise(resolve => {
+ callee.ontrack = (e) => {
+ const stream = e.streams[0];
+ const v = document.createElement('video');
+ v.autoplay = true;
+ v.srcObject = stream;
+ v.id = stream.id
+ v.addEventListener('loadedmetadata', () => {
+ if (++callCount === 2) {
+ resolve();
+ }
+ });
+ };
+ });
+
+ const offer = await caller.createOffer();
+ // Replace BUNDLE, the mid header extension and all ssrc lines
+ // with bogus. The receiver will be forced to do payload type demuxing
+ // which is still possible because the different m-lines arrive on
+ // different ports/sockets.
+ const sdp = offer.sdp.replace('BUNDLE', 'SOMETHING')
+ .replace(/rtp-hdrext:sdes/g, 'rtp-hdrext:something')
+ .replace(/a=ssrc:/g, 'a=notssrc');
+
+ await callee.setRemoteDescription({type: 'offer', sdp});
+ await caller.setLocalDescription(offer);
+
+ const answer = await callee.createAnswer();
+ await caller.setRemoteDescription(answer);
+ await callee.setLocalDescription(answer);
+
+ await metadataToBeLoaded;
+}, 'Can demux two video tracks with the same payload type on an unbundled connection');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/protocol/rtp-extension-support.html b/testing/web-platform/tests/webrtc/protocol/rtp-extension-support.html
new file mode 100644
index 0000000000..045701c171
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/protocol/rtp-extension-support.html
@@ -0,0 +1,78 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerConnection RTP extensions</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../third_party/sdp/sdp.js"></script>
+<script>
+'use strict';
+
+async function setup() {
+ const pc1 = new RTCPeerConnection();
+ pc1.addTransceiver('audio');
+ // Make sure there is more than one rid, since there's no reason to use
+ // rtp-stream-id/repaired-rtp-stream-id otherwise. Some implementations
+ // may use them for unicast anyway, which isn't a spec violation, just
+ // a little silly.
+ pc1.addTransceiver('video', {sendEncodings: [{rid: '0'}, {rid: '1'}]});
+ const offer = await pc1.createOffer();
+ pc1.close();
+ return offer.sdp;
+}
+
+// Extensions that MUST be supported
+const mandatoryExtensions = [
+ // Directly referenced in WebRTC RTP usage
+ 'urn:ietf:params:rtp-hdrext:ssrc-audio-level', // RFC 8834 5.2.2
+ 'urn:ietf:params:rtp-hdrext:sdes:mid', // RFC 8834 5.2.4
+ 'urn:3gpp:video-orientation', // RFC 8834 5.2.5
+ // Required for support of simulcast with RID
+ 'urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id', // RFC 8852 4.3
+ 'urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id', // RFC 8852 4.4
+];
+
+// For further testing:
+// - Add test for rapid synchronization - RFC 8834 5.2.1
+// - Add test for encrypted header extensions (RFC 6904)
+// - Separate tests for extensions in audio and video sections
+
+for (const extension of mandatoryExtensions) {
+ promise_test(async t => {
+ const sdp = await setup();
+ const extensions = SDPUtils.matchPrefix(sdp, 'a=extmap:')
+ .map(SDPUtils.parseExtmap);
+ assert_true(!!extensions.find(ext => ext.uri === extension));
+ }, `RTP header extension ${extension} is present in offer`);
+}
+
+// Test for illegal remote behavior: Reassignment of hdrext ID
+// in a subsequent offer/answer cycle.
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ pc1.addTransceiver('audio');
+ await pc1.setLocalDescription();
+ await pc2.setRemoteDescription(pc1.localDescription);
+ await pc2.setLocalDescription();
+ await pc1.setRemoteDescription(pc2.localDescription);
+ // Do a second offer/answer cycle.
+ await pc1.setLocalDescription();
+ await pc2.setRemoteDescription(pc1.localDescription);
+ const answer = await pc2.createAnswer();
+
+ // Swap the extension number of the two required extensions
+ answer.sdp = answer.sdp.replace('urn:ietf:params:rtp-hdrext:ssrc-audio-level',
+ 'xyzzy')
+ .replace('urn:ietf:params:rtp-hdrext:sdes:mid',
+ 'urn:ietf:params:rtp-hdrext:ssrc-audio-level')
+ .replace('xyzzy',
+ 'urn:ietf:params:rtp-hdrext:sdes:mid');
+
+ return promise_rejects_dom(t, 'InvalidAccessError',
+ pc1.setRemoteDescription(answer));
+}, 'RTP header extension reassignment causes failure');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/protocol/rtp-headerextensions.html b/testing/web-platform/tests/webrtc/protocol/rtp-headerextensions.html
new file mode 100644
index 0000000000..c377a613f6
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/protocol/rtp-headerextensions.html
@@ -0,0 +1,101 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>payload type handling (assuming rtcp-mux)</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../third_party/sdp/sdp.js"></script>
+<script>
+'use strict';
+// Tests behaviour from https://www.rfc-editor.org/rfc/rfc8834.html#name-header-extensions
+
+function createOfferSdp(extmaps) {
+ let sdp = `v=0
+o=- 0 3 IN IP4 127.0.0.1
+s=-
+t=0 0
+a=fingerprint:sha-256 A7:24:72:CA:6E:02:55:39:BA:66:DF:6E:CC:4C:D8:B0:1A:BF:1A:56:65:7D:F4:03:AD:7E:77:43:2A:29:EC:93
+a=ice-ufrag:6HHHdzzeIhkE0CKj
+a=ice-pwd:XYDGVpfvklQIEnZ6YnyLsAew
+`;
+ sdp += 'a=group:BUNDLE ' + ['audio', 'video'].filter(kind => extmaps[kind]).join(' ') + '\r\n';
+ if (extmaps.audio) {
+ sdp += `m=audio 9 RTP/SAVPF 111
+c=IN IP4 0.0.0.0
+a=rtcp-mux
+a=sendonly
+a=mid:audio
+a=rtpmap:111 opus/48000/2
+a=setup:actpass
+` + extmaps.audio.map(ext => SDPUtils.writeExtmap(ext));
+ }
+ if (extmaps.video) {
+ sdp += `m=video 9 RTP/SAVPF 112
+c=IN IP4 0.0.0.0
+a=rtcp-mux
+a=sendonly
+a=mid:video
+a=rtpmap:112 VP8/90000
+a=setup:actpass
+` + extmaps.video.map(ext => SDPUtils.writeExtmap(ext));
+ }
+ return sdp;
+}
+
+[
+ // https://www.rfc-editor.org/rfc/rfc8834.html#section-5.2.4
+ {
+ audio: [{id: 1, uri: 'urn:ietf:params:rtp-hdrext:sdes:mid'}],
+ video: [{id: 1, uri: 'urn:ietf:params:rtp-hdrext:sdes:mid'}],
+ description: 'MID',
+ },
+ {
+ // https://www.rfc-editor.org/rfc/rfc8834.html#section-5.2.2
+ audio: [{id: 1, uri: 'urn:ietf:params:rtp-hdrext:ssrc-audio-level'}],
+ description: 'Audio level',
+ },
+ {
+ // https://www.rfc-editor.org/rfc/rfc8834.html#section-5.2.5
+ video: [{id: 1, uri: 'urn:3gpp:video-orientation'}],
+ description: 'Video orientation',
+ }
+].forEach(testcase => {
+ promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ await pc.setRemoteDescription({type: 'offer', sdp: createOfferSdp(testcase)});
+ const answer = await pc.createAnswer();
+ const sections = SDPUtils.splitSections(answer.sdp);
+ sections.shift();
+ sections.forEach(section => {
+ const rtpParameters = SDPUtils.parseRtpParameters(section);
+ assert_equals(rtpParameters.headerExtensions.length, 1);
+ assert_equals(rtpParameters.headerExtensions[0].id, testcase[SDPUtils.getKind(section)][0].id);
+ assert_equals(rtpParameters.headerExtensions[0].uri, testcase[SDPUtils.getKind(section)][0].uri);
+ });
+ }, testcase.description + ' header extension is supported.');
+});
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ pc.addTransceiver('video');
+ const offer = await pc.createOffer();
+ const section = SDPUtils.splitSections(offer.sdp)[1];
+ const extensions = SDPUtils.matchPrefix(section, 'a=extmap:')
+ .map(line => SDPUtils.parseExtmap(line));
+ const extension_not_mid = extensions.find(e => e.uri !== 'urn:ietf:params:rtp-hdrext:sdes:mid');
+ await pc.setRemoteDescription({type :'offer', sdp: offer.sdp.replace(extension_not_mid.uri, 'bogus')});
+
+ await pc.setLocalDescription();
+ const answer_section = SDPUtils.splitSections(pc.localDescription.sdp)[1];
+ const answer_extensions = SDPUtils.matchPrefix(answer_section, 'a=extmap:')
+ .map(line => SDPUtils.parseExtmap(line));
+ assert_equals(answer_extensions.length, extensions.length - 1);
+ assert_false(!!extensions.find(e => e.uri === 'bogus'));
+ for (const answer_extension of answer_extensions) {
+ assert_true(!!extensions.find(e => e.uri === answer_extension.uri));
+ }
+}, 'Negotiates the subset of supported extensions offered');
+</script>
diff --git a/testing/web-platform/tests/webrtc/protocol/rtp-payloadtypes.html b/testing/web-platform/tests/webrtc/protocol/rtp-payloadtypes.html
new file mode 100644
index 0000000000..af7656d131
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/protocol/rtp-payloadtypes.html
@@ -0,0 +1,61 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>payload type handling (assuming rtcp-mux)</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../RTCPeerConnection-helper.js"></script>
+<script>
+'use strict';
+// Tests behaviour from https://tools.ietf.org/html/rfc5761#section-4
+
+function createOfferSdp(opusPayloadType) {
+ return `v=0
+o=- 0 3 IN IP4 127.0.0.1
+s=-
+t=0 0
+a=fingerprint:sha-256 A7:24:72:CA:6E:02:55:39:BA:66:DF:6E:CC:4C:D8:B0:1A:BF:1A:56:65:7D:F4:03:AD:7E:77:43:2A:29:EC:93
+a=ice-ufrag:6HHHdzzeIhkE0CKj
+a=ice-pwd:XYDGVpfvklQIEnZ6YnyLsAew
+m=audio 9 RTP/SAVPF ${opusPayloadType}
+c=IN IP4 0.0.0.0
+a=rtcp-mux
+a=sendonly
+a=mid:audio
+a=rtpmap:${opusPayloadType} opus/48000/2
+a=setup:actpass
+`;
+}
+
+promise_test(async t => {
+ for (let payloadType = 96; payloadType <= 127; payloadType++) {
+ const pc = new RTCPeerConnection();
+ await pc.setRemoteDescription({type: 'offer', sdp: createOfferSdp(payloadType)});
+ const answer = await pc.createAnswer();
+ assert_true(answer.sdp.includes(`a=rtpmap:${payloadType} opus/48000/2`));
+ pc.close();
+ }
+}, 'setRemoteDescription with a codec in the range 96-127 works');
+
+// This is written as a separate test since it currently fails in Chrome.
+promise_test(async t => {
+ for (let payloadType = 35; payloadType <= 63; payloadType++) {
+ const pc = new RTCPeerConnection();
+ await pc.setRemoteDescription({type: 'offer', sdp: createOfferSdp(payloadType)});
+ const answer = await pc.createAnswer();
+ assert_true(answer.sdp.includes(`a=rtpmap:${payloadType} opus/48000/2`));
+ pc.close();
+ }
+}, 'setRemoteDescription with a codec in the range 35-63 works');
+
+promise_test(async t => {
+ for (let payloadType = 64; payloadType <= 95; payloadType++) {
+ const pc = new RTCPeerConnection();
+ await promise_rejects_dom(t, 'InvalidAccessError',
+ pc.setRemoteDescription({type: 'offer', sdp: createOfferSdp(payloadType)}),
+ 'Failed to reject on PT ' + payloadType);
+
+
+ pc.close();
+ }
+}, 'setRemoteDescription with a codec in the range 64-95 throws an InvalidAccessError');
+</script>
diff --git a/testing/web-platform/tests/webrtc/protocol/rtx-codecs.https.html b/testing/web-platform/tests/webrtc/protocol/rtx-codecs.https.html
new file mode 100644
index 0000000000..78519c75cc
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/protocol/rtx-codecs.https.html
@@ -0,0 +1,153 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTX codec integrity checks</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../RTCPeerConnection-helper.js"></script>
+<script src="../third_party/sdp/sdp.js"></script>
+<script>
+'use strict';
+
+// Tests for conformance to rules for RTX codecs.
+// Basic rule: Offers and answers must contain RTX codecs, and the
+// RTX codecs must have an a=fmtp line that points to a non-RTX codec.
+
+// Helper function for doing one round of offer/answer exchange
+// between two local peer connections.
+// Calls setRemoteDescription(offer/answer) before
+// setLocalDescription(offer/answer) to ensure the remote description
+// is set and candidates can be added before the local peer connection
+// starts generating candidates and ICE checks.
+async function doSignalingHandshake(localPc, remotePc, options={}) {
+ let offer = await localPc.createOffer();
+ // Modify offer if callback has been provided
+ if (options.modifyOffer) {
+ offer = await options.modifyOffer(offer);
+ }
+
+ // Apply offer.
+ await remotePc.setRemoteDescription(offer);
+ await localPc.setLocalDescription(offer);
+
+ let answer = await remotePc.createAnswer();
+ // Modify answer if callback has been provided
+ if (options.modifyAnswer) {
+ answer = await options.modifyAnswer(answer);
+ }
+
+ // Apply answer.
+ await localPc.setRemoteDescription(answer);
+ await remotePc.setLocalDescription(answer);
+}
+
+function verifyRtxReferences(description) {
+ const mediaSection = SDPUtils.getMediaSections(description.sdp)[0];
+ const rtpParameters = SDPUtils.parseRtpParameters(mediaSection);
+ for (const codec of rtpParameters.codecs) {
+ if (codec.name === 'rtx') {
+ assert_own_property(codec.parameters, 'apt', 'rtx codec has apt parameter');
+ const referenced_codec = rtpParameters.codecs.find(
+ c => c.payloadType === parseInt(codec.parameters.apt));
+ assert_true(referenced_codec !== undefined, `Found referenced codec`);
+ }
+ }
+}
+
+
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ const offer = await generateVideoReceiveOnlyOffer(pc);
+ verifyRtxReferences(offer);
+}, 'Initial offer should have sensible RTX mappings');
+
+async function negotiateAndReturnAnswer(t) {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+ let [track, streams] = await getTrackFromUserMedia('video');
+ const sender = pc1.addTrack(track);
+ await doSignalingHandshake(pc1, pc2);
+ return pc2.localDescription;
+}
+
+promise_test(async t => {
+ const answer = await negotiateAndReturnAnswer(t);
+ verifyRtxReferences(answer);
+}, 'Self-negotiated answer should have sensible RTX parameters');
+
+promise_test(async t => {
+ const sampleOffer = `v=0
+o=- 1878890426675213188 2 IN IP4 127.0.0.1
+s=-
+t=0 0
+a=group:BUNDLE video
+a=msid-semantic: WMS
+m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99
+c=IN IP4 0.0.0.0
+a=rtcp:9 IN IP4 0.0.0.0
+a=ice-ufrag:RGPK
+a=ice-pwd:rAyHEAKC7ckxQgWaRZXukz+Z
+a=ice-options:trickle
+a=fingerprint:sha-256 8C:29:0A:8F:11:06:BF:1C:58:B3:CA:E6:F1:F1:DC:99:4C:6C:89:E9:FF:BC:D4:38:11:18:1F:40:19:C8:49:37
+a=setup:actpass
+a=mid:video
+a=recvonly
+a=rtcp-mux
+a=rtpmap:97 rtx/90000
+a=fmtp:97 apt=98
+a=rtpmap:98 VP8/90000
+a=rtcp-fb:98 ccm fir
+a=rtcp-fb:98 nack
+a=rtcp-fb:98 nack pli
+a=rtcp-fb:98 goog-remb
+a=rtcp-fb:98 transport-cc
+`;
+ const pc = new RTCPeerConnection();
+ let [track, streams] = await getTrackFromUserMedia('video');
+ const sender = pc.addTrack(track);
+ await pc.setRemoteDescription({type: 'offer', sdp: sampleOffer});
+ const answer = await pc.createAnswer();
+ verifyRtxReferences(answer);
+}, 'A remote offer generates sensible RTX references in answer');
+
+promise_test(async t => {
+ const sampleOffer = `v=0
+o=- 1878890426675213188 2 IN IP4 127.0.0.1
+s=-
+t=0 0
+a=group:BUNDLE video
+a=msid-semantic: WMS
+m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99
+c=IN IP4 0.0.0.0
+a=rtcp:9 IN IP4 0.0.0.0
+a=ice-ufrag:RGPK
+a=ice-pwd:rAyHEAKC7ckxQgWaRZXukz+Z
+a=ice-options:trickle
+a=fingerprint:sha-256 8C:29:0A:8F:11:06:BF:1C:58:B3:CA:E6:F1:F1:DC:99:4C:6C:89:E9:FF:BC:D4:38:11:18:1F:40:19:C8:49:37
+a=setup:actpass
+a=mid:video
+a=recvonly
+a=rtcp-mux
+a=rtpmap:96 VP8/90000
+a=rtpmap:97 rtx/90000
+a=fmtp:97 apt=98
+a=rtpmap:98 VP8/90000
+a=rtcp-fb:98 ccm fir
+a=rtcp-fb:98 nack
+a=rtcp-fb:98 nack pli
+a=rtcp-fb:98 goog-remb
+a=rtcp-fb:98 transport-cc
+a=rtpmap:99 rtx/90000
+a=fmtp:99 apt=96
+`;
+ const pc = new RTCPeerConnection();
+ let [track, streams] = await getTrackFromUserMedia('video');
+ const sender = pc.addTrack(track);
+ await pc.setRemoteDescription({type: 'offer', sdp: sampleOffer});
+ const answer = await pc.createAnswer();
+ verifyRtxReferences(answer);
+}, 'A remote offer with duplicate codecs generates sensible RTX references in answer');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/protocol/sctp-format.html b/testing/web-platform/tests/webrtc/protocol/sctp-format.html
new file mode 100644
index 0000000000..207e51d4c3
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/protocol/sctp-format.html
@@ -0,0 +1,25 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerconnection SDP SCTP format test</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../RTCPeerConnection-helper.js"></script>
+<script>
+'use strict';
+
+promise_test(async t => {
+ const caller = new RTCPeerConnection();
+ const callee = new RTCPeerConnection();
+ t.add_cleanup(() => caller.close());
+ t.add_cleanup(() => callee.close());
+ caller.createDataChannel('channel');
+ const offer = await caller.createOffer();
+ const [preamble, media_section, postamble] = offer.sdp.split('\r\nm=');
+ assert_true(typeof(postamble) === 'undefined');
+ assert_greater_than(media_section.search(
+ /^application \d+ UDP\/DTLS\/SCTP webrtc-datachannel\r\n/), -1);
+ assert_greater_than(media_section.search(/\r\na=sctp-port:\d+\r\n/), -1);
+ assert_greater_than(media_section.search(/\r\na=mid:/), -1);
+}, 'Generated Datachannel SDP uses correct SCTP offer syntax');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/protocol/sdes-dont-dont-dont.html b/testing/web-platform/tests/webrtc/protocol/sdes-dont-dont-dont.html
new file mode 100644
index 0000000000..e938c84c8b
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/protocol/sdes-dont-dont-dont.html
@@ -0,0 +1,55 @@
+<!doctype html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<title>RTCPeerConnection MUST NOT support SDES</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';
+
+// Test support for
+// https://www.rfc-editor.org/rfc/rfc8826#section-4.3.1
+
+const sdp = `v=0
+o=- 0 3 IN IP4 127.0.0.1
+s=-
+t=0 0
+m=video 9 UDP/TLS/RTP/SAVPF 100
+c=IN IP4 0.0.0.0
+a=rtcp-mux
+a=sendonly
+a=mid:video
+a=rtpmap:100 VP8/90000
+a=fmtp:100 max-fr=30;max-fs=3600
+a=crypto:0 AES_CM_128_HMAC_SHA1_80 inline:2nra27hTUb9ilyn2rEkBEQN9WOFts26F/jvofasw
+a=ice-ufrag:ETEn
+a=ice-pwd:OtSK0WpNtpUjkY4+86js7Z/l
+`;
+
+// Negative test for Chrome legacy behavior.
+promise_test(async t => {
+ const sdes_constraint = {'mandatory': {'DtlsSrtpKeyAgreement': false}};
+ const pc = new RTCPeerConnection(null, sdes_constraint);
+ t.add_cleanup(() => pc.close());
+
+ pc.addTransceiver('audio');
+ const offer = await pc.createOffer();
+ assert_false(offer.sdp.includes('\na=crypto:'));
+}, 'does not create offers with SDES');
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ try {
+ await pc.setRemoteDescription({type: 'offer', sdp});
+ assert_unreached("Must not accept SDP without fingerprint");
+ } catch (e) {
+ // TODO: which error is correct? See
+ // https://github.com/w3c/webrtc-pc/issues/2672
+ assert_true(['OperationError', 'InvalidAccessError'].includes(e.name));
+ }
+}, 'rejects a remote offer that only includes SDES and no DTLS fingerprint');
+</script>
diff --git a/testing/web-platform/tests/webrtc/protocol/simulcast-answer.html b/testing/web-platform/tests/webrtc/protocol/simulcast-answer.html
new file mode 100644
index 0000000000..5e19bc08ff
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/protocol/simulcast-answer.html
@@ -0,0 +1,101 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerConnection Simulcast Answer</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+'use strict';
+
+const offer_sdp = `v=0
+o=- 3840232462471583827 2 IN IP4 127.0.0.1
+s=-
+t=0 0
+a=group:BUNDLE 0
+a=msid-semantic: WMS
+m=video 9 UDP/TLS/RTP/SAVPF 96
+c=IN IP4 0.0.0.0
+a=rtcp:9 IN IP4 0.0.0.0
+a=ice-ufrag:Li6+
+a=ice-pwd:3C05CTZBRQVmGCAq7hVasHlT
+a=ice-options:trickle
+a=fingerprint:sha-256 5B:D3:8E:66:0E:7D:D3:F3:8E:E6:80:28:19:FC:55:AD:58:5D:B9:3D:A8:DE:45:4A:E7:87:02:F8:3C:0B:3B:B3
+a=setup:actpass
+a=mid:0
+a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id
+a=recvonly
+a=rtcp-mux
+a=rtpmap:96 VP8/90000
+a=rtcp-fb:96 goog-remb
+a=rtcp-fb:96 transport-cc
+a=rtcp-fb:96 ccm fir
+a=rid:foo recv
+a=rid:bar recv
+a=rid:baz recv
+a=simulcast:recv foo;bar;baz
+`;
+// Tests for the construction of answers with simulcast according to:
+// draft-ietf-mmusic-sdp-simulcast-13
+// draft-ietf-mmusic-rid-15
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const expected_rids = ['foo', 'bar', 'baz'];
+
+ await pc.setRemoteDescription({type: 'offer', sdp: offer_sdp});
+ const transceiver = pc.getTransceivers()[0];
+ // The created transceiver should be in "recvonly" state. Allow it to send.
+ transceiver.direction = 'sendonly';
+ const answer = await pc.createAnswer();
+ const answer_lines = answer.sdp.split('\r\n');
+ // Check for a RID line for each layer.
+ for (const rid of expected_rids) {
+ const result = answer_lines.find(line => line.startsWith(`a=rid:${rid}`));
+ assert_not_equals(result, undefined, `RID attribute for '${rid}' missing.`);
+ }
+
+ // Check for simulcast attribute with send direction and all RIDs.
+ const result = answer_lines.find(
+ line => line.startsWith(`a=simulcast:send ${expected_rids.join(';')}`));
+ assert_not_equals(result, undefined, 'Could not find simulcast attribute.');
+}, 'createAnswer() with multiple send encodings should create simulcast answer');
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const expected_rids = ['foo', 'bar', 'baz'];
+
+ // Try to disable the `bar` encoding in a=simulcast by prefixing it with the
+ // `~` character.
+ await pc.setRemoteDescription({type: 'offer', sdp: offer_sdp.replace(/(a=simulcast:.*)bar/, '$1~bar')});
+ const transceiver = pc.getTransceivers()[0];
+ transceiver.direction = 'sendonly';
+ await pc.setLocalDescription();
+
+ const parameters = pc.getSenders()[0].getParameters();
+ const barEncoding = parameters.encodings.find(encoding => encoding.rid === 'bar');
+ assert_not_equals(barEncoding, undefined);
+ assert_not_equals(barEncoding.active, false);
+}, 'Using the ~rid SDP syntax in a remote offer does not control the local encodings active flag');
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const expected_rids = ['foo', 'bar', 'baz'];
+
+ await pc.setRemoteDescription({type: 'offer', sdp: offer_sdp});
+ const transceiver = pc.getTransceivers()[0];
+ transceiver.direction = 'sendonly';
+ await pc.setLocalDescription();
+
+ // Disabling the encoding should not change the rid to ~rid.
+ const parameters = pc.getSenders()[0].getParameters();
+ parameters.encodings.forEach(e => e.active = false);
+ await pc.getSenders()[0].setParameters(parameters);
+ const offer = await pc.createOffer();
+
+ const offer_lines = offer.sdp.split('\r\n');
+ const result = offer_lines.find(
+ line => line.startsWith(`a=simulcast:send ${expected_rids.join(';')}`));
+ assert_not_equals(result, undefined, 'Could not find simulcast attribute.');
+}, 'Disabling encodings locally does not change the SDP');
+</script>
diff --git a/testing/web-platform/tests/webrtc/protocol/simulcast-offer.html b/testing/web-platform/tests/webrtc/protocol/simulcast-offer.html
new file mode 100644
index 0000000000..77ae7f9510
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/protocol/simulcast-offer.html
@@ -0,0 +1,33 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerConnection Simulcast Offer</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+'use strict';
+
+// Tests for the construction of offers with simulcast according to:
+// draft-ietf-mmusic-sdp-simulcast-13
+// draft-ietf-mmusic-rid-15
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+ const expected_rids = ['foo', 'bar', 'baz'];
+ pc.addTransceiver('video', {
+ sendEncodings: expected_rids.map(rid => ({rid}))
+ });
+
+ const offer = await pc.createOffer();
+ let offer_lines = offer.sdp.split('\r\n');
+ // Check for a RID line for each layer.
+ for (const rid of expected_rids) {
+ let result = offer_lines.find(line => line.startsWith(`a=rid:${rid}`));
+ assert_not_equals(result, undefined, `RID attribute for '${rid}' missing.`);
+ }
+
+ // Check for simulcast attribute with send direction and all RIDs.
+ let result = offer_lines.find(
+ line => line.startsWith(`a=simulcast:send ${expected_rids.join(';')}`));
+ assert_not_equals(result, undefined, "Could not find simulcast attribute.");
+}, 'createOffer() with multiple send encodings should create simulcast offer');
+</script>
diff --git a/testing/web-platform/tests/webrtc/protocol/split.https.html b/testing/web-platform/tests/webrtc/protocol/split.https.html
new file mode 100644
index 0000000000..3fc3bda2a5
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/protocol/split.https.html
@@ -0,0 +1,98 @@
+<!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>
diff --git a/testing/web-platform/tests/webrtc/protocol/unknown-mediatypes.html b/testing/web-platform/tests/webrtc/protocol/unknown-mediatypes.html
new file mode 100644
index 0000000000..f5176d1c87
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/protocol/unknown-mediatypes.html
@@ -0,0 +1,34 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerconnection SDP handling of unknown media types</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../RTCPeerConnection-helper.js"></script>
+<script>
+'use strict';
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+
+ const stream = await getNoiseStream({audio: true});
+ pc1.addTrack(stream.getTracks()[0], stream);
+ const offer = await pc1.createOffer();
+ await pc2.setRemoteDescription({
+ type: 'offer',
+ sdp: offer.sdp
+ .replace('m=audio ', 'm=unicorns ')
+ });
+ await pc1.setLocalDescription(offer);
+ const answer = await pc2.createAnswer();
+ await pc2.setLocalDescription(answer);
+ // Do not attempt to call pc1.setRemoteDescription.
+
+ const [preamble, media_section, postamble] = answer.sdp.split('\r\nm=');
+ assert_true(typeof(postamble) === 'undefined');
+ assert_greater_than(media_section.search(
+ /^unicorns 0/), -1);
+}, 'Unknown media types are rejected with the port set to 0');
+</script>
diff --git a/testing/web-platform/tests/webrtc/protocol/video-codecs.https.html b/testing/web-platform/tests/webrtc/protocol/video-codecs.https.html
new file mode 100644
index 0000000000..4ce0618bca
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/protocol/video-codecs.https.html
@@ -0,0 +1,95 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerConnection.prototype.createOffer</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../RTCPeerConnection-helper.js"></script>
+<script>
+'use strict';
+
+/*
+ * Chromium note: this requires build bots with H264 support. See
+ * https://bugs.chromium.org/p/chromium/issues/detail?id=840659
+ * for details on how to enable support.
+ */
+// Tests for conformance to RFC 7742,
+// "WebRTC Video Processing and Codec Requirements"
+// The document was formerly known as draft-ietf-rtcweb-video-codecs.
+//
+// This tests that the browser is a WebRTC Browser as defined there.
+
+// TODO: Section 3.2: screen capture video MUST be prepared
+// to handle resolution changes.
+
+// TODO: Section 4: MUST support generating CVO (orientation)
+
+// Section 5: Browsers MUST implement VP8 and H.264 Constrained Baseline
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ const offer = await generateVideoReceiveOnlyOffer(pc);
+ let video_section_found = false;
+ for (let section of offer.sdp.split(/\r\nm=/)) {
+ if (section.search('video') != 0) {
+ continue;
+ }
+ video_section_found = true;
+ // RTPMAP lines have the format a=rtpmap:<pt> <codec>/<clock rate>
+ let rtpmap_regex = /\r\na=rtpmap:(\d+) (\S+)\/\d+\r\n/g;
+ let match = rtpmap_regex.exec(offer.sdp);
+ let payload_type_map = new Array();
+ while (match) {
+ payload_type_map[match[1]] = match[2];
+ match = rtpmap_regex.exec(offer.sdp);
+ }
+ assert_true(payload_type_map.indexOf('VP8') > -1,
+ 'VP8 is supported');
+ assert_true(payload_type_map.indexOf('H264') > -1,
+ 'H.264 is supported');
+ // TODO: Verify that one of the H.264 PTs supports constrained baseline
+ }
+ assert_true(video_section_found);
+}, 'H.264 and VP8 should be supported in initial offer');
+
+async function negotiateParameters() {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ let [track, streams] = await getTrackFromUserMedia('video');
+ const sender = pc1.addTrack(track);
+ await exchangeOfferAnswer(pc1, pc2);
+ return sender.getParameters();
+}
+
+function parseFmtp(fmtp) {
+ const params = fmtp.split(';');
+ return params.map(param => param.split('='));
+}
+promise_test(async t => {
+ const params = await negotiateParameters();
+ assert_true(!!params.codecs.find(codec => codec.mimeType === 'video/H264'));
+ assert_true(!!params.codecs.find(codec => codec.mimeType === 'video/VP8'));
+}, 'H.264 and VP8 should be negotiated after handshake');
+
+// TODO: Section 6: Recipients MUST be able to decode 320x240@20 fps
+// TODO: Section 6.1: VP8 MUST support RFC 7741 payload formats
+// TODO: Section 6.1: VP8 MUST respect max-fr/max-fs
+// TODO: Section 6.1: VP8 MUST encode and decode square pixels
+// TODO: Section 6.2: H.264 MUST support RFC 6184 payload formats
+// TODO: Section 6.2: MUST support Constrained Baseline level 1.2
+// TODO: Section 6.2: SHOULD support Constrained High level 1.3
+// TODO: Section 6.2: MUST support packetization mode 1.
+promise_test(async t => {
+ const params = await negotiateParameters();
+ const h264 = params.codecs.filter(codec => codec.mimeType === 'video/H264');
+ h264.map(codec => {
+ const codec_params = parseFmtp(codec.sdpFmtpLine);
+ assert_true(!!codec_params.find(x => x[0] === 'profile-level-id'));
+ })
+}, 'All H.264 codecs MUST include profile-level-id');
+
+// TODO: Section 6.2: SHOULD interpret max-mbps, max-smbps, max-fs et al
+// TODO: Section 6.2: MUST NOT include sprop-parameter-sets
+// TODO: Section 6.2: MUST support SEI "filler payload"
+// TODO: Section 6.2: MUST support SEI "full frame freeze"
+// TODO: Section 6.2: MUST be prepared to receive User Data messages
+// TODO: Section 6.2: MUST encode and decode square pixels unless signaled
+</script>
diff --git a/testing/web-platform/tests/webrtc/protocol/vp8-fmtp.html b/testing/web-platform/tests/webrtc/protocol/vp8-fmtp.html
new file mode 100644
index 0000000000..16ea635949
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/protocol/vp8-fmtp.html
@@ -0,0 +1,44 @@
+<!doctype html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<title>RTCPeerConnection Failed State</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../RTCPeerConnection-helper.js"></script>
+<script>
+'use strict';
+
+// Test support for
+// https://tools.ietf.org/html/rfc7741#section-6.1
+
+const sdp = `v=0
+o=- 0 3 IN IP4 127.0.0.1
+s=-
+t=0 0
+a=fingerprint:sha-256 A7:24:72:CA:6E:02:55:39:BA:66:DF:6E:CC:4C:D8:B0:1A:BF:1A:56:65:7D:F4:03:AD:7E:77:43:2A:29:EC:93
+m=video 9 UDP/TLS/RTP/SAVPF 100
+c=IN IP4 0.0.0.0
+a=rtcp-mux
+a=sendonly
+a=mid:video
+a=rtpmap:100 VP8/90000
+a=fmtp:100 max-fr=30;max-fs=3600
+a=setup:actpass
+a=ice-ufrag:ETEn
+a=ice-pwd:OtSK0WpNtpUjkY4+86js7Z/l
+`;
+
+promise_test(async t => {
+ const pc = new RTCPeerConnection();
+ t.add_cleanup(() => pc.close());
+
+ await pc.setRemoteDescription({type: 'offer', sdp});
+ await pc.setLocalDescription();
+ const receiver = pc.getReceivers()[0];
+ const parameters = receiver.getParameters();
+ const {sdpFmtpLine} = parameters.codecs[0];
+ assert_true(!!sdpFmtpLine);
+ assert_true(sdpFmtpLine.split(';').includes('max-fr=30'));
+ assert_true(sdpFmtpLine.split(';').includes('max-fs=3600'));
+}, 'setRemoteDescription parses max-fr and max-fs fmtp parameters');
+</script>
diff --git a/testing/web-platform/tests/webrtc/receiver-track-live.https.html b/testing/web-platform/tests/webrtc/receiver-track-live.https.html
new file mode 100644
index 0000000000..34569297a6
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/receiver-track-live.https.html
@@ -0,0 +1,72 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Remote tracks should not get ended except for stop/close</title>
+<script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src=/resources/testdriver.js></script>
+<script src=/resources/testdriver-vendor.js></script>
+<script src='../mediacapture-streams/permission-helper.js'></script>
+ <script src="RTCPeerConnection-helper.js"></script>
+</head>
+<body>
+ <video id="video" controls autoplay playsinline></video>
+ <script>
+ let pc1, pc2;
+ let localTrack, remoteTrack;
+ promise_test(async (test) => {
+ await setMediaPermission("granted", ["microphone"]);
+ const localStream = await navigator.mediaDevices.getUserMedia({audio: true});
+ localTrack = localStream.getAudioTracks()[0];
+
+ pc1 = new RTCPeerConnection();
+ pc1.addTrack(localTrack, localStream);
+ pc2 = new RTCPeerConnection();
+
+ let trackPromise = new Promise(resolve => {
+ pc2.ontrack = e => resolve(e.track);
+ });
+
+ exchangeIceCandidates(pc1, pc2);
+ await exchangeOfferAnswer(pc1, pc2);
+
+ remoteTrack = await trackPromise;
+ video.srcObject = new MediaStream([remoteTrack]);
+ await video.play();
+ }, "Setup audio call");
+
+ promise_test(async (test) => {
+ pc1.getTransceivers()[0].direction = "inactive";
+
+ let offer = await pc1.createOffer();
+ await pc1.setLocalDescription(offer);
+
+ // Let's remove ssrc lines
+ let sdpLines = offer.sdp.split("\r\n");
+ offer.sdp = sdpLines.filter(line => line && !line.startsWith("a=ssrc")).join("\r\n") + "\r\n";
+
+ await pc2.setRemoteDescription(offer);
+ let answer = await pc2.createAnswer();
+ await pc2.setLocalDescription(answer);
+ await pc1.setRemoteDescription(answer);
+
+ assert_equals(remoteTrack.readyState, "live");
+ }, "Inactivate the audio transceiver");
+
+ promise_test(async (test) => {
+ pc1.getTransceivers()[0].direction = "sendonly";
+
+ await exchangeOfferAnswer(pc1, pc2);
+
+ assert_equals(remoteTrack.readyState, "live");
+ }, "Reactivate the audio transceiver");
+
+ promise_test(async (test) => {
+ pc1.close();
+ pc2.close();
+ localTrack.stop();
+ }, "Clean-up");
+ </script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/webrtc/recvonly-transceiver-can-become-sendrecv.https.html b/testing/web-platform/tests/webrtc/recvonly-transceiver-can-become-sendrecv.https.html
new file mode 100644
index 0000000000..30bbec4f9f
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/recvonly-transceiver-can-become-sendrecv.https.html
@@ -0,0 +1,50 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+'use strict';
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const audioTransceiver = pc1.addTransceiver('audio', {direction:'recvonly'});
+
+ await pc1.setLocalDescription();
+ await pc2.setRemoteDescription(pc1.localDescription);
+ await pc2.setLocalDescription();
+ await pc1.setRemoteDescription(pc2.localDescription);
+
+ audioTransceiver.direction = 'sendrecv';
+
+ await pc1.setLocalDescription();
+ await pc2.setRemoteDescription(pc1.localDescription);
+ await pc2.setLocalDescription();
+ await pc1.setRemoteDescription(pc2.localDescription);
+}, '[audio] recvonly transceiver can become sendrecv');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const videoTransceiver = pc1.addTransceiver('video', {direction:'recvonly'});
+
+ await pc1.setLocalDescription();
+ await pc2.setRemoteDescription(pc1.localDescription);
+ await pc2.setLocalDescription();
+ await pc1.setRemoteDescription(pc2.localDescription);
+
+ videoTransceiver.direction = 'sendrecv';
+
+ await pc1.setLocalDescription();
+ await pc2.setRemoteDescription(pc1.localDescription);
+ await pc2.setLocalDescription();
+ await pc1.setRemoteDescription(pc2.localDescription);
+}, '[video] recvonly transceiver can become sendrecv');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/resources/RTCCertificate-postMessage-iframe.html b/testing/web-platform/tests/webrtc/resources/RTCCertificate-postMessage-iframe.html
new file mode 100644
index 0000000000..9e52ba0c88
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/resources/RTCCertificate-postMessage-iframe.html
@@ -0,0 +1,9 @@
+<!doctype html>
+<script>
+window.onmessage = async (event) => {
+ let certificate = event.data;
+ if (!certificate)
+ certificate = await RTCPeerConnection.generateCertificate({ name: 'ECDSA', namedCurve: 'P-256'});
+ event.source.postMessage(certificate, "*");
+}
+</script>
diff --git a/testing/web-platform/tests/webrtc/simplecall-no-ssrcs.https.html b/testing/web-platform/tests/webrtc/simplecall-no-ssrcs.https.html
new file mode 100644
index 0000000000..f2e2084623
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/simplecall-no-ssrcs.https.html
@@ -0,0 +1,118 @@
+<!doctype html>
+<html>
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+ <title>RTCPeerConnection Connection Test</title>
+ <script src="RTCPeerConnection-helper.js"></script>
+</head>
+<body>
+ <div id="log"></div>
+ <div>
+ <video id="local-view" muted autoplay="autoplay"></video>
+ <video id="remote-view" muted autoplay="autoplay"/>
+ </video>
+ </div>
+
+ <!-- These files are in place when executing on W3C. -->
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script type="text/javascript">
+ var test = async_test('Can set up a basic WebRTC call without announcing ssrcs.');
+
+ var gFirstConnection = null;
+ var gSecondConnection = null;
+
+ // if the remote video gets video data that implies the negotiation
+ // as well as the ICE and DTLS connection are up.
+ document.getElementById('remote-view')
+ .addEventListener('loadedmetadata', function() {
+ // Call negotiated: done.
+ test.done();
+ });
+
+ function getNoiseStreamOkCallback(localStream) {
+ gFirstConnection = new RTCPeerConnection(null);
+ test.add_cleanup(() => gFirstConnection.close());
+ gFirstConnection.onicecandidate = onIceCandidateToFirst;
+ localStream.getTracks().forEach(function(track) {
+ gFirstConnection.addTrack(track, localStream);
+ });
+ gFirstConnection.createOffer().then(onOfferCreated, failed('createOffer'));
+
+ var videoTag = document.getElementById('local-view');
+ videoTag.srcObject = localStream;
+ };
+
+ var onOfferCreated = test.step_func(function(offer) {
+ gFirstConnection.setLocalDescription(offer);
+
+ // remove all a=ssrc: lines and the (obsolete) msid-semantic line.
+ var sdp = offer.sdp.replace(/^a=ssrc:.*$\r\n/gm, '')
+ .replace(/^a=msid-semantic.*$\r\n/gm, '');
+
+ // This would normally go across the application's signaling solution.
+ // In our case, the "signaling" is to call this function.
+ receiveCall(sdp);
+ });
+
+ function receiveCall(offerSdp) {
+ gSecondConnection = new RTCPeerConnection(null);
+ test.add_cleanup(() => gSecondConnection.close());
+ gSecondConnection.onicecandidate = onIceCandidateToSecond;
+ gSecondConnection.ontrack = onRemoteTrack;
+
+ var parsedOffer = new RTCSessionDescription({ type: 'offer',
+ sdp: offerSdp });
+ gSecondConnection.setRemoteDescription(parsedOffer);
+
+ gSecondConnection.createAnswer().then(onAnswerCreated,
+ failed('createAnswer'));
+ };
+
+ var onAnswerCreated = test.step_func(function(answer) {
+ gSecondConnection.setLocalDescription(answer);
+
+ // remove all a=ssrc: lines, the msid-semantic line and any a=msid:.
+ var sdp = answer.sdp.replace(/^a=ssrc:.*$\r\n/gm, '')
+ .replace(/^a=msid-semantic.*$\r\n/gm, '')
+ .replace(/^a=msid:.*$\r\n/gm, '');
+
+ // Similarly, this would go over the application's signaling solution.
+ handleAnswer(sdp);
+ });
+
+ function handleAnswer(answerSdp) {
+ var parsedAnswer = new RTCSessionDescription({ type: 'answer',
+ sdp: answerSdp });
+ gFirstConnection.setRemoteDescription(parsedAnswer);
+ };
+
+ var onIceCandidateToFirst = test.step_func(function(event) {
+ gSecondConnection.addIceCandidate(event.candidate);
+ });
+
+ var onIceCandidateToSecond = test.step_func(function(event) {
+ gFirstConnection.addIceCandidate(event.candidate);
+ });
+
+ var onRemoteTrack = test.step_func(function(event) {
+ var videoTag = document.getElementById('remote-view');
+ if (!videoTag.srcObject) {
+ videoTag.srcObject = event.streams[0];
+ }
+ });
+
+ // Returns a suitable error callback.
+ function failed(function_name) {
+ return test.unreached_func('WebRTC called error callback for ' + function_name);
+ }
+
+ // This function starts the test.
+ test.step(function() {
+ getNoiseStream({ video: true, audio: true })
+ .then(test.step_func(getNoiseStreamOkCallback), failed('getNoiseStream'));
+ });
+</script>
+
+</body>
+</html>
diff --git a/testing/web-platform/tests/webrtc/simplecall.https.html b/testing/web-platform/tests/webrtc/simplecall.https.html
new file mode 100644
index 0000000000..dbf6b9a508
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/simplecall.https.html
@@ -0,0 +1,109 @@
+<!doctype html>
+<html>
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+ <title>RTCPeerConnection Connection Test</title>
+ <script src="RTCPeerConnection-helper.js"></script>
+</head>
+<body>
+ <div id="log"></div>
+ <div>
+ <video id="local-view" muted autoplay="autoplay"></video>
+ <video id="remote-view" muted autoplay="autoplay"/>
+ </video>
+ </div>
+
+ <!-- These files are in place when executing on W3C. -->
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script type="text/javascript">
+ var test = async_test('Can set up a basic WebRTC call.');
+
+ var gFirstConnection = null;
+ var gSecondConnection = null;
+
+ // if the remote video gets video data that implies the negotiation
+ // as well as the ICE and DTLS connection are up.
+ document.getElementById('remote-view')
+ .addEventListener('loadedmetadata', function() {
+ // Call negotiated: done.
+ test.done();
+ });
+
+ function getNoiseStreamOkCallback(localStream) {
+ gFirstConnection = new RTCPeerConnection(null);
+ test.add_cleanup(() => gFirstConnection.close());
+ gFirstConnection.onicecandidate = onIceCandidateToFirst;
+ localStream.getTracks().forEach(function(track) {
+ gFirstConnection.addTrack(track, localStream);
+ });
+ gFirstConnection.createOffer().then(onOfferCreated, failed('createOffer'));
+
+ var videoTag = document.getElementById('local-view');
+ videoTag.srcObject = localStream;
+ };
+
+ var onOfferCreated = test.step_func(function(offer) {
+ gFirstConnection.setLocalDescription(offer);
+
+ // This would normally go across the application's signaling solution.
+ // In our case, the "signaling" is to call this function.
+ receiveCall(offer.sdp);
+ });
+
+ function receiveCall(offerSdp) {
+ gSecondConnection = new RTCPeerConnection(null);
+ test.add_cleanup(() => gSecondConnection.close());
+ gSecondConnection.onicecandidate = onIceCandidateToSecond;
+ gSecondConnection.ontrack = onRemoteTrack;
+
+ var parsedOffer = new RTCSessionDescription({ type: 'offer',
+ sdp: offerSdp });
+ gSecondConnection.setRemoteDescription(parsedOffer);
+
+ gSecondConnection.createAnswer().then(onAnswerCreated,
+ failed('createAnswer'));
+ };
+
+ var onAnswerCreated = test.step_func(function(answer) {
+ gSecondConnection.setLocalDescription(answer);
+
+ // Similarly, this would go over the application's signaling solution.
+ handleAnswer(answer.sdp);
+ });
+
+ function handleAnswer(answerSdp) {
+ var parsedAnswer = new RTCSessionDescription({ type: 'answer',
+ sdp: answerSdp });
+ gFirstConnection.setRemoteDescription(parsedAnswer);
+ };
+
+ var onIceCandidateToFirst = test.step_func(function(event) {
+ gSecondConnection.addIceCandidate(event.candidate);
+ });
+
+ var onIceCandidateToSecond = test.step_func(function(event) {
+ gFirstConnection.addIceCandidate(event.candidate);
+ });
+
+ var onRemoteTrack = test.step_func(function(event) {
+ var videoTag = document.getElementById('remote-view');
+ if (!videoTag.srcObject) {
+ videoTag.srcObject = event.streams[0];
+ }
+ });
+
+ // Returns a suitable error callback.
+ function failed(function_name) {
+ return test.unreached_func('WebRTC called error callback for ' + function_name);
+ }
+
+ // This function starts the test.
+ test.step(function() {
+ getNoiseStream({ video: true, audio: true })
+ .then(test.step_func(getNoiseStreamOkCallback), failed('getNoiseStream'));
+ });
+</script>
+
+</body>
+</html>
diff --git a/testing/web-platform/tests/webrtc/simulcast/basic.https.html b/testing/web-platform/tests/webrtc/simulcast/basic.https.html
new file mode 100644
index 0000000000..f7b9def762
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/simulcast/basic.https.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerConnection Simulcast Tests</title>
+<meta name="timeout" content="long">
+<script src="../third_party/sdp/sdp.js"></script>
+<script src="simulcast.js"></script>
+<script src="../RTCPeerConnection-helper.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="../../mediacapture-streams/permission-helper.js"></script>
+<script>
+promise_test(async t => {
+ const rids = [0, 1];
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ return negotiateSimulcastAndWaitForVideo(t, rids, pc1, pc2);
+}, 'Basic simulcast setup with two spatial layers');
+</script>
diff --git a/testing/web-platform/tests/webrtc/simulcast/getStats.https.html b/testing/web-platform/tests/webrtc/simulcast/getStats.https.html
new file mode 100644
index 0000000000..b5a9e6eb28
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/simulcast/getStats.https.html
@@ -0,0 +1,34 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerConnection Simulcast Tests - getStats</title>
+<meta name="timeout" content="long">
+<script src="../third_party/sdp/sdp.js"></script>
+<script src="simulcast.js"></script>
+<script src="../RTCPeerConnection-helper.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="../../mediacapture-streams/permission-helper.js"></script>
+<script>
+promise_test(async t => {
+ const rids = [0, 1, 2];
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ await negotiateSimulcastAndWaitForVideo(t, rids, pc1, pc2);
+
+ const outboundStats = [];
+ const senderStats = await pc1.getSenders()[0].getStats();
+ senderStats.forEach(stat => {
+ if (stat.type === 'outbound-rtp') {
+ outboundStats.push(stat);
+ }
+ });
+ assert_equals(outboundStats.length, 3, "getStats result should contain three layers");
+ const statsRids = outboundStats.map(stat => parseInt(stat.rid, 10));
+ assert_array_equals(rids, statsRids.sort(), "getStats result should match the rids provided");
+}, 'Simulcast getStats results');
+</script>
diff --git a/testing/web-platform/tests/webrtc/simulcast/h264.https.html b/testing/web-platform/tests/webrtc/simulcast/h264.https.html
new file mode 100644
index 0000000000..038449aa6e
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/simulcast/h264.https.html
@@ -0,0 +1,31 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerConnection Simulcast Tests</title>
+<meta name="timeout" content="long">
+<script src="../third_party/sdp/sdp.js"></script>
+<script src="simulcast.js"></script>
+<script src="../RTCPeerConnection-helper.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="../../mediacapture-streams/permission-helper.js"></script>
+<script>
+/*
+ * Chromium note: this requires build bots with H264 support. See
+ * https://bugs.chromium.org/p/chromium/issues/detail?id=840659
+ * for details on how to enable support.
+ */
+promise_test(async t => {
+ assert_implements('getCapabilities' in RTCRtpSender, 'RTCRtpSender.getCapabilities not supported');
+ assert_implements(RTCRtpSender.getCapabilities('video').codecs.find(c => c.mimeType === 'video/H264'), 'H264 not supported');
+
+ const rids = [0, 1];
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ return negotiateSimulcastAndWaitForVideo(t, rids, pc1, pc2, {mimeType: 'video/H264'});
+}, 'H264 simulcast setup with two streams');
+</script>
diff --git a/testing/web-platform/tests/webrtc/simulcast/negotiation-encodings.https.html b/testing/web-platform/tests/webrtc/simulcast/negotiation-encodings.https.html
new file mode 100644
index 0000000000..c16e2674b0
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/simulcast/negotiation-encodings.https.html
@@ -0,0 +1,534 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerConnection Simulcast Tests - negotiation/encodings</title>
+<meta name="timeout" content="long">
+<script src="../third_party/sdp/sdp.js"></script>
+<script src="simulcast.js"></script>
+<script src="../RTCPeerConnection-helper.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="../../mediacapture-streams/permission-helper.js"></script>
+<script>
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const stream = await getNoiseStream({video: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const sender = pc1.addTrack(stream.getTracks()[0]);
+ // pc1 is unicast right now
+ pc2.addTrack(stream.getTracks()[0]);
+
+ await doOfferToRecvSimulcastAndAnswer(pc2, pc1, ["foo", "bar"]);
+ assert_equals(pc1.getTransceivers().length, 1);
+ const {encodings} = sender.getParameters();
+ const rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo", "bar"]);
+}, 'addTrack, then sRD(simulcast recv offer) results in simulcast');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const stream = await getNoiseStream({audio: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const sender = pc1.addTrack(stream.getTracks()[0]);
+ // pc1 is unicast right now
+ pc2.addTrack(stream.getTracks()[0]);
+
+ await doOfferToRecvSimulcastAndAnswer(pc2, pc1, ["foo", "bar"]);
+ assert_equals(pc1.getTransceivers().length, 1);
+ const {encodings} = sender.getParameters();
+ const rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, [undefined]);
+}, 'simulcast is not supported for audio');
+
+// We do not have a test case for sRD(offer) narrowing a simulcast envelope
+// from addTransceiver, since that transceiver cannot be paired up with a remote
+// offer m-section
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const {sender} = pc1.addTransceiver("video", {sendEncodings: [{rid: "foo"}, {rid: "bar"}]});
+ let {encodings} = sender.getParameters();
+
+ await doOfferToSendSimulcastAndAnswer(pc1, pc2, ["foo"]);
+
+ assert_equals(pc1.getTransceivers().length, 1);
+ encodings = sender.getParameters().encodings;
+ const rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo"]);
+ const scaleDownByValues = encodings.map(({scaleResolutionDownBy}) => scaleResolutionDownBy);
+ assert_array_equals(scaleDownByValues, [2]);
+}, 'sRD(recv simulcast answer) can narrow the simulcast envelope specified by addTransceiver');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const {sender} = pc1.addTransceiver("video", {sendEncodings: [{rid: "foo"}, {rid: "bar"}]});
+ let {encodings} = sender.getParameters();
+
+ await doOfferToSendSimulcastAndAnswer(pc1, pc2, ["foo", "bar"]);
+
+ assert_equals(pc1.getTransceivers().length, 1);
+ encodings = sender.getParameters().encodings;
+ let rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo", "bar"]);
+
+ await doOfferToSendSimulcastAndAnswer(pc1, pc2, ["foo"]);
+
+ assert_equals(pc1.getTransceivers().length, 1);
+ encodings = sender.getParameters().encodings;
+ rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo"]);
+ const scaleDownByValues = encodings.map(({scaleResolutionDownBy}) => scaleResolutionDownBy);
+ assert_array_equals(scaleDownByValues, [2]);
+}, 'sRD(recv simulcast answer) can narrow the simulcast envelope from a previous negotiation');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const {sender} = pc1.addTransceiver("video", {sendEncodings: [{rid: "foo"}, {rid: "bar"}]});
+ let {encodings} = sender.getParameters();
+
+ await doOfferToSendSimulcastAndAnswer(pc1, pc2, ["foo", "bar"]);
+
+ assert_equals(pc1.getTransceivers().length, 1);
+ encodings = sender.getParameters().encodings;
+ let rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo", "bar"]);
+
+ // doAnswerToSendSimulcast causes pc2 to barf unless we set the direction to
+ // sendrecv
+ pc2.getTransceivers()[0].direction = "sendrecv";
+ pc2.getTransceivers()[1].direction = "sendrecv";
+
+ await doOfferToRecvSimulcast(pc2, pc1, ["foo"]);
+ assert_equals(pc1.getTransceivers().length, 1);
+ encodings = sender.getParameters().encodings;
+ rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo", "bar"], "[[SendEncodings]] is not updated in have-remote-offer for reoffers");
+
+ await doAnswerToSendSimulcast(pc2, pc1);
+
+ assert_equals(pc1.getTransceivers().length, 1);
+ encodings = sender.getParameters().encodings;
+ rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo"]);
+ const scaleDownByValues = encodings.map(({scaleResolutionDownBy}) => scaleResolutionDownBy);
+ assert_array_equals(scaleDownByValues, [2]);
+}, 'sRD(simulcast offer) can narrow the simulcast envelope from a previous negotiation');
+
+// https://github.com/w3c/webrtc-pc/issues/2780
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const stream = await getNoiseStream({video: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const sender = pc1.addTrack(stream.getTracks()[0]);
+ pc2.addTrack(stream.getTracks()[0]);
+
+ await doOfferToRecvSimulcastAndAnswer(pc2, pc1, ["foo", "bar", "foo"]);
+ assert_equals(pc1.getTransceivers().length, 1);
+ let {encodings} = sender.getParameters();
+ let rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo", "bar"]);
+ assert_true(pc1.remoteDescription.sdp.includes("a=simulcast:recv foo;bar;foo"), "Duplicate rids should be present in offer");
+ assert_false(pc1.localDescription.sdp.includes("a=simulcast:send foo;bar;foo"), "Duplicate rids should not be present in answer");
+ assert_true(pc1.localDescription.sdp.includes("a=simulcast:send foo;bar"), "Answer should use the correct rids");
+ assert_equals(pc1.getTransceivers().length, 1);
+ encodings = sender.getParameters().encodings;
+ rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo", "bar"]);
+}, 'Duplicate rids in sRD(offer) are ignored');
+
+// https://github.com/w3c/webrtc-pc/issues/2769
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const stream = await getNoiseStream({video: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const sender = pc1.addTrack(stream.getTracks()[0]);
+ pc2.addTrack(stream.getTracks()[0]);
+
+ await doOfferToRecvSimulcastAndAnswer(pc2, pc1, ["foo,bar", "1,2"]);
+ assert_equals(pc1.getTransceivers().length, 1);
+ let {encodings} = sender.getParameters();
+ let rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo", "1"]);
+ assert_true(pc1.remoteDescription.sdp.includes("a=simulcast:recv foo,bar;1,2"), "Choices of rids should be present in offer");
+ assert_true(pc1.localDescription.sdp.includes("a=simulcast:send foo;1\r\n"), "Choices of rids should not be present in answer");
+ assert_equals(pc1.getTransceivers().length, 1);
+ encodings = sender.getParameters().encodings;
+ rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo", "1"]);
+}, 'Choices in rids in sRD(offer) are ignored');
+
+// https://github.com/w3c/webrtc-pc/issues/2764
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const stream = await getNoiseStream({video: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const sender = pc1.addTrack(stream.getTracks()[0]);
+ pc2.addTrack(stream.getTracks()[0]);
+
+ await doOfferToRecvSimulcast(pc2, pc1, ["foo", "bar"]);
+ assert_equals(pc1.getTransceivers().length, 1);
+ let {encodings} = sender.getParameters();
+ let rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo", "bar"]);
+
+ await pc1.setRemoteDescription({sdp: "", type: "rollback"});
+
+ assert_equals(pc1.getTransceivers().length, 1);
+ encodings = sender.getParameters().encodings;
+ rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, [undefined]);
+}, 'addTrack, then rollback of sRD(simulcast offer), brings us back to having a single encoding without a rid');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const stream = await getNoiseStream({video: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ pc2.addTrack(stream.getTracks()[0]);
+ await doOfferToRecvSimulcast(pc2, pc1, ["foo", "bar"]);
+ const sender = pc1.addTrack(stream.getTracks()[0]);
+ assert_equals(pc1.getTransceivers().length, 1);
+
+ assert_equals(pc1.getTransceivers().length, 1);
+ let {encodings} = sender.getParameters();
+ let rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo", "bar"]);
+
+ await pc1.setRemoteDescription({sdp: "", type: "rollback"});
+
+ assert_equals(pc1.getTransceivers().length, 1);
+ encodings = sender.getParameters().encodings;
+ rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, [undefined]);
+}, 'sRD(simulcast offer), addTrack, then rollback brings us back to having a single encoding');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const stream = await getNoiseStream({video: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const {sender} = pc1.addTransceiver(stream.getTracks()[0], {sendEncodings: [{rid: "foo"}, {rid: "bar"}]});
+
+ await doOfferToSendSimulcast(pc1, pc2);
+ await doAnswerToRecvSimulcast(pc1, pc2, ["bar", "foo"]);
+ assert_true(pc1.remoteDescription.sdp.includes("a=simulcast:recv bar;foo"), "Answer should have reordered rids");
+
+ assert_equals(pc1.getTransceivers().length, 1);
+ const {encodings} = sender.getParameters();
+ const rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo", "bar"]);
+}, 'Reordering of rids in sRD(answer) is ignored');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const stream = await getNoiseStream({video: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const {sender} = pc1.addTransceiver(stream.getTracks()[0], {sendEncodings: [{rid: "foo"}, {rid: "bar"}]});
+
+ await doOfferToSendSimulcastAndAnswer(pc1, pc2, ["foo", "bar"]);
+
+ assert_equals(pc1.getTransceivers().length, 1);
+ let {encodings} = sender.getParameters();
+ let rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo", "bar"]);
+
+ await doOfferToSendSimulcast(pc1, pc2);
+ await doAnswerToRecvSimulcast(pc1, pc2, ["bar", "foo"]);
+ assert_true(pc1.remoteDescription.sdp.includes("a=simulcast:recv bar;foo"), "Answer should have reordered rids");
+
+ assert_equals(pc1.getTransceivers().length, 1);
+ encodings = sender.getParameters().encodings;
+ rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo", "bar"]);
+}, 'Reordering of rids in sRD(reanswer) is ignored');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const stream = await getNoiseStream({video: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const {sender} = pc1.addTransceiver(stream.getTracks()[0], {sendEncodings: [{rid: "foo"}, {rid: "bar"}]});
+ await doOfferToSendSimulcastAndAnswer(pc1, pc2, ["foo", "bar"]);
+
+ assert_equals(pc1.getTransceivers().length, 1);
+ let {encodings} = sender.getParameters();
+ let rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo", "bar"]);
+
+ // doAnswerToSendSimulcast causes pc2 to barf unless we set the direction to
+ // sendrecv
+ pc2.getTransceivers()[0].direction = "sendrecv";
+ pc2.getTransceivers()[1].direction = "sendrecv";
+
+ await doOfferToRecvSimulcast(pc2, pc1, ["bar", "foo"]);
+ await doAnswerToSendSimulcast(pc2, pc1);
+ assert_true(pc1.remoteDescription.sdp.includes("a=simulcast:recv bar;foo"), "Reoffer should have reordered rids");
+
+ assert_equals(pc1.getTransceivers().length, 1);
+ encodings = sender.getParameters().encodings;
+ rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo", "bar"]);
+}, 'Reordering of rids in sRD(reoffer) is ignored');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const stream = await getNoiseStream({video: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const {sender} = pc1.addTransceiver(stream.getTracks()[0], {sendEncodings: [{rid: "foo"}, {rid: "bar"}]});
+ await doOfferToSendSimulcastAndAnswer(pc1, pc2, ["foo", "bar"]);
+
+ assert_equals(pc1.getTransceivers().length, 1);
+ let encodings = sender.getParameters().encodings;
+ let rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo", "bar"]);
+
+ // doAnswerToSendSimulcast causes pc2 to barf unless we set the direction to
+ // sendrecv
+ pc2.getTransceivers()[0].direction = "sendrecv";
+ pc2.getTransceivers()[1].direction = "sendrecv";
+
+ // Keep the second encoding!
+ await doOfferToRecvSimulcast(pc2, pc1, ["bar"]);
+ assert_equals(pc1.getTransceivers().length, 1);
+ encodings = sender.getParameters().encodings;
+ rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo", "bar"]);
+
+ await pc1.setRemoteDescription({sdp: "", type: "rollback"});
+
+ encodings = sender.getParameters().encodings;
+ rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo", "bar"]);
+}, 'Rollback of sRD(reoffer) with a single rid results in all previous encodings');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const {sender} = pc1.addTransceiver("video", {sendEncodings: [{rid: "foo"}, {rid: "bar"}]});
+ let {encodings} = sender.getParameters();
+
+ await doOfferToSendSimulcastAndAnswer(pc1, pc2, ["bar"]);
+
+ assert_equals(pc1.getTransceivers().length, 1);
+ encodings = sender.getParameters().encodings;
+ const rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["bar"]);
+ const scaleDownByValues = encodings.map(({scaleResolutionDownBy}) => scaleResolutionDownBy);
+ assert_array_equals(scaleDownByValues, [1]);
+}, 'sRD(recv simulcast answer) can narrow the simulcast envelope specified by addTransceiver by removing the first encoding');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const {sender} = pc1.addTransceiver("video", {sendEncodings: [{rid: "foo"}, {rid: "bar"}]});
+ let {encodings} = sender.getParameters();
+
+ await doOfferToSendSimulcastAndAnswer(pc1, pc2, ["foo", "bar"]);
+
+ assert_equals(pc1.getTransceivers().length, 1);
+ encodings = sender.getParameters().encodings;
+ let rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo", "bar"]);
+
+ await doOfferToSendSimulcastAndAnswer(pc1, pc2, ["bar"]);
+
+ assert_equals(pc1.getTransceivers().length, 1);
+ encodings = sender.getParameters().encodings;
+ rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["bar"]);
+ const scaleDownByValues = encodings.map(({scaleResolutionDownBy}) => scaleResolutionDownBy);
+ assert_array_equals(scaleDownByValues, [1]);
+}, 'sRD(recv simulcast answer) can narrow the simulcast envelope from a previous negotiation by removing the first encoding');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const {sender} = pc1.addTransceiver("video", {sendEncodings: [{rid: "foo"}, {rid: "bar"}]});
+ let {encodings} = sender.getParameters();
+
+ await doOfferToSendSimulcastAndAnswer(pc1, pc2, ["foo", "bar"]);
+
+ assert_equals(pc1.getTransceivers().length, 1);
+ encodings = sender.getParameters().encodings;
+ let rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo", "bar"]);
+
+ // doAnswerToSendSimulcast causes pc2 to barf unless we set the direction to
+ // sendrecv
+ pc2.getTransceivers()[0].direction = "sendrecv";
+ pc2.getTransceivers()[1].direction = "sendrecv";
+
+ await doOfferToRecvSimulcast(pc2, pc1, ["bar"]);
+ assert_equals(pc1.getTransceivers().length, 1);
+ encodings = sender.getParameters().encodings;
+ rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo", "bar"], "[[SendEncodings]] is not updated in have-remote-offer for reoffers");
+
+ await doAnswerToSendSimulcast(pc2, pc1);
+
+ assert_equals(pc1.getTransceivers().length, 1);
+ encodings = sender.getParameters().encodings;
+ rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["bar"]);
+ const scaleDownByValues = encodings.map(({scaleResolutionDownBy}) => scaleResolutionDownBy);
+ assert_array_equals(scaleDownByValues, [1]);
+}, 'sRD(simulcast offer) can narrow the simulcast envelope from a previous negotiation by removing the first encoding');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const {sender} = pc1.addTransceiver("video", {sendEncodings: [{rid: "foo"}, {rid: "bar"}]});
+ let {encodings} = sender.getParameters();
+
+ await doOfferToSendSimulcastAndAnswer(pc1, pc2, ["foo", "bar"]);
+
+ assert_equals(pc1.getTransceivers().length, 1);
+ encodings = sender.getParameters().encodings;
+ let rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo", "bar"]);
+
+ pc1.getTransceivers()[0].direction = "inactive";
+ await doOfferToSendSimulcastAndAnswer(pc1, pc2, ["foo", "bar"]);
+ assert_equals(pc1.getTransceivers().length, 1);
+ encodings = sender.getParameters().encodings;
+ rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo", "bar"]);
+}, 'sender renegotiation to inactive does not disable simulcast');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const {sender} = pc1.addTransceiver("video", {sendEncodings: [{rid: "foo"}, {rid: "bar"}]});
+ let {encodings} = sender.getParameters();
+
+ await doOfferToSendSimulcastAndAnswer(pc1, pc2, ["foo", "bar"]);
+
+ assert_equals(pc1.getTransceivers().length, 1);
+ encodings = sender.getParameters().encodings;
+ let rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo", "bar"]);
+
+ pc1.getTransceivers()[0].direction = "recvonly";
+ await doOfferToSendSimulcastAndAnswer(pc1, pc2, ["foo", "bar"]);
+ assert_equals(pc1.getTransceivers().length, 1);
+ encodings = sender.getParameters().encodings;
+ rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo", "bar"]);
+}, 'sender renegotiation to recvonly does not disable simulcast');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const {sender} = pc1.addTransceiver("video", {sendEncodings: [{rid: "foo"}, {rid: "bar"}]});
+ let {encodings} = sender.getParameters();
+
+ await doOfferToSendSimulcastAndAnswer(pc1, pc2, ["foo", "bar"]);
+
+ assert_equals(pc1.getTransceivers().length, 1);
+ encodings = sender.getParameters().encodings;
+ let rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo", "bar"]);
+
+ pc2.getTransceivers()[0].direction = "inactive";
+ pc2.getTransceivers()[1].direction = "inactive";
+ await doOfferToSendSimulcastAndAnswer(pc1, pc2, ["foo", "bar"]);
+ assert_equals(pc1.getTransceivers().length, 1);
+ encodings = sender.getParameters().encodings;
+ rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo", "bar"]);
+}, 'receiver renegotiation to inactive does not disable simulcast');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const {sender} = pc1.addTransceiver("video", {sendEncodings: [{rid: "foo"}, {rid: "bar"}]});
+ let {encodings} = sender.getParameters();
+
+ await doOfferToSendSimulcastAndAnswer(pc1, pc2, ["foo", "bar"]);
+
+ assert_equals(pc1.getTransceivers().length, 1);
+ encodings = sender.getParameters().encodings;
+ let rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo", "bar"]);
+
+ pc2.getTransceivers()[0].direction = "sendonly";
+ pc2.getTransceivers()[1].direction = "sendonly";
+ await doOfferToSendSimulcastAndAnswer(pc1, pc2, ["foo", "bar"]);
+ assert_equals(pc1.getTransceivers().length, 1);
+ encodings = sender.getParameters().encodings;
+ rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo", "bar"]);
+}, 'receiver renegotiation to sendonly does not disable simulcast');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/simulcast/rid-manipulation.html b/testing/web-platform/tests/webrtc/simulcast/rid-manipulation.html
new file mode 100644
index 0000000000..a88506305a
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/simulcast/rid-manipulation.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>RTCPeerConnection Simulcast Tests - RID manipulation</title>
+<meta name="timeout" content="long">
+<script src="../third_party/sdp/sdp.js"></script>
+<script src="simulcast.js"></script>
+<script src="../RTCPeerConnection-helper.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="../../mediacapture-streams/permission-helper.js"></script>
+<script>
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+ const rids = [0, 1, 2];
+ pc1.addTransceiver("video", {sendEncodings: rids.map(rid => ({rid}))});
+ const [{sender}] = pc1.getTransceivers();
+
+ const negotiateSfuAnswer = async asimulcast => {
+ const offer = await pc1.createOffer();
+ await pc1.setLocalDescription(offer);
+ offer.sdp = swapRidAndMidExtensionsInSimulcastOffer(offer, rids);
+ await pc2.setRemoteDescription(offer);
+ const answer = await pc2.createAnswer();
+ await pc2.setLocalDescription(answer);
+ answer.sdp = swapRidAndMidExtensionsInSimulcastAnswer(answer,pc1.localDescription, rids);
+ answer.sdp = answer.sdp.replace('a=simulcast:recv 0;1;2', asimulcast);
+ return answer;
+ };
+ await pc1.setRemoteDescription(await negotiateSfuAnswer('a=simulcast:recv foo;1;2'));
+ await pc1.setRemoteDescription(await negotiateSfuAnswer('a=simulcast:recv foo;bar;2'));
+}, 'Remote reanswer altering rids does not throw an exception.');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/simulcast/setParameters-active.https.html b/testing/web-platform/tests/webrtc/simulcast/setParameters-active.https.html
new file mode 100644
index 0000000000..dbe162c610
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/simulcast/setParameters-active.https.html
@@ -0,0 +1,104 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerConnection Simulcast Tests - setParameters/active</title>
+<meta name="timeout" content="long">
+<script src="../third_party/sdp/sdp.js"></script>
+<script src="simulcast.js"></script>
+<script src="../RTCPeerConnection-helper.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="../../mediacapture-streams/permission-helper.js"></script>
+<script>
+async function queryReceiverStats(pc) {
+ const inboundStats = [];
+ await Promise.all(pc.getReceivers().map(async receiver => {
+ const receiverStats = await receiver.getStats();
+ receiverStats.forEach(stat => {
+ if (stat.type === 'inbound-rtp') {
+ inboundStats.push(stat);
+ }
+ });
+ }));
+ return inboundStats.map(s => s.framesDecoded);
+}
+
+promise_test(async t => {
+ const rids = [0, 1];
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ await negotiateSimulcastAndWaitForVideo(t, rids, pc1, pc2);
+
+ // Deactivate first sender.
+ const parameters = pc1.getSenders()[0].getParameters();
+ parameters.encodings[0].active = false;
+ await pc1.getSenders()[0].setParameters(parameters);
+
+ // Assert (almost) no new frames are received on the first encoding.
+ // Without any action we would expect to have received around 30fps.
+ await new Promise(resolve => t.step_timeout(resolve, 200)); // Wait a bit.
+ const initialStats = await queryReceiverStats(pc2);
+ await new Promise(resolve => t.step_timeout(resolve, 1000)); // Wait more.
+ const subsequentStats = await queryReceiverStats(pc2);
+
+ assert_equals(subsequentStats[0], initialStats[0]);
+ assert_greater_than(subsequentStats[1], initialStats[1]);
+}, 'Simulcast setParameters active=false on first encoding stops sending frames for that encoding');
+
+promise_test(async t => {
+ const rids = [0, 1];
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ await negotiateSimulcastAndWaitForVideo(t, rids, pc1, pc2);
+
+ // Deactivate second sender.
+ const parameters = pc1.getSenders()[0].getParameters();
+ parameters.encodings[1].active = false;
+ await pc1.getSenders()[0].setParameters(parameters);
+
+ // Assert (almost) no new frames are received on the second encoding.
+ // Without any action we would expect to have received around 30fps.
+ await new Promise(resolve => t.step_timeout(resolve, 200)); // Wait a bit.
+ const initialStats = await queryReceiverStats(pc2);
+ await new Promise(resolve => t.step_timeout(resolve, 1000)); // Wait more.
+ const subsequentStats = await queryReceiverStats(pc2);
+
+ assert_equals(subsequentStats[1], initialStats[1]);
+ assert_greater_than(subsequentStats[0], initialStats[0]);
+}, 'Simulcast setParameters active=false on second encoding stops sending frames for that encoding');
+
+promise_test(async t => {
+ const rids = [0, 1];
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ await negotiateSimulcastAndWaitForVideo(t, rids, pc1, pc2);
+
+ // Deactivate all senders.
+ const parameters = pc1.getSenders()[0].getParameters();
+ parameters.encodings.forEach(e => {
+ e.active = false;
+ });
+ await pc1.getSenders()[0].setParameters(parameters);
+
+ // Assert (almost) no new frames are received.
+ // Without any action we would expect to have received around 30fps.
+ await new Promise(resolve => t.step_timeout(resolve, 200)); // Wait a bit.
+ const initialStats = await queryReceiverStats(pc2);
+ await new Promise(resolve => t.step_timeout(resolve, 1000)); // Wait more.
+ const subsequentStats = await queryReceiverStats(pc2);
+
+ subsequentStats.forEach((framesDecoded, idx) => {
+ assert_equals(framesDecoded, initialStats[idx]);
+ });
+}, 'Simulcast setParameters active=false stops sending frames');
+</script>
diff --git a/testing/web-platform/tests/webrtc/simulcast/setParameters-encodings.https.html b/testing/web-platform/tests/webrtc/simulcast/setParameters-encodings.https.html
new file mode 100644
index 0000000000..ac04ca55fb
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/simulcast/setParameters-encodings.https.html
@@ -0,0 +1,462 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerConnection Simulcast Tests - setParameters/encodings</title>
+<meta name="timeout" content="long">
+<script src="../third_party/sdp/sdp.js"></script>
+<script src="simulcast.js"></script>
+<script src="../RTCPeerConnection-helper.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="../../mediacapture-streams/permission-helper.js"></script>
+<script>
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const {sender} = pc1.addTransceiver("video", {sendEncodings: [{rid: "foo"}, {rid: "bar"}]});
+
+ await doOfferToSendSimulcast(pc1, pc2);
+
+ await pc2.setLocalDescription();
+ const simulcastAnswer = midToRid(pc2.localDescription, pc1.localDescription, ["foo"]);
+
+ const parameters = sender.getParameters();
+ parameters.encodings[1].scaleResolutionDownBy = 3.3;
+ const answerDone = pc1.setRemoteDescription({type: "answer", sdp: simulcastAnswer});
+ await sender.setParameters(parameters);
+ await answerDone;
+
+ assert_equals(pc1.getTransceivers().length, 1);
+ const {encodings} = sender.getParameters();
+ const rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo"]);
+}, 'sRD(simulcast answer) can narrow the simulcast envelope when interrupted by a setParameters');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const {sender} = pc1.addTransceiver("video", {sendEncodings: [{rid: "foo"}, {rid: "bar"}]});
+
+ await doOfferToSendSimulcastAndAnswer(pc1, pc2, ["foo", "bar"]);
+
+ assert_equals(pc1.getTransceivers().length, 1);
+ let encodings = sender.getParameters().encodings;
+ let rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo", "bar"]);
+
+ const reoffer = await pc2.createOffer();
+ const simulcastSdp = midToRid(reoffer, pc1.localDescription, ["foo"]);
+
+ const parameters = sender.getParameters();
+ parameters.encodings[1].scaleResolutionDownBy = 3.3;
+ const reofferDone = pc1.setRemoteDescription({type: "offer", sdp: simulcastSdp});
+ await sender.setParameters(parameters);
+ await reofferDone;
+ await pc1.setLocalDescription();
+
+ encodings = sender.getParameters().encodings;
+ rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo"]);
+}, 'sRD(simulcast offer) can narrow the simulcast envelope when interrupted by a setParameters');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const {sender} = pc1.addTransceiver("video", {sendEncodings: [{rid: "foo"}, {rid: "bar"}]});
+
+ const parameters = sender.getParameters();
+ parameters.encodings[0].scaleResolutionDownBy = 2.3;
+ parameters.encodings[1].scaleResolutionDownBy = 3.3;
+ await sender.setParameters(parameters);
+
+ await doOfferToSendSimulcast(pc1, pc2);
+ await doAnswerToRecvSimulcast(pc1, pc2, []);
+
+ assert_equals(pc1.getTransceivers().length, 1);
+ const encodings = sender.getParameters().encodings;
+ const rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo"]);
+ assert_equals(encodings[0].scaleResolutionDownBy, 2.3);
+}, 'a simulcast setParameters followed by a sRD(unicast answer) results in keeping the first encoding');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const {sender} = pc1.addTransceiver("video", {sendEncodings: [{rid: "foo"}, {rid: "bar"}]});
+ await doOfferToSendSimulcast(pc1, pc2);
+
+ await pc2.setLocalDescription();
+ const unicastAnswer = midToRid(pc2.localDescription, pc1.localDescription, []);
+
+ const parameters = sender.getParameters();
+ parameters.encodings[0].scaleResolutionDownBy = 2.3;
+ parameters.encodings[1].scaleResolutionDownBy = 3.3;
+ const answerDone = pc1.setRemoteDescription({type: "answer", sdp: unicastAnswer});
+ await sender.setParameters(parameters);
+ await answerDone;
+
+ assert_equals(pc1.getTransceivers().length, 1);
+ const encodings = sender.getParameters().encodings;
+ const rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo"]);
+ assert_equals(encodings[0].scaleResolutionDownBy, 2.3);
+}, 'sRD(unicast answer) interrupted by setParameters(simulcast) results in keeping the first encoding');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const {sender} = pc1.addTransceiver("video", {sendEncodings: [{rid: "foo"}, {rid: "bar"}]});
+
+ await doOfferToSendSimulcastAndAnswer(pc1, pc2, ["foo", "bar"]);
+ assert_equals(pc1.getTransceivers().length, 1);
+ let encodings = sender.getParameters().encodings;
+ let rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo", "bar"]);
+
+ const reoffer = await pc2.createOffer();
+ const unicastSdp = midToRid(reoffer, pc1.localDescription, []);
+ const parameters = sender.getParameters();
+ parameters.encodings[0].scaleResolutionDownBy = 2.3;
+ parameters.encodings[1].scaleResolutionDownBy = 3.3;
+ const reofferDone = pc1.setRemoteDescription({type: "offer", sdp: unicastSdp});
+ await sender.setParameters(parameters);
+ await reofferDone;
+ await pc1.setLocalDescription();
+
+ encodings = sender.getParameters().encodings;
+ rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo"]);
+ assert_equals(encodings[0].scaleResolutionDownBy, 2.3);
+}, 'sRD(unicast reoffer) interrupted by setParameters(simulcast) results in keeping the first encoding');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const {sender} = pc1.addTransceiver("video", {sendEncodings: [{rid: "foo"}, {rid: "bar"}]});
+
+ await doOfferToSendSimulcast(pc1, pc2);
+ await pc2.setLocalDescription();
+ const simulcastAnswer = midToRid(pc2.localDescription, pc1.localDescription, ["foo"]);
+ const parameters = sender.getParameters();
+ parameters.encodings[0].scaleResolutionDownBy = 3.3;
+ const answerDone = pc1.setRemoteDescription({type: "answer", sdp: simulcastAnswer});
+ await sender.setParameters(parameters);
+ await answerDone;
+
+ const {encodings} = sender.getParameters();
+ assert_equals(encodings.length, 1);
+ assert_equals(encodings[0].scaleResolutionDownBy, 3.3);
+}, 'sRD(simulcast answer) interrupted by a setParameters does not result in losing modifications from the setParameters to the encodings that remain');
+
+const simulcastOffer = `v=0
+o=- 3840232462471583827 0 IN IP4 127.0.0.1
+s=-
+t=0 0
+a=group:BUNDLE 0
+a=msid-semantic: WMS
+m=video 9 UDP/TLS/RTP/SAVPF 96
+c=IN IP4 0.0.0.0
+a=rtcp:9 IN IP4 0.0.0.0
+a=ice-ufrag:Li6+
+a=ice-pwd:3C05CTZBRQVmGCAq7hVasHlT
+a=ice-options:trickle
+a=fingerprint:sha-256 5B:D3:8E:66:0E:7D:D3:F3:8E:E6:80:28:19:FC:55:AD:58:5D:B9:3D:A8:DE:45:4A:E7:87:02:F8:3C:0B:3B:B3
+a=setup:actpass
+a=mid:0
+a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id
+a=recvonly
+a=rtcp-mux
+a=rtpmap:96 VP8/90000
+a=rtcp-fb:96 goog-remb
+a=rtcp-fb:96 transport-cc
+a=rtcp-fb:96 ccm fir
+a=rid:foo recv
+a=rid:bar recv
+a=simulcast:recv foo;bar
+`;
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+
+ const stream = await getNoiseStream({video: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const sender = pc1.addTrack(stream.getTracks()[0]);
+ const parameters = sender.getParameters();
+ parameters.encodings[0].scaleResolutionDownBy = 3.0;
+ await sender.setParameters(parameters);
+
+ await pc1.setRemoteDescription({type: "offer", sdp: simulcastOffer});
+
+ const {encodings} = sender.getParameters();
+ const rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo", "bar"]);
+ assert_equals(encodings[0].scaleResolutionDownBy, 2.0);
+ assert_equals(encodings[1].scaleResolutionDownBy, 1.0);
+}, 'addTrack, then a unicast setParameters, then sRD(simulcast offer) results in simulcast without the settings from setParameters');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+
+ const stream = await getNoiseStream({video: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const sender = pc1.addTrack(stream.getTracks()[0]);
+ const parameters = sender.getParameters();
+ parameters.encodings[0].scaleResolutionDownBy = 3.0;
+
+ const offerDone = pc1.setRemoteDescription({type: "offer", sdp: simulcastOffer});
+ await sender.setParameters(parameters);
+ await offerDone;
+
+ const {encodings} = sender.getParameters();
+ const rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo", "bar"]);
+ assert_equals(encodings[0].scaleResolutionDownBy, 2.0);
+ assert_equals(encodings[1].scaleResolutionDownBy, 1.0);
+}, 'addTrack, then sRD(simulcast offer) interrupted by a unicast setParameters results in simulcast without the settings from setParameters');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+
+ const stream = await getNoiseStream({video: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const {sender} = pc1.addTransceiver(stream.getTracks()[0], {sendEncodings: [{rid: "foo"}, {rid: "bar"}]});
+ await doOfferToSendSimulcastAndAnswer(pc1, pc2, ["foo", "bar"]);
+ pc2.getTransceivers()[0].direction = "sendrecv";
+ pc2.getTransceivers()[1].direction = "sendrecv";
+
+ await doOfferToRecvSimulcast(pc2, pc1, []);
+ // Race simulcast setParameters against sLD(unicast reanswer)
+ const answer = await pc1.createAnswer();
+ const aTask = queueAWebrtcTask();
+ // This also queues a task to clear [[LastReturnedParameters]]
+ const parameters = sender.getParameters();
+ // This might or might not queue a task right away (it might do some
+ // microtask stuff first), but it doesn't really matter.
+ const sLDDone = pc1.setLocalDescription(answer);
+ await aTask;
+ // Task queue should now have the task that clears
+ // [[LastReturnedParameters]], _then_ the success task for sLD.
+ // setParameters should succeed because [[LastReturnedParameters]] has not
+ // yet been cleared, and the steps in the success task for sLD have not run
+ // either.
+ await sender.setParameters(parameters);
+ await sLDDone;
+
+ assert_equals(pc1.getTransceivers().length, 1);
+ const {encodings} = sender.getParameters();
+ const rids = encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo"]);
+}, 'getParameters, then sLD(unicast answer) interrupted by a simulcast setParameters results in unicast');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+
+ const stream = await getNoiseStream({video: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const {sender} = pc1.addTransceiver(stream.getTracks()[0], {sendEncodings: [{rid: "foo"}, {rid: "bar"}]});
+ await doOfferToSendSimulcastAndAnswer(pc1, pc2, ["foo", "bar"]);
+ pc2.getTransceivers()[0].direction = "sendrecv";
+ pc2.getTransceivers()[1].direction = "sendrecv";
+
+ await doOfferToRecvSimulcast(pc2, pc1, []);
+ const answer = await pc1.createAnswer();
+
+ // The timing on this is very difficult. We want to ensure that our
+ // getParameters call happens after the initial steps in sLD, but
+ // before the queued task that sLD runs when it completes.
+ const aTask = queueAWebrtcTask();
+ const sLDDone = pc1.setLocalDescription(answer);
+ // We now have a queued task (aTask). We might also have the success task for
+ // sLD, but maybe not. Allowing aTask to finish gives us our best chance that
+ // the success task for sLD is queued, but not run yet.
+ await aTask;
+ const parameters = sender.getParameters();
+ // Hopefully we now have the success task for sLD, followed by the
+ // success task for getParameters.
+ await sLDDone;
+ // Success task for getParameters should not have run yet.
+ await promise_rejects_dom(t, 'InvalidStateError', sender.setParameters(parameters));
+},'Success task for setLocalDescription(answer) clears [[LastReturnedParameters]]');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+
+ const stream = await getNoiseStream({video: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const {sender} = pc1.addTransceiver(stream.getTracks()[0], {sendEncodings: [{rid: "foo"}, {rid: "bar"}]});
+ await doOfferToSendSimulcastAndAnswer(pc1, pc2, ["foo", "bar"]);
+ pc2.getTransceivers()[0].direction = "sendrecv";
+ pc2.getTransceivers()[1].direction = "sendrecv";
+
+ await pc2.setLocalDescription();
+ const simulcastOffer = midToRid(
+ pc2.localDescription,
+ pc1.localDescription,
+ []
+ );
+
+ // The timing on this is very difficult. We need to ensure that our
+ // getParameters call happens after the initial steps in sRD, but
+ // before the queued task that sRD runs when it completes.
+ const aTask = queueAWebrtcTask();
+ const sRDDone = pc1.setRemoteDescription({ type: "offer", sdp: simulcastOffer });
+
+ await aTask;
+ const parameters = sender.getParameters();
+ await sRDDone;
+ await promise_rejects_dom(t, 'InvalidStateError', sender.setParameters(parameters));
+},'Success task for setRemoteDescription(offer) clears [[LastReturnedParameters]]');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ t.add_cleanup(() => pc2.close());
+
+ const stream = await getNoiseStream({video: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const {sender} = pc1.addTransceiver(stream.getTracks()[0], {sendEncodings: [{rid: "foo"}, {rid: "bar"}]});
+ await doOfferToSendSimulcastAndAnswer(pc1, pc2, ["foo", "bar"]);
+ pc2.getTransceivers()[0].direction = "sendrecv";
+ pc2.getTransceivers()[1].direction = "sendrecv";
+
+ await doOfferToSendSimulcast(pc1, pc2);
+ await pc2.setLocalDescription();
+ const simulcastAnswer = midToRid(
+ pc2.localDescription,
+ pc1.localDescription,
+ []
+ );
+
+ // The timing on this is very difficult. We need to ensure that our
+ // getParameters call happens after the initial steps in sRD, but
+ // before the queued task that sRD runs when it completes.
+ const aTask = queueAWebrtcTask();
+ const sRDDone = pc1.setRemoteDescription({ type: "answer", sdp: simulcastAnswer });
+ await aTask;
+
+ const parameters = sender.getParameters();
+ await sRDDone;
+ await promise_rejects_dom(t, 'InvalidStateError', sender.setParameters(parameters));
+},'Success task for setRemoteDescription(answer) clears [[LastReturnedParameters]]');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const stream = await getNoiseStream({video: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const sender = pc1.addTrack(stream.getTracks()[0]);
+ pc2.addTrack(stream.getTracks()[0]);
+
+ await doOfferToRecvSimulcast(pc2, pc1, ["foo", "bar"]);
+ let parameters = sender.getParameters();
+ let rids = parameters.encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo", "bar"]);
+ parameters.encodings[0].scaleResolutionDownBy = 3;
+ parameters.encodings[1].scaleResolutionDownBy = 5;
+ await sender.setParameters(parameters);
+
+ await pc1.setRemoteDescription({sdp: "", type: "rollback"});
+
+ parameters = sender.getParameters();
+ rids = parameters.encodings.map(({rid}) => rid);
+ assert_array_equals(rids, [undefined]);
+ assert_equals(parameters.encodings[0].scaleResolutionDownBy, 1);
+}, 'addTrack, then rollback of sRD(simulcast offer), brings us back to having a single encoding without any previously set parameters');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const stream = await getNoiseStream({video: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const {sender} = pc1.addTransceiver(stream.getTracks()[0], {sendEncodings: [{rid: "foo"}, {rid: "bar"}]});
+
+ await doOfferToSendSimulcastAndAnswer(pc1, pc2, ["foo", "bar"]);
+ let parameters = sender.getParameters();
+ let rids = parameters.encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo", "bar"]);
+ parameters.encodings[0].scaleResolutionDownBy = 3;
+ parameters.encodings[1].scaleResolutionDownBy = 5;
+ await sender.setParameters(parameters);
+
+ await doOfferToRecvSimulcast(pc2, pc1, []);
+ parameters = sender.getParameters();
+ rids = parameters.encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo", "bar"]);
+
+ await pc1.setRemoteDescription({sdp: "", type: "rollback"});
+ parameters = sender.getParameters();
+ rids = parameters.encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo", "bar"]);
+ assert_equals(parameters.encodings[0].scaleResolutionDownBy, 3);
+ assert_equals(parameters.encodings[1].scaleResolutionDownBy, 5);
+}, 'rollback of a remote offer that disabled a previously negotiated simulcast should restore simulcast along with any previously set parameters');
+
+promise_test(async t => {
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ const stream = await getNoiseStream({video: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const sender = pc1.addTrack(stream.getTracks()[0]);
+ pc2.addTrack(stream.getTracks()[0]);
+
+ await doOfferToRecvSimulcast(pc2, pc1, ["foo", "bar"]);
+ const aTask = queueAWebrtcTask();
+ let parameters = sender.getParameters();
+ let rids = parameters.encodings.map(({rid}) => rid);
+ assert_array_equals(rids, ["foo", "bar"]);
+ parameters.encodings[0].scaleResolutionDownBy = 3;
+ parameters.encodings[1].scaleResolutionDownBy = 5;
+
+ const rollbackDone = pc1.setRemoteDescription({sdp: "", type: "rollback"});
+ await aTask;
+ await sender.setParameters(parameters);
+ await rollbackDone;
+
+ parameters = sender.getParameters();
+ rids = parameters.encodings.map(({rid}) => rid);
+ assert_array_equals(rids, [undefined]);
+ assert_equals(parameters.encodings[0].scaleResolutionDownBy, 1);
+}, 'rollback of sRD(simulcast offer) interrupted by setParameters(simulcast) brings us back to having a single encoding without any previously set parameters');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/simulcast/simulcast.js b/testing/web-platform/tests/webrtc/simulcast/simulcast.js
new file mode 100644
index 0000000000..4682729233
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/simulcast/simulcast.js
@@ -0,0 +1,254 @@
+'use strict';
+/* Helper functions to munge SDP and split the sending track into
+ * separate tracks on the receiving end. This can be done in a number
+ * of ways, the one used here uses the fact that the MID and RID header
+ * extensions which are used for packet routing share the same wire
+ * format. The receiver interprets the rids from the sender as mids
+ * which allows receiving the different spatial resolutions on separate
+ * m-lines and tracks.
+ */
+
+const ridExtensions = [
+ "urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id",
+ "urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id",
+];
+
+function ridToMid(description, rids) {
+ const sections = SDPUtils.splitSections(description.sdp);
+ const dtls = SDPUtils.getDtlsParameters(sections[1], sections[0]);
+ const ice = SDPUtils.getIceParameters(sections[1], sections[0]);
+ const rtpParameters = SDPUtils.parseRtpParameters(sections[1]);
+ const setupValue = description.sdp.match(/a=setup:(.*)/)[1];
+ const directionValue =
+ sections[1].match(/a=sendrecv|a=sendonly|a=recvonly|a=inactive/)[0];
+ const mline = SDPUtils.parseMLine(sections[1]);
+
+ // Skip mid extension; we are replacing it with the rid extmap
+ rtpParameters.headerExtensions = rtpParameters.headerExtensions.filter(
+ ext => ext.uri != "urn:ietf:params:rtp-hdrext:sdes:mid"
+ );
+
+ for (const ext of rtpParameters.headerExtensions) {
+ if (ext.uri == "urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id") {
+ ext.uri = "urn:ietf:params:rtp-hdrext:sdes:mid";
+ }
+ }
+
+ // Filter rtx as we have no way to (re)interpret rrid.
+ // Not doing this makes probing use RTX, it's not understood and ramp-up is slower.
+ rtpParameters.codecs = rtpParameters.codecs.filter(c => c.name.toUpperCase() !== 'RTX');
+
+ if (!rids) {
+ rids = Array.from(description.sdp.matchAll(/a=rid:(.*) send/g)).map(r => r[1]);
+ }
+
+ let sdp = SDPUtils.writeSessionBoilerplate() +
+ SDPUtils.writeDtlsParameters(dtls, setupValue) +
+ SDPUtils.writeIceParameters(ice) +
+ 'a=group:BUNDLE ' + rids.join(' ') + '\r\n';
+ const baseRtpDescription = SDPUtils.writeRtpDescription(mline.kind, rtpParameters);
+ for (const rid of rids) {
+ sdp += baseRtpDescription +
+ 'a=mid:' + rid + '\r\n' +
+ 'a=msid:rid-' + rid + ' rid-' + rid + '\r\n';
+ sdp += directionValue + "\r\n";
+ }
+ return sdp;
+}
+
+function midToRid(description, localDescription, rids) {
+ const sections = SDPUtils.splitSections(description.sdp);
+ const dtls = SDPUtils.getDtlsParameters(sections[1], sections[0]);
+ const ice = SDPUtils.getIceParameters(sections[1], sections[0]);
+ const rtpParameters = SDPUtils.parseRtpParameters(sections[1]);
+ const setupValue = description.sdp.match(/a=setup:(.*)/)[1];
+ const directionValue =
+ sections[1].match(/a=sendrecv|a=sendonly|a=recvonly|a=inactive/)[0];
+ const mline = SDPUtils.parseMLine(sections[1]);
+
+ // Skip rid extensions; we are replacing them with the mid extmap
+ rtpParameters.headerExtensions = rtpParameters.headerExtensions.filter(
+ ext => !ridExtensions.includes(ext.uri)
+ );
+
+ for (const ext of rtpParameters.headerExtensions) {
+ if (ext.uri == "urn:ietf:params:rtp-hdrext:sdes:mid") {
+ ext.uri = "urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id";
+ }
+ }
+
+ const localMid = localDescription ? SDPUtils.getMid(SDPUtils.splitSections(localDescription.sdp)[1]) : "0";
+
+ if (!rids) {
+ rids = [];
+ for (let i = 1; i < sections.length; i++) {
+ rids.push(SDPUtils.getMid(sections[i]));
+ }
+ }
+
+ let sdp = SDPUtils.writeSessionBoilerplate() +
+ SDPUtils.writeDtlsParameters(dtls, setupValue) +
+ SDPUtils.writeIceParameters(ice) +
+ 'a=group:BUNDLE ' + localMid + '\r\n';
+ sdp += SDPUtils.writeRtpDescription(mline.kind, rtpParameters);
+ // Although we are converting mids to rids, we still need a mid.
+ // The first one will be consistent with trickle ICE candidates.
+ sdp += 'a=mid:' + localMid + '\r\n';
+ sdp += directionValue + "\r\n";
+
+ for (const rid of rids) {
+ const stringrid = String(rid); // allow integers
+ const choices = stringrid.split(",");
+ choices.forEach(choice => {
+ sdp += 'a=rid:' + choice + ' recv\r\n';
+ });
+ }
+ if (rids.length) {
+ sdp += 'a=simulcast:recv ' + rids.join(';') + '\r\n';
+ }
+
+ return sdp;
+}
+
+async function doOfferToSendSimulcast(offerer, answerer) {
+ await offerer.setLocalDescription();
+
+ // Is this a renegotiation? If so, we cannot remove (or reorder!) any mids,
+ // even if some rids have been removed or reordered.
+ let mids = [];
+ if (answerer.localDescription) {
+ // Renegotiation. Mids must be the same as before, because renegotiation
+ // can never remove or reorder mids, nor can it expand the simulcast
+ // envelope.
+ mids = [...answerer.localDescription.sdp.matchAll(/a=mid:(.*)/g)].map(
+ e => e[1]
+ );
+ } else {
+ // First negotiation; the mids will be exactly the same as the rids
+ const simulcastAttr = offerer.localDescription.sdp.match(
+ /a=simulcast:send (.*)/
+ );
+ if (simulcastAttr) {
+ mids = simulcastAttr[1].split(";");
+ }
+ }
+
+ const nonSimulcastOffer = ridToMid(offerer.localDescription, mids);
+ await answerer.setRemoteDescription({
+ type: "offer",
+ sdp: nonSimulcastOffer,
+ });
+}
+
+async function doAnswerToRecvSimulcast(offerer, answerer, rids) {
+ await answerer.setLocalDescription();
+ const simulcastAnswer = midToRid(
+ answerer.localDescription,
+ offerer.localDescription,
+ rids
+ );
+ await offerer.setRemoteDescription({ type: "answer", sdp: simulcastAnswer });
+}
+
+async function doOfferToRecvSimulcast(offerer, answerer, rids) {
+ await offerer.setLocalDescription();
+ const simulcastOffer = midToRid(
+ offerer.localDescription,
+ answerer.localDescription,
+ rids
+ );
+ await answerer.setRemoteDescription({ type: "offer", sdp: simulcastOffer });
+}
+
+async function doAnswerToSendSimulcast(offerer, answerer) {
+ await answerer.setLocalDescription();
+
+ // See which mids the offerer had; it will barf if we remove or reorder them
+ const mids = [...offerer.localDescription.sdp.matchAll(/a=mid:(.*)/g)].map(
+ e => e[1]
+ );
+
+ const nonSimulcastAnswer = ridToMid(answerer.localDescription, mids);
+ await offerer.setRemoteDescription({
+ type: "answer",
+ sdp: nonSimulcastAnswer,
+ });
+}
+
+async function doOfferToSendSimulcastAndAnswer(offerer, answerer, rids) {
+ await doOfferToSendSimulcast(offerer, answerer);
+ await doAnswerToRecvSimulcast(offerer, answerer, rids);
+}
+
+async function doOfferToRecvSimulcastAndAnswer(offerer, answerer, rids) {
+ await doOfferToRecvSimulcast(offerer, answerer, rids);
+ await doAnswerToSendSimulcast(offerer, answerer);
+}
+
+function swapRidAndMidExtensionsInSimulcastOffer(offer, rids) {
+ return ridToMid(offer, rids);
+}
+
+function swapRidAndMidExtensionsInSimulcastAnswer(answer, localDescription, rids) {
+ return midToRid(answer, localDescription, rids);
+}
+
+async function negotiateSimulcastAndWaitForVideo(
+ t, rids, pc1, pc2, codec, scalabilityMode = undefined) {
+ exchangeIceCandidates(pc1, pc2);
+
+ const metadataToBeLoaded = [];
+ pc2.ontrack = (e) => {
+ const stream = e.streams[0];
+ const v = document.createElement('video');
+ v.autoplay = true;
+ v.srcObject = stream;
+ v.id = stream.id
+ metadataToBeLoaded.push(new Promise((resolve) => {
+ v.addEventListener('loadedmetadata', () => {
+ resolve();
+ });
+ }));
+ };
+
+ const sendEncodings = rids.map(rid => ({rid}));
+ // Use a 2X downscale factor between each layer. To improve ramp-up time, the
+ // top layer is scaled down by a factor 2. Smaller layer comes first. For
+ // example if MediaStreamTrack is 720p and we want to send three layers we'll
+ // get {90p, 180p, 360p}.
+ let scaleResolutionDownBy = 2;
+ for (let i = sendEncodings.length - 1; i >= 0; --i) {
+ if (scalabilityMode) {
+ sendEncodings[i].scalabilityMode = scalabilityMode;
+ }
+ sendEncodings[i].scaleResolutionDownBy = scaleResolutionDownBy;
+ scaleResolutionDownBy *= 2;
+ }
+
+ // Use getUserMedia as getNoiseStream does not have enough entropy to ramp-up.
+ await setMediaPermission();
+ const stream = await navigator.mediaDevices.getUserMedia({video: {width: 1280, height: 720}});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const transceiver = pc1.addTransceiver(stream.getVideoTracks()[0], {
+ streams: [stream],
+ sendEncodings: sendEncodings,
+ });
+ if (codec) {
+ preferCodec(transceiver, codec.mimeType, codec.sdpFmtpLine);
+ }
+
+ const offer = await pc1.createOffer();
+ await pc1.setLocalDescription(offer),
+ await pc2.setRemoteDescription({
+ type: 'offer',
+ sdp: swapRidAndMidExtensionsInSimulcastOffer(offer, rids),
+ });
+ const answer = await pc2.createAnswer();
+ await pc2.setLocalDescription(answer);
+ await pc1.setRemoteDescription({
+ type: 'answer',
+ sdp: swapRidAndMidExtensionsInSimulcastAnswer(answer, pc1.localDescription, rids),
+ });
+ assert_equals(metadataToBeLoaded.length, rids.length);
+ return Promise.all(metadataToBeLoaded);
+}
diff --git a/testing/web-platform/tests/webrtc/simulcast/vp8.https.html b/testing/web-platform/tests/webrtc/simulcast/vp8.https.html
new file mode 100644
index 0000000000..3d04bc7172
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/simulcast/vp8.https.html
@@ -0,0 +1,26 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerConnection Simulcast Tests</title>
+<meta name="timeout" content="long">
+<script src="../third_party/sdp/sdp.js"></script>
+<script src="simulcast.js"></script>
+<script src="../RTCPeerConnection-helper.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="../../mediacapture-streams/permission-helper.js"></script>
+<script>
+promise_test(async t => {
+ assert_implements('getCapabilities' in RTCRtpSender, 'RTCRtpSender.getCapabilities not supported');
+ assert_implements(RTCRtpSender.getCapabilities('video').codecs.find(c => c.mimeType === 'video/VP8'), 'VP8 not supported');
+
+ const rids = [0, 1];
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ return negotiateSimulcastAndWaitForVideo(t, rids, pc1, pc2, {mimeType: 'video/VP8'});
+}, 'VP8 simulcast setup with two streams');
+</script>
diff --git a/testing/web-platform/tests/webrtc/simulcast/vp9-scalability-mode.https.html b/testing/web-platform/tests/webrtc/simulcast/vp9-scalability-mode.https.html
new file mode 100644
index 0000000000..9dc8a3103d
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/simulcast/vp9-scalability-mode.https.html
@@ -0,0 +1,35 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerConnection Simulcast Tests</title>
+<meta name="timeout" content="long">
+<script src="../third_party/sdp/sdp.js"></script>
+<script src="simulcast.js"></script>
+<script src="../RTCPeerConnection-helper.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="../../mediacapture-streams/permission-helper.js"></script>
+<script>
+promise_test(async t => {
+ assert_implements('getCapabilities' in RTCRtpSender, 'RTCRtpSender.getCapabilities not supported');
+ assert_implements(RTCRtpSender.getCapabilities('video').codecs.find(c => c.mimeType === 'video/VP9'), 'VP9 not supported');
+
+ const rids = [0, 1];
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ // This is not a scalability mode test (see wpt/webrtc-svc/ for those) but a
+ // VP9 simulcast test. Setting `scalabilityMode` should not be needed, however
+ // many browsers interprets multiple VP9 encodings to mean multiple spatial
+ // layers by default. During a transition period, Chromium-based browsers
+ // requires explicitly specifying the scalability mode as a way to opt-in to
+ // spec-compliant simulcast. See also wpt/webrtc/simulcast/vp9.https.html for
+ // a version of this test that does not set the scalability mode.
+ const scalabilityMode = 'L1T2';
+ return negotiateSimulcastAndWaitForVideo(
+ t, rids, pc1, pc2, {mimeType: 'video/VP9'}, scalabilityMode);
+}, 'VP9 simulcast setup with two streams and L1T2 set');
+</script>
diff --git a/testing/web-platform/tests/webrtc/simulcast/vp9.https.html b/testing/web-platform/tests/webrtc/simulcast/vp9.https.html
new file mode 100644
index 0000000000..a033dab477
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/simulcast/vp9.https.html
@@ -0,0 +1,26 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerConnection Simulcast Tests</title>
+<meta name="timeout" content="long">
+<script src="../third_party/sdp/sdp.js"></script>
+<script src="simulcast.js"></script>
+<script src="../RTCPeerConnection-helper.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="../../mediacapture-streams/permission-helper.js"></script>
+<script>
+promise_test(async t => {
+ assert_implements('getCapabilities' in RTCRtpSender, 'RTCRtpSender.getCapabilities not supported');
+ assert_implements(RTCRtpSender.getCapabilities('video').codecs.find(c => c.mimeType === 'video/VP9'), 'VP9 not supported');
+
+ const rids = [0, 1];
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+
+ return negotiateSimulcastAndWaitForVideo(t, rids, pc1, pc2, {mimeType: 'video/VP9'});
+}, 'VP9 simulcast setup with two streams');
+</script>
diff --git a/testing/web-platform/tests/webrtc/third_party/README.md b/testing/web-platform/tests/webrtc/third_party/README.md
new file mode 100644
index 0000000000..56a2295dd1
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/third_party/README.md
@@ -0,0 +1,5 @@
+## sdp
+Third-party SDP module from
+ https://www.npmjs.com/package/sdp
+without tests or dependencies. See the commit message for version
+and commit information
diff --git a/testing/web-platform/tests/webrtc/third_party/sdp/LICENSE b/testing/web-platform/tests/webrtc/third_party/sdp/LICENSE
new file mode 100644
index 0000000000..09502ec0a1
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/third_party/sdp/LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2017 Philipp Hancke
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/testing/web-platform/tests/webrtc/third_party/sdp/sdp.js b/testing/web-platform/tests/webrtc/third_party/sdp/sdp.js
new file mode 100644
index 0000000000..a7538a671e
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/third_party/sdp/sdp.js
@@ -0,0 +1,825 @@
+/* eslint-env node */
+'use strict';
+
+// SDP helpers.
+var SDPUtils = {};
+
+// Generate an alphanumeric identifier for cname or mids.
+// TODO: use UUIDs instead? https://gist.github.com/jed/982883
+SDPUtils.generateIdentifier = function() {
+ return Math.random().toString(36).substr(2, 10);
+};
+
+// The RTCP CNAME used by all peerconnections from the same JS.
+SDPUtils.localCName = SDPUtils.generateIdentifier();
+
+// Splits SDP into lines, dealing with both CRLF and LF.
+SDPUtils.splitLines = function(blob) {
+ return blob.trim().split('\n').map(function(line) {
+ return line.trim();
+ });
+};
+// Splits SDP into sessionpart and mediasections. Ensures CRLF.
+SDPUtils.splitSections = function(blob) {
+ var parts = blob.split('\nm=');
+ return parts.map(function(part, index) {
+ return (index > 0 ? 'm=' + part : part).trim() + '\r\n';
+ });
+};
+
+// returns the session description.
+SDPUtils.getDescription = function(blob) {
+ var sections = SDPUtils.splitSections(blob);
+ return sections && sections[0];
+};
+
+// returns the individual media sections.
+SDPUtils.getMediaSections = function(blob) {
+ var sections = SDPUtils.splitSections(blob);
+ sections.shift();
+ return sections;
+};
+
+// Returns lines that start with a certain prefix.
+SDPUtils.matchPrefix = function(blob, prefix) {
+ return SDPUtils.splitLines(blob).filter(function(line) {
+ return line.indexOf(prefix) === 0;
+ });
+};
+
+// Parses an ICE candidate line. Sample input:
+// candidate:702786350 2 udp 41819902 8.8.8.8 60769 typ relay raddr 8.8.8.8
+// rport 55996"
+SDPUtils.parseCandidate = function(line) {
+ var parts;
+ // Parse both variants.
+ if (line.indexOf('a=candidate:') === 0) {
+ parts = line.substring(12).split(' ');
+ } else {
+ parts = line.substring(10).split(' ');
+ }
+
+ var candidate = {
+ foundation: parts[0],
+ component: parseInt(parts[1], 10),
+ protocol: parts[2].toLowerCase(),
+ priority: parseInt(parts[3], 10),
+ ip: parts[4],
+ address: parts[4], // address is an alias for ip.
+ port: parseInt(parts[5], 10),
+ // skip parts[6] == 'typ'
+ type: parts[7]
+ };
+
+ for (var i = 8; i < parts.length; i += 2) {
+ switch (parts[i]) {
+ case 'raddr':
+ candidate.relatedAddress = parts[i + 1];
+ break;
+ case 'rport':
+ candidate.relatedPort = parseInt(parts[i + 1], 10);
+ break;
+ case 'tcptype':
+ candidate.tcpType = parts[i + 1];
+ break;
+ case 'ufrag':
+ candidate.ufrag = parts[i + 1]; // for backward compability.
+ candidate.usernameFragment = parts[i + 1];
+ break;
+ default: // extension handling, in particular ufrag
+ candidate[parts[i]] = parts[i + 1];
+ break;
+ }
+ }
+ return candidate;
+};
+
+// Translates a candidate object into SDP candidate attribute.
+SDPUtils.writeCandidate = function(candidate) {
+ var sdp = [];
+ sdp.push(candidate.foundation);
+ sdp.push(candidate.component);
+ sdp.push(candidate.protocol.toUpperCase());
+ sdp.push(candidate.priority);
+ sdp.push(candidate.address || candidate.ip);
+ sdp.push(candidate.port);
+
+ var type = candidate.type;
+ sdp.push('typ');
+ sdp.push(type);
+ if (type !== 'host' && candidate.relatedAddress &&
+ candidate.relatedPort) {
+ sdp.push('raddr');
+ sdp.push(candidate.relatedAddress);
+ sdp.push('rport');
+ sdp.push(candidate.relatedPort);
+ }
+ if (candidate.tcpType && candidate.protocol.toLowerCase() === 'tcp') {
+ sdp.push('tcptype');
+ sdp.push(candidate.tcpType);
+ }
+ if (candidate.usernameFragment || candidate.ufrag) {
+ sdp.push('ufrag');
+ sdp.push(candidate.usernameFragment || candidate.ufrag);
+ }
+ return 'candidate:' + sdp.join(' ');
+};
+
+// Parses an ice-options line, returns an array of option tags.
+// a=ice-options:foo bar
+SDPUtils.parseIceOptions = function(line) {
+ return line.substr(14).split(' ');
+};
+
+// Parses an rtpmap line, returns RTCRtpCoddecParameters. Sample input:
+// a=rtpmap:111 opus/48000/2
+SDPUtils.parseRtpMap = function(line) {
+ var parts = line.substr(9).split(' ');
+ var parsed = {
+ payloadType: parseInt(parts.shift(), 10) // was: id
+ };
+
+ parts = parts[0].split('/');
+
+ parsed.name = parts[0];
+ parsed.clockRate = parseInt(parts[1], 10); // was: clockrate
+ parsed.channels = parts.length === 3 ? parseInt(parts[2], 10) : 1;
+ // legacy alias, got renamed back to channels in ORTC.
+ parsed.numChannels = parsed.channels;
+ return parsed;
+};
+
+// Generate an a=rtpmap line from RTCRtpCodecCapability or
+// RTCRtpCodecParameters.
+SDPUtils.writeRtpMap = function(codec) {
+ var pt = codec.payloadType;
+ if (codec.preferredPayloadType !== undefined) {
+ pt = codec.preferredPayloadType;
+ }
+ var channels = codec.channels || codec.numChannels || 1;
+ return 'a=rtpmap:' + pt + ' ' + codec.name + '/' + codec.clockRate +
+ (channels !== 1 ? '/' + channels : '') + '\r\n';
+};
+
+// Parses an a=extmap line (headerextension from RFC 5285). Sample input:
+// a=extmap:2 urn:ietf:params:rtp-hdrext:toffset
+// a=extmap:2/sendonly urn:ietf:params:rtp-hdrext:toffset
+SDPUtils.parseExtmap = function(line) {
+ var parts = line.substr(9).split(' ');
+ return {
+ id: parseInt(parts[0], 10),
+ direction: parts[0].indexOf('/') > 0 ? parts[0].split('/')[1] : 'sendrecv',
+ uri: parts[1]
+ };
+};
+
+// Generates a=extmap line from RTCRtpHeaderExtensionParameters or
+// RTCRtpHeaderExtension.
+SDPUtils.writeExtmap = function(headerExtension) {
+ return 'a=extmap:' + (headerExtension.id || headerExtension.preferredId) +
+ (headerExtension.direction && headerExtension.direction !== 'sendrecv'
+ ? '/' + headerExtension.direction
+ : '') +
+ ' ' + headerExtension.uri + '\r\n';
+};
+
+// Parses an ftmp line, returns dictionary. Sample input:
+// a=fmtp:96 vbr=on;cng=on
+// Also deals with vbr=on; cng=on
+SDPUtils.parseFmtp = function(line) {
+ var parsed = {};
+ var kv;
+ var parts = line.substr(line.indexOf(' ') + 1).split(';');
+ for (var j = 0; j < parts.length; j++) {
+ kv = parts[j].trim().split('=');
+ parsed[kv[0].trim()] = kv[1];
+ }
+ return parsed;
+};
+
+// Generates an a=ftmp line from RTCRtpCodecCapability or RTCRtpCodecParameters.
+SDPUtils.writeFmtp = function(codec) {
+ var line = '';
+ var pt = codec.payloadType;
+ if (codec.preferredPayloadType !== undefined) {
+ pt = codec.preferredPayloadType;
+ }
+ if (codec.parameters && Object.keys(codec.parameters).length) {
+ var params = [];
+ Object.keys(codec.parameters).forEach(function(param) {
+ if (codec.parameters[param]) {
+ params.push(param + '=' + codec.parameters[param]);
+ } else {
+ params.push(param);
+ }
+ });
+ line += 'a=fmtp:' + pt + ' ' + params.join(';') + '\r\n';
+ }
+ return line;
+};
+
+// Parses an rtcp-fb line, returns RTCPRtcpFeedback object. Sample input:
+// a=rtcp-fb:98 nack rpsi
+SDPUtils.parseRtcpFb = function(line) {
+ var parts = line.substr(line.indexOf(' ') + 1).split(' ');
+ return {
+ type: parts.shift(),
+ parameter: parts.join(' ')
+ };
+};
+// Generate a=rtcp-fb lines from RTCRtpCodecCapability or RTCRtpCodecParameters.
+SDPUtils.writeRtcpFb = function(codec) {
+ var lines = '';
+ var pt = codec.payloadType;
+ if (codec.preferredPayloadType !== undefined) {
+ pt = codec.preferredPayloadType;
+ }
+ if (codec.rtcpFeedback && codec.rtcpFeedback.length) {
+ // FIXME: special handling for trr-int?
+ codec.rtcpFeedback.forEach(function(fb) {
+ lines += 'a=rtcp-fb:' + pt + ' ' + fb.type +
+ (fb.parameter && fb.parameter.length ? ' ' + fb.parameter : '') +
+ '\r\n';
+ });
+ }
+ return lines;
+};
+
+// Parses an RFC 5576 ssrc media attribute. Sample input:
+// a=ssrc:3735928559 cname:something
+SDPUtils.parseSsrcMedia = function(line) {
+ var sp = line.indexOf(' ');
+ var parts = {
+ ssrc: parseInt(line.substr(7, sp - 7), 10)
+ };
+ var colon = line.indexOf(':', sp);
+ if (colon > -1) {
+ parts.attribute = line.substr(sp + 1, colon - sp - 1);
+ parts.value = line.substr(colon + 1);
+ } else {
+ parts.attribute = line.substr(sp + 1);
+ }
+ return parts;
+};
+
+SDPUtils.parseSsrcGroup = function(line) {
+ var parts = line.substr(13).split(' ');
+ return {
+ semantics: parts.shift(),
+ ssrcs: parts.map(function(ssrc) {
+ return parseInt(ssrc, 10);
+ })
+ };
+};
+
+// Extracts the MID (RFC 5888) from a media section.
+// returns the MID or undefined if no mid line was found.
+SDPUtils.getMid = function(mediaSection) {
+ var mid = SDPUtils.matchPrefix(mediaSection, 'a=mid:')[0];
+ if (mid) {
+ return mid.substr(6);
+ }
+};
+
+SDPUtils.parseFingerprint = function(line) {
+ var parts = line.substr(14).split(' ');
+ return {
+ algorithm: parts[0].toLowerCase(), // algorithm is case-sensitive in Edge.
+ value: parts[1]
+ };
+};
+
+// Extracts DTLS parameters from SDP media section or sessionpart.
+// FIXME: for consistency with other functions this should only
+// get the fingerprint line as input. See also getIceParameters.
+SDPUtils.getDtlsParameters = function(mediaSection, sessionpart) {
+ var lines = SDPUtils.matchPrefix(mediaSection + sessionpart,
+ 'a=fingerprint:');
+ // Note: a=setup line is ignored since we use the 'auto' role.
+ // Note2: 'algorithm' is not case sensitive except in Edge.
+ return {
+ role: 'auto',
+ fingerprints: lines.map(SDPUtils.parseFingerprint)
+ };
+};
+
+// Serializes DTLS parameters to SDP.
+SDPUtils.writeDtlsParameters = function(params, setupType) {
+ var sdp = 'a=setup:' + setupType + '\r\n';
+ params.fingerprints.forEach(function(fp) {
+ sdp += 'a=fingerprint:' + fp.algorithm + ' ' + fp.value + '\r\n';
+ });
+ return sdp;
+};
+
+// Parses a=crypto lines into
+// https://rawgit.com/aboba/edgertc/master/msortc-rs4.html#dictionary-rtcsrtpsdesparameters-members
+SDPUtils.parseCryptoLine = function(line) {
+ var parts = line.substr(9).split(' ');
+ return {
+ tag: parseInt(parts[0], 10),
+ cryptoSuite: parts[1],
+ keyParams: parts[2],
+ sessionParams: parts.slice(3),
+ };
+};
+
+SDPUtils.writeCryptoLine = function(parameters) {
+ return 'a=crypto:' + parameters.tag + ' ' +
+ parameters.cryptoSuite + ' ' +
+ (typeof parameters.keyParams === 'object'
+ ? SDPUtils.writeCryptoKeyParams(parameters.keyParams)
+ : parameters.keyParams) +
+ (parameters.sessionParams ? ' ' + parameters.sessionParams.join(' ') : '') +
+ '\r\n';
+};
+
+// Parses the crypto key parameters into
+// https://rawgit.com/aboba/edgertc/master/msortc-rs4.html#rtcsrtpkeyparam*
+SDPUtils.parseCryptoKeyParams = function(keyParams) {
+ if (keyParams.indexOf('inline:') !== 0) {
+ return null;
+ }
+ var parts = keyParams.substr(7).split('|');
+ return {
+ keyMethod: 'inline',
+ keySalt: parts[0],
+ lifeTime: parts[1],
+ mkiValue: parts[2] ? parts[2].split(':')[0] : undefined,
+ mkiLength: parts[2] ? parts[2].split(':')[1] : undefined,
+ };
+};
+
+SDPUtils.writeCryptoKeyParams = function(keyParams) {
+ return keyParams.keyMethod + ':'
+ + keyParams.keySalt +
+ (keyParams.lifeTime ? '|' + keyParams.lifeTime : '') +
+ (keyParams.mkiValue && keyParams.mkiLength
+ ? '|' + keyParams.mkiValue + ':' + keyParams.mkiLength
+ : '');
+};
+
+// Extracts all SDES paramters.
+SDPUtils.getCryptoParameters = function(mediaSection, sessionpart) {
+ var lines = SDPUtils.matchPrefix(mediaSection + sessionpart,
+ 'a=crypto:');
+ return lines.map(SDPUtils.parseCryptoLine);
+};
+
+// Parses ICE information from SDP media section or sessionpart.
+// FIXME: for consistency with other functions this should only
+// get the ice-ufrag and ice-pwd lines as input.
+SDPUtils.getIceParameters = function(mediaSection, sessionpart) {
+ var ufrag = SDPUtils.matchPrefix(mediaSection + sessionpart,
+ 'a=ice-ufrag:')[0];
+ var pwd = SDPUtils.matchPrefix(mediaSection + sessionpart,
+ 'a=ice-pwd:')[0];
+ if (!(ufrag && pwd)) {
+ return null;
+ }
+ return {
+ usernameFragment: ufrag.substr(12),
+ password: pwd.substr(10),
+ };
+};
+
+// Serializes ICE parameters to SDP.
+SDPUtils.writeIceParameters = function(params) {
+ return 'a=ice-ufrag:' + params.usernameFragment + '\r\n' +
+ 'a=ice-pwd:' + params.password + '\r\n';
+};
+
+// Parses the SDP media section and returns RTCRtpParameters.
+SDPUtils.parseRtpParameters = function(mediaSection) {
+ var description = {
+ codecs: [],
+ headerExtensions: [],
+ fecMechanisms: [],
+ rtcp: []
+ };
+ var lines = SDPUtils.splitLines(mediaSection);
+ var mline = lines[0].split(' ');
+ for (var i = 3; i < mline.length; i++) { // find all codecs from mline[3..]
+ var pt = mline[i];
+ var rtpmapline = SDPUtils.matchPrefix(
+ mediaSection, 'a=rtpmap:' + pt + ' ')[0];
+ if (rtpmapline) {
+ var codec = SDPUtils.parseRtpMap(rtpmapline);
+ var fmtps = SDPUtils.matchPrefix(
+ mediaSection, 'a=fmtp:' + pt + ' ');
+ // Only the first a=fmtp:<pt> is considered.
+ codec.parameters = fmtps.length ? SDPUtils.parseFmtp(fmtps[0]) : {};
+ codec.rtcpFeedback = SDPUtils.matchPrefix(
+ mediaSection, 'a=rtcp-fb:' + pt + ' ')
+ .map(SDPUtils.parseRtcpFb);
+ description.codecs.push(codec);
+ // parse FEC mechanisms from rtpmap lines.
+ switch (codec.name.toUpperCase()) {
+ case 'RED':
+ case 'ULPFEC':
+ description.fecMechanisms.push(codec.name.toUpperCase());
+ break;
+ default: // only RED and ULPFEC are recognized as FEC mechanisms.
+ break;
+ }
+ }
+ }
+ SDPUtils.matchPrefix(mediaSection, 'a=extmap:').forEach(function(line) {
+ description.headerExtensions.push(SDPUtils.parseExtmap(line));
+ });
+ // FIXME: parse rtcp.
+ return description;
+};
+
+// Generates parts of the SDP media section describing the capabilities /
+// parameters.
+SDPUtils.writeRtpDescription = function(kind, caps) {
+ var sdp = '';
+
+ // Build the mline.
+ sdp += 'm=' + kind + ' ';
+ sdp += caps.codecs.length > 0 ? '9' : '0'; // reject if no codecs.
+ sdp += ' UDP/TLS/RTP/SAVPF ';
+ sdp += caps.codecs.map(function(codec) {
+ if (codec.preferredPayloadType !== undefined) {
+ return codec.preferredPayloadType;
+ }
+ return codec.payloadType;
+ }).join(' ') + '\r\n';
+
+ sdp += 'c=IN IP4 0.0.0.0\r\n';
+ sdp += 'a=rtcp:9 IN IP4 0.0.0.0\r\n';
+
+ // Add a=rtpmap lines for each codec. Also fmtp and rtcp-fb.
+ caps.codecs.forEach(function(codec) {
+ sdp += SDPUtils.writeRtpMap(codec);
+ sdp += SDPUtils.writeFmtp(codec);
+ sdp += SDPUtils.writeRtcpFb(codec);
+ });
+ var maxptime = 0;
+ caps.codecs.forEach(function(codec) {
+ if (codec.maxptime > maxptime) {
+ maxptime = codec.maxptime;
+ }
+ });
+ if (maxptime > 0) {
+ sdp += 'a=maxptime:' + maxptime + '\r\n';
+ }
+ sdp += 'a=rtcp-mux\r\n';
+
+ if (caps.headerExtensions) {
+ caps.headerExtensions.forEach(function(extension) {
+ sdp += SDPUtils.writeExtmap(extension);
+ });
+ }
+ // FIXME: write fecMechanisms.
+ return sdp;
+};
+
+// Parses the SDP media section and returns an array of
+// RTCRtpEncodingParameters.
+SDPUtils.parseRtpEncodingParameters = function(mediaSection) {
+ var encodingParameters = [];
+ var description = SDPUtils.parseRtpParameters(mediaSection);
+ var hasRed = description.fecMechanisms.indexOf('RED') !== -1;
+ var hasUlpfec = description.fecMechanisms.indexOf('ULPFEC') !== -1;
+
+ // filter a=ssrc:... cname:, ignore PlanB-msid
+ var ssrcs = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:')
+ .map(function(line) {
+ return SDPUtils.parseSsrcMedia(line);
+ })
+ .filter(function(parts) {
+ return parts.attribute === 'cname';
+ });
+ var primarySsrc = ssrcs.length > 0 && ssrcs[0].ssrc;
+ var secondarySsrc;
+
+ var flows = SDPUtils.matchPrefix(mediaSection, 'a=ssrc-group:FID')
+ .map(function(line) {
+ var parts = line.substr(17).split(' ');
+ return parts.map(function(part) {
+ return parseInt(part, 10);
+ });
+ });
+ if (flows.length > 0 && flows[0].length > 1 && flows[0][0] === primarySsrc) {
+ secondarySsrc = flows[0][1];
+ }
+
+ description.codecs.forEach(function(codec) {
+ if (codec.name.toUpperCase() === 'RTX' && codec.parameters.apt) {
+ var encParam = {
+ ssrc: primarySsrc,
+ codecPayloadType: parseInt(codec.parameters.apt, 10)
+ };
+ if (primarySsrc && secondarySsrc) {
+ encParam.rtx = {ssrc: secondarySsrc};
+ }
+ encodingParameters.push(encParam);
+ if (hasRed) {
+ encParam = JSON.parse(JSON.stringify(encParam));
+ encParam.fec = {
+ ssrc: primarySsrc,
+ mechanism: hasUlpfec ? 'red+ulpfec' : 'red'
+ };
+ encodingParameters.push(encParam);
+ }
+ }
+ });
+ if (encodingParameters.length === 0 && primarySsrc) {
+ encodingParameters.push({
+ ssrc: primarySsrc
+ });
+ }
+
+ // we support both b=AS and b=TIAS but interpret AS as TIAS.
+ var bandwidth = SDPUtils.matchPrefix(mediaSection, 'b=');
+ if (bandwidth.length) {
+ if (bandwidth[0].indexOf('b=TIAS:') === 0) {
+ bandwidth = parseInt(bandwidth[0].substr(7), 10);
+ } else if (bandwidth[0].indexOf('b=AS:') === 0) {
+ // use formula from JSEP to convert b=AS to TIAS value.
+ bandwidth = parseInt(bandwidth[0].substr(5), 10) * 1000 * 0.95
+ - (50 * 40 * 8);
+ } else {
+ bandwidth = undefined;
+ }
+ encodingParameters.forEach(function(params) {
+ params.maxBitrate = bandwidth;
+ });
+ }
+ return encodingParameters;
+};
+
+// parses http://draft.ortc.org/#rtcrtcpparameters*
+SDPUtils.parseRtcpParameters = function(mediaSection) {
+ var rtcpParameters = {};
+
+ // Gets the first SSRC. Note tha with RTX there might be multiple
+ // SSRCs.
+ var remoteSsrc = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:')
+ .map(function(line) {
+ return SDPUtils.parseSsrcMedia(line);
+ })
+ .filter(function(obj) {
+ return obj.attribute === 'cname';
+ })[0];
+ if (remoteSsrc) {
+ rtcpParameters.cname = remoteSsrc.value;
+ rtcpParameters.ssrc = remoteSsrc.ssrc;
+ }
+
+ // Edge uses the compound attribute instead of reducedSize
+ // compound is !reducedSize
+ var rsize = SDPUtils.matchPrefix(mediaSection, 'a=rtcp-rsize');
+ rtcpParameters.reducedSize = rsize.length > 0;
+ rtcpParameters.compound = rsize.length === 0;
+
+ // parses the rtcp-mux attrŅ–bute.
+ // Note that Edge does not support unmuxed RTCP.
+ var mux = SDPUtils.matchPrefix(mediaSection, 'a=rtcp-mux');
+ rtcpParameters.mux = mux.length > 0;
+
+ return rtcpParameters;
+};
+
+// parses either a=msid: or a=ssrc:... msid lines and returns
+// the id of the MediaStream and MediaStreamTrack.
+SDPUtils.parseMsid = function(mediaSection) {
+ var parts;
+ var spec = SDPUtils.matchPrefix(mediaSection, 'a=msid:');
+ if (spec.length === 1) {
+ parts = spec[0].substr(7).split(' ');
+ return {stream: parts[0], track: parts[1]};
+ }
+ var planB = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:')
+ .map(function(line) {
+ return SDPUtils.parseSsrcMedia(line);
+ })
+ .filter(function(msidParts) {
+ return msidParts.attribute === 'msid';
+ });
+ if (planB.length > 0) {
+ parts = planB[0].value.split(' ');
+ return {stream: parts[0], track: parts[1]};
+ }
+};
+
+// SCTP
+// parses draft-ietf-mmusic-sctp-sdp-26 first and falls back
+// to draft-ietf-mmusic-sctp-sdp-05
+SDPUtils.parseSctpDescription = function(mediaSection) {
+ var mline = SDPUtils.parseMLine(mediaSection);
+ var maxSizeLine = SDPUtils.matchPrefix(mediaSection, 'a=max-message-size:');
+ var maxMessageSize;
+ if (maxSizeLine.length > 0) {
+ maxMessageSize = parseInt(maxSizeLine[0].substr(19), 10);
+ }
+ if (isNaN(maxMessageSize)) {
+ maxMessageSize = 65536;
+ }
+ var sctpPort = SDPUtils.matchPrefix(mediaSection, 'a=sctp-port:');
+ if (sctpPort.length > 0) {
+ return {
+ port: parseInt(sctpPort[0].substr(12), 10),
+ protocol: mline.fmt,
+ maxMessageSize: maxMessageSize
+ };
+ }
+ var sctpMapLines = SDPUtils.matchPrefix(mediaSection, 'a=sctpmap:');
+ if (sctpMapLines.length > 0) {
+ var parts = SDPUtils.matchPrefix(mediaSection, 'a=sctpmap:')[0]
+ .substr(10)
+ .split(' ');
+ return {
+ port: parseInt(parts[0], 10),
+ protocol: parts[1],
+ maxMessageSize: maxMessageSize
+ };
+ }
+};
+
+// SCTP
+// outputs the draft-ietf-mmusic-sctp-sdp-26 version that all browsers
+// support by now receiving in this format, unless we originally parsed
+// as the draft-ietf-mmusic-sctp-sdp-05 format (indicated by the m-line
+// protocol of DTLS/SCTP -- without UDP/ or TCP/)
+SDPUtils.writeSctpDescription = function(media, sctp) {
+ var output = [];
+ if (media.protocol !== 'DTLS/SCTP') {
+ output = [
+ 'm=' + media.kind + ' 9 ' + media.protocol + ' ' + sctp.protocol + '\r\n',
+ 'c=IN IP4 0.0.0.0\r\n',
+ 'a=sctp-port:' + sctp.port + '\r\n'
+ ];
+ } else {
+ output = [
+ 'm=' + media.kind + ' 9 ' + media.protocol + ' ' + sctp.port + '\r\n',
+ 'c=IN IP4 0.0.0.0\r\n',
+ 'a=sctpmap:' + sctp.port + ' ' + sctp.protocol + ' 65535\r\n'
+ ];
+ }
+ if (sctp.maxMessageSize !== undefined) {
+ output.push('a=max-message-size:' + sctp.maxMessageSize + '\r\n');
+ }
+ return output.join('');
+};
+
+// Generate a session ID for SDP.
+// https://tools.ietf.org/html/draft-ietf-rtcweb-jsep-20#section-5.2.1
+// recommends using a cryptographically random +ve 64-bit value
+// but right now this should be acceptable and within the right range
+SDPUtils.generateSessionId = function() {
+ return Math.floor((Math.random() * 4294967296) + 1);
+};
+
+// Write boilder plate for start of SDP
+// sessId argument is optional - if not supplied it will
+// be generated randomly
+// sessVersion is optional and defaults to 2
+// sessUser is optional and defaults to 'thisisadapterortc'
+SDPUtils.writeSessionBoilerplate = function(sessId, sessVer, sessUser) {
+ var sessionId;
+ var version = sessVer !== undefined ? sessVer : 2;
+ if (sessId) {
+ sessionId = sessId;
+ } else {
+ sessionId = SDPUtils.generateSessionId();
+ }
+ var user = sessUser || 'thisisadapterortc';
+ // FIXME: sess-id should be an NTP timestamp.
+ return 'v=0\r\n' +
+ 'o=' + user + ' ' + sessionId + ' ' + version +
+ ' IN IP4 127.0.0.1\r\n' +
+ 's=-\r\n' +
+ 't=0 0\r\n';
+};
+
+SDPUtils.writeMediaSection = function(transceiver, caps, type, stream) {
+ var sdp = SDPUtils.writeRtpDescription(transceiver.kind, caps);
+
+ // Map ICE parameters (ufrag, pwd) to SDP.
+ sdp += SDPUtils.writeIceParameters(
+ transceiver.iceGatherer.getLocalParameters());
+
+ // Map DTLS parameters to SDP.
+ sdp += SDPUtils.writeDtlsParameters(
+ transceiver.dtlsTransport.getLocalParameters(),
+ type === 'offer' ? 'actpass' : 'active');
+
+ sdp += 'a=mid:' + transceiver.mid + '\r\n';
+
+ if (transceiver.direction) {
+ sdp += 'a=' + transceiver.direction + '\r\n';
+ } else if (transceiver.rtpSender && transceiver.rtpReceiver) {
+ sdp += 'a=sendrecv\r\n';
+ } else if (transceiver.rtpSender) {
+ sdp += 'a=sendonly\r\n';
+ } else if (transceiver.rtpReceiver) {
+ sdp += 'a=recvonly\r\n';
+ } else {
+ sdp += 'a=inactive\r\n';
+ }
+
+ if (transceiver.rtpSender) {
+ // spec.
+ var msid = 'msid:' + stream.id + ' ' +
+ transceiver.rtpSender.track.id + '\r\n';
+ sdp += 'a=' + msid;
+
+ // for Chrome.
+ sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].ssrc +
+ ' ' + msid;
+ if (transceiver.sendEncodingParameters[0].rtx) {
+ sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].rtx.ssrc +
+ ' ' + msid;
+ sdp += 'a=ssrc-group:FID ' +
+ transceiver.sendEncodingParameters[0].ssrc + ' ' +
+ transceiver.sendEncodingParameters[0].rtx.ssrc +
+ '\r\n';
+ }
+ }
+ // FIXME: this should be written by writeRtpDescription.
+ sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].ssrc +
+ ' cname:' + SDPUtils.localCName + '\r\n';
+ if (transceiver.rtpSender && transceiver.sendEncodingParameters[0].rtx) {
+ sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].rtx.ssrc +
+ ' cname:' + SDPUtils.localCName + '\r\n';
+ }
+ return sdp;
+};
+
+// Gets the direction from the mediaSection or the sessionpart.
+SDPUtils.getDirection = function(mediaSection, sessionpart) {
+ // Look for sendrecv, sendonly, recvonly, inactive, default to sendrecv.
+ var lines = SDPUtils.splitLines(mediaSection);
+ for (var i = 0; i < lines.length; i++) {
+ switch (lines[i]) {
+ case 'a=sendrecv':
+ case 'a=sendonly':
+ case 'a=recvonly':
+ case 'a=inactive':
+ return lines[i].substr(2);
+ default:
+ // FIXME: What should happen here?
+ }
+ }
+ if (sessionpart) {
+ return SDPUtils.getDirection(sessionpart);
+ }
+ return 'sendrecv';
+};
+
+SDPUtils.getKind = function(mediaSection) {
+ var lines = SDPUtils.splitLines(mediaSection);
+ var mline = lines[0].split(' ');
+ return mline[0].substr(2);
+};
+
+SDPUtils.isRejected = function(mediaSection) {
+ return mediaSection.split(' ', 2)[1] === '0';
+};
+
+SDPUtils.parseMLine = function(mediaSection) {
+ var lines = SDPUtils.splitLines(mediaSection);
+ var parts = lines[0].substr(2).split(' ');
+ return {
+ kind: parts[0],
+ port: parseInt(parts[1], 10),
+ protocol: parts[2],
+ fmt: parts.slice(3).join(' ')
+ };
+};
+
+SDPUtils.parseOLine = function(mediaSection) {
+ var line = SDPUtils.matchPrefix(mediaSection, 'o=')[0];
+ var parts = line.substr(2).split(' ');
+ return {
+ username: parts[0],
+ sessionId: parts[1],
+ sessionVersion: parseInt(parts[2], 10),
+ netType: parts[3],
+ addressType: parts[4],
+ address: parts[5]
+ };
+};
+
+// a very naive interpretation of a valid SDP.
+SDPUtils.isValidSDP = function(blob) {
+ if (typeof blob !== 'string' || blob.length === 0) {
+ return false;
+ }
+ var lines = SDPUtils.splitLines(blob);
+ for (var i = 0; i < lines.length; i++) {
+ if (lines[i].length < 2 || lines[i].charAt(1) !== '=') {
+ return false;
+ }
+ // TODO: check the modifier a bit more.
+ }
+ return true;
+};
+
+// Expose public methods.
+if (typeof module === 'object') {
+ module.exports = SDPUtils;
+}
diff --git a/testing/web-platform/tests/webrtc/toJSON.html b/testing/web-platform/tests/webrtc/toJSON.html
new file mode 100644
index 0000000000..8d71353425
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/toJSON.html
@@ -0,0 +1,48 @@
+<!doctype html>
+<title>WebRTC objects toJSON() methods</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script>
+'use strict';
+// The tests for
+// * RTCSessionDescription.toJSON()
+// * RTCIceCandidate.toJSON()
+// are kept in a single file since they are similar and typically
+// would need to be changed together.
+test(t => {
+ const desc = new RTCSessionDescription({
+ type: 'offer',
+ sdp: 'bogus sdp',
+ });
+ const json = desc.toJSON();
+
+ // Assert that candidates which should be serialized are present.
+ assert_equals(json.type, desc.type);
+ assert_equals(json.sdp, desc.sdp);
+
+ // Assert that no other attributes are present by checking the size.
+ assert_equals(Object.keys(json).length, 2);
+
+}, 'RTCSessionDescription.toJSON serializes only specific attributes');
+
+test(t => {
+ const candidate = new RTCIceCandidate({
+ sdpMLineIndex: 0,
+ sdpMid: '0',
+ candidate: 'candidate:1905690388 1 udp 2113937151 192.168.0.1 58041 typ host',
+ usernameFragment: 'test'
+ });
+ const json = candidate.toJSON();
+
+ // Assert that candidates which should be serialized are present.
+ assert_equals(json.sdpMLineIndex, candidate.sdpMLineIndex);
+ assert_equals(json.sdpMid, candidate.sdpMid);
+ assert_equals(json.candidate, candidate.candidate);
+ assert_equals(json.usernameFragment, candidate.usernameFragment);
+
+ // Assert that no other attributes are present by checking the size.
+ assert_equals(Object.keys(json).length, 4);
+
+}, 'RTCIceCandidate.toJSON serializes only specific attributes');
+
+</script>
diff --git a/testing/web-platform/tests/webrtc/tools/.eslintrc.js b/testing/web-platform/tests/webrtc/tools/.eslintrc.js
new file mode 100644
index 0000000000..321f8e9a25
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/tools/.eslintrc.js
@@ -0,0 +1,154 @@
+module.exports = {
+ rules: {
+ 'no-undef': 1,
+ 'no-unused-vars': 0
+ },
+ plugins: [
+ 'html'
+ ],
+ env: {
+ browser: true,
+ es6: true
+ },
+ globals: {
+ // testharness globals
+ test: true,
+ async_test: true,
+ promise_test: true,
+ IdlArray: true,
+ assert_true: true,
+ assert_false: true,
+ assert_equals: true,
+ assert_not_equals: true,
+ assert_array_equals: true,
+ assert_in_array: true,
+ assert_unreached: true,
+ assert_idl_attribute: true,
+ assert_own_property: true,
+ assert_greater_than: true,
+ assert_less_than: true,
+ assert_greater_than_equal: true,
+ assert_less_than_equal: true,
+ assert_approx_equals: true,
+
+
+ // WebRTC globals
+ RTCPeerConnection: true,
+ RTCRtpSender: true,
+ RTCRtpReceiver: true,
+ RTCRtpTransceiver: true,
+ RTCIceTransport: true,
+ RTCDtlsTransport: true,
+ RTCSctpTransport: true,
+ RTCDataChannel: true,
+ RTCCertificate: true,
+ RTCDTMFSender: true,
+ RTCError: true,
+ RTCTrackEvent: true,
+ RTCPeerConnectionIceEvent: true,
+ RTCDTMFToneChangeEvent: true,
+ RTCDataChannelEvent: true,
+ RTCRtpContributingSource: true,
+ RTCRtpSynchronizationSource: true,
+
+ // dictionary-helper.js
+ assert_unsigned_int_field: true,
+ assert_int_field: true,
+ assert_string_field: true,
+ assert_number_field: true,
+ assert_boolean_field: true,
+ assert_array_field: true,
+ assert_dict_field: true,
+ assert_enum_field: true,
+
+ assert_optional_unsigned_int_field: true,
+ assert_optional_int_field: true,
+ assert_optional_string_field: true,
+ assert_optional_number_field: true,
+ assert_optional_boolean_field: true,
+ assert_optional_array_field: true,
+ assert_optional_dict_field: true,
+ assert_optional_enum_field: true,
+
+ // identity-helper.sub.js
+ parseAssertionResult: true,
+ getIdpDomains: true,
+ assert_rtcerror_rejection: true,
+ hostString: true,
+
+ // RTCConfiguration-helper.js
+ config_test: true,
+
+ // RTCDTMFSender-helper.js
+ createDtmfSender: true,
+ test_tone_change_events: true,
+ getTransceiver: true,
+
+ // RTCPeerConnection-helper.js
+ countLine: true,
+ countAudioLine: true,
+ countVideoLine: true,
+ countApplicationLine: true,
+ similarMediaDescriptions: true,
+ assert_is_session_description: true,
+ isSimilarSessionDescription: true,
+ assert_session_desc_equals: true,
+ assert_session_desc_not_equals: true,
+ generateOffer: true,
+ generateAnswer: true,
+ test_state_change_event: true,
+ test_never_resolve: true,
+ exchangeIceCandidates: true,
+ exchangeOfferAnswer: true,
+ createDataChannelPair: true,
+ awaitMessage: true,
+ blobToArrayBuffer: true,
+ assert_equals_typed_array: true,
+ generateMediaStreamTrack: true,
+ getTrackFromUserMedia: true,
+ getUserMediaTracksAndStreams: true,
+ performOffer: true,
+ Resolver: true,
+
+ // RTCRtpCapabilities-helper.js
+ validateRtpCapabilities: true,
+ validateCodecCapability: true,
+ validateHeaderExtensionCapability: true,
+
+ // RTCRtpParameters-helper.js
+ validateSenderRtpParameters: true,
+ validateReceiverRtpParameters: true,
+ validateRtpParameters: true,
+ validateEncodingParameters: true,
+ validateRtcpParameters: true,
+ validateHeaderExtensionParameters: true,
+ validateCodecParameters: true,
+
+ // RTCStats-helper.js
+ validateStatsReport: true,
+ assert_stats_report_has_stats: true,
+ findStatsFromReport: true,
+ getRequiredStats: true,
+ getStatsById: true,
+ validateIdField: true,
+ validateOptionalIdField: true,
+ validateRtcStats: true,
+ validateRtpStreamStats: true,
+ validateCodecStats: true,
+ validateReceivedRtpStreamStats: true,
+ validateInboundRtpStreamStats: true,
+ validateRemoteInboundRtpStreamStats: true,
+ validateSentRtpStreamStats: true,
+ validateOutboundRtpStreamStats: true,
+ validateRemoteOutboundRtpStreamStats: true,
+ validateContributingSourceStats: true,
+ validatePeerConnectionStats: true,
+ validateMediaStreamStats: true,
+ validateMediaStreamTrackStats: true,
+ validateDataChannelStats: true,
+ validateTransportStats: true,
+ validateIceCandidateStats: true,
+ validateIceCandidatePairStats: true,
+ validateCertificateStats: true,
+ }
+}
diff --git a/testing/web-platform/tests/webrtc/tools/README.md b/testing/web-platform/tests/webrtc/tools/README.md
new file mode 100644
index 0000000000..68bc284fdf
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/tools/README.md
@@ -0,0 +1,14 @@
+WebRTC Tools
+============
+
+This directory contains a simple Node.js project to aid the development of
+WebRTC tests.
+
+## Lint
+
+```bash
+npm run lint
+```
+
+Does basic linting of the JavaScript code. Mainly for catching usage of
+undefined variables.
diff --git a/testing/web-platform/tests/webrtc/tools/codemod-peerconnection-addcleanup b/testing/web-platform/tests/webrtc/tools/codemod-peerconnection-addcleanup
new file mode 100644
index 0000000000..920921d2e4
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/tools/codemod-peerconnection-addcleanup
@@ -0,0 +1,58 @@
+/* a codemod for ensuring RTCPeerConnection is cleaned up in tests.
+ * For each `new RTCPeerConnection` add a
+ * `test.add_cleanup(() => pc.close())`
+ * Only applies in promise_tests if there is no add_cleanup in the
+ * test function body.
+ */
+export default function transformer(file, api) {
+ const j = api.jscodeshift;
+ return j(file.source)
+ // find each RTCPeerConnection constructor
+ .find(j.NewExpression, {callee: {type: 'Identifier', name: 'RTCPeerConnection'}})
+
+ // check it is inside a promise_test
+ .filter(path => {
+ // iterate parentPath until you find a CallExpression
+ let nextPath = path.parentPath;
+ while (nextPath && nextPath.value.type !== 'CallExpression') {
+ nextPath = nextPath.parentPath;
+ }
+ return nextPath && nextPath.value.callee.name === 'promise_test';
+ })
+ // check there is no add_cleanup in the function body
+ .filter(path => {
+ let nextPath = path.parentPath;
+ while (nextPath && nextPath.value.type !== 'CallExpression') {
+ nextPath = nextPath.parentPath;
+ }
+ const body = nextPath.value.arguments[0].body;
+ return j(body).find(j.Identifier, {name: 'add_cleanup'}).length === 0;
+ })
+ .forEach(path => {
+ // iterate parentPath until you find a CallExpression
+ let nextPath = path.parentPath;
+ while (nextPath && nextPath.value.type !== 'CallExpression') {
+ nextPath = nextPath.parentPath;
+ }
+ const declaration = path.parentPath.parentPath.parentPath;
+ const pc = path.parentPath.value.id;
+
+ declaration.insertAfter(
+ j.expressionStatement(
+ j.callExpression(
+ j.memberExpression(
+ nextPath.node.arguments[0].params[0],
+ j.identifier('add_cleanup')
+ ),
+ [j.arrowFunctionExpression([],
+ j.callExpression(
+ j.memberExpression(pc, j.identifier('close'), false),
+ []
+ )
+ )]
+ )
+ )
+ );
+ })
+ .toSource();
+};
diff --git a/testing/web-platform/tests/webrtc/tools/html-codemod.js b/testing/web-platform/tests/webrtc/tools/html-codemod.js
new file mode 100644
index 0000000000..6a31e8c4c6
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/tools/html-codemod.js
@@ -0,0 +1,34 @@
+/*
+ * extract script content from a series of html files, run a
+ * jscodeshift codemod on them and overwrite the original file.
+ *
+ * Usage: node html-codemod.js codemod-file list of files to process
+ */
+const { JSDOM } = require('jsdom');
+const fs = require('fs');
+const {execFileSync} = require('child_process');
+
+const codemod = process.argv[2];
+const filenames = process.argv.slice(3);
+filenames.forEach((filename) => {
+ const originalContent = fs.readFileSync(filename, 'utf-8');
+ const dom = new JSDOM(originalContent);
+ const document = dom.window.document;
+ const scriptTags = document.querySelectorAll('script');
+ const lastTag = scriptTags[scriptTags.length - 1];
+ const script = lastTag.innerHTML;
+ if (!script) {
+ console.log('NO SCRIPT FOUND', filename);
+ return;
+ }
+ const scriptFilename = filename + '.codemod.js';
+ const scriptFile = fs.writeFileSync(scriptFilename, script);
+ // exec jscodeshift
+ const output = execFileSync('./node_modules/.bin/jscodeshift', ['-t', codemod, scriptFilename]);
+ console.log(filename, output.toString()); // output jscodeshift output.
+ // read back file, resubstitute
+ const newScript = fs.readFileSync(scriptFilename, 'utf-8').toString();
+ const modifiedContent = originalContent.split(script).join(newScript);
+ fs.writeFileSync(filename, modifiedContent);
+ fs.unlinkSync(scriptFilename);
+});
diff --git a/testing/web-platform/tests/webrtc/tools/package.json b/testing/web-platform/tests/webrtc/tools/package.json
new file mode 100644
index 0000000000..f26cfcc142
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/tools/package.json
@@ -0,0 +1,16 @@
+{
+ "name": "webrtc-testing-tools",
+ "version": "1.0.0",
+ "description": "Tools for WebRTC testing",
+ "scripts": {
+ "lint": "eslint -c .eslintrc.js ../*.html ../*.js"
+ },
+ "devDependencies": {
+ "eslint": "^7.24.0",
+ "eslint-plugin-html": "^4.0.0",
+ "jscodeshift": "^0.5.1",
+ "jsdom": "^16.5.3"
+ },
+ "license": "BSD",
+ "private": true
+}