summaryrefslogtreecommitdiffstats
path: root/dom/media/webrtc/tests
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
commit36d22d82aa202bb199967e9512281e9a53db42c9 (patch)
tree105e8c98ddea1c1e4784a60a5a6410fa416be2de /dom/media/webrtc/tests
parentInitial commit. (diff)
downloadfirefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz
firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--dom/media/webrtc/tests/crashtests/1770075.html8
-rw-r--r--dom/media/webrtc/tests/crashtests/1789908.html25
-rw-r--r--dom/media/webrtc/tests/crashtests/1799168.html16
-rw-r--r--dom/media/webrtc/tests/crashtests/1816708.html21
-rw-r--r--dom/media/webrtc/tests/crashtests/1821477.html16
-rw-r--r--dom/media/webrtc/tests/crashtests/crashtests.list7
-rw-r--r--dom/media/webrtc/tests/fuzztests/moz.build22
-rw-r--r--dom/media/webrtc/tests/fuzztests/sdp_parser_libfuzz.cpp30
-rw-r--r--dom/media/webrtc/tests/mochitests/NetworkPreparationChromeScript.js43
-rw-r--r--dom/media/webrtc/tests/mochitests/addTurnsSelfsignedCert.js32
-rw-r--r--dom/media/webrtc/tests/mochitests/blacksilence.js134
-rw-r--r--dom/media/webrtc/tests/mochitests/dataChannel.js352
-rw-r--r--dom/media/webrtc/tests/mochitests/head.js1445
-rw-r--r--dom/media/webrtc/tests/mochitests/helpers_from_wpt/sdp.js889
-rw-r--r--dom/media/webrtc/tests/mochitests/iceTestUtils.js302
-rw-r--r--dom/media/webrtc/tests/mochitests/identity/identityPcTest.js79
-rw-r--r--dom/media/webrtc/tests/mochitests/identity/idp-bad.js1
-rw-r--r--dom/media/webrtc/tests/mochitests/identity/idp-min.js24
-rw-r--r--dom/media/webrtc/tests/mochitests/identity/idp-redirect-http-trick.js3
-rw-r--r--dom/media/webrtc/tests/mochitests/identity/idp-redirect-http-trick.js^headers^2
-rw-r--r--dom/media/webrtc/tests/mochitests/identity/idp-redirect-http.js3
-rw-r--r--dom/media/webrtc/tests/mochitests/identity/idp-redirect-http.js^headers^2
-rw-r--r--dom/media/webrtc/tests/mochitests/identity/idp-redirect-https-double.js3
-rw-r--r--dom/media/webrtc/tests/mochitests/identity/idp-redirect-https-double.js^headers^2
-rw-r--r--dom/media/webrtc/tests/mochitests/identity/idp-redirect-https-odd-path.js3
-rw-r--r--dom/media/webrtc/tests/mochitests/identity/idp-redirect-https-odd-path.js^headers^2
-rw-r--r--dom/media/webrtc/tests/mochitests/identity/idp-redirect-https.js3
-rw-r--r--dom/media/webrtc/tests/mochitests/identity/idp-redirect-https.js^headers^2
-rw-r--r--dom/media/webrtc/tests/mochitests/identity/idp.js119
-rw-r--r--dom/media/webrtc/tests/mochitests/identity/idp.sjs18
-rw-r--r--dom/media/webrtc/tests/mochitests/identity/login.html31
-rw-r--r--dom/media/webrtc/tests/mochitests/identity/mochitest.ini47
-rw-r--r--dom/media/webrtc/tests/mochitests/identity/test_fingerprints.html91
-rw-r--r--dom/media/webrtc/tests/mochitests/identity/test_getIdentityAssertion.html101
-rw-r--r--dom/media/webrtc/tests/mochitests/identity/test_idpproxy.html178
-rw-r--r--dom/media/webrtc/tests/mochitests/identity/test_loginNeeded.html72
-rw-r--r--dom/media/webrtc/tests/mochitests/identity/test_peerConnection_asymmetricIsolation.html31
-rw-r--r--dom/media/webrtc/tests/mochitests/identity/test_peerConnection_peerIdentity.html21
-rw-r--r--dom/media/webrtc/tests/mochitests/identity/test_setIdentityProvider.html67
-rw-r--r--dom/media/webrtc/tests/mochitests/identity/test_setIdentityProviderWithErrors.html57
-rw-r--r--dom/media/webrtc/tests/mochitests/mediaStreamPlayback.js241
-rw-r--r--dom/media/webrtc/tests/mochitests/mochitest.ini65
-rw-r--r--dom/media/webrtc/tests/mochitests/mochitest_datachannel.ini52
-rw-r--r--dom/media/webrtc/tests/mochitests/mochitest_getusermedia.ini105
-rw-r--r--dom/media/webrtc/tests/mochitests/mochitest_peerconnection.ini311
-rw-r--r--dom/media/webrtc/tests/mochitests/network.js16
-rw-r--r--dom/media/webrtc/tests/mochitests/nonTrickleIce.js97
-rw-r--r--dom/media/webrtc/tests/mochitests/parser_rtp.js131
-rw-r--r--dom/media/webrtc/tests/mochitests/pc.js2495
-rw-r--r--dom/media/webrtc/tests/mochitests/peerconnection_audio_forced_sample_rate.js32
-rw-r--r--dom/media/webrtc/tests/mochitests/sdpUtils.js398
-rw-r--r--dom/media/webrtc/tests/mochitests/simulcast.js232
-rw-r--r--dom/media/webrtc/tests/mochitests/stats.js1596
-rw-r--r--dom/media/webrtc/tests/mochitests/templates.js615
-rw-r--r--dom/media/webrtc/tests/mochitests/test_1488832.html37
-rw-r--r--dom/media/webrtc/tests/mochitests/test_1717318.html26
-rw-r--r--dom/media/webrtc/tests/mochitests/test_a_noOp.html32
-rw-r--r--dom/media/webrtc/tests/mochitests/test_dataChannel_basicAudio.html25
-rw-r--r--dom/media/webrtc/tests/mochitests/test_dataChannel_basicAudioVideo.html26
-rw-r--r--dom/media/webrtc/tests/mochitests/test_dataChannel_basicAudioVideoCombined.html26
-rw-r--r--dom/media/webrtc/tests/mochitests/test_dataChannel_basicAudioVideoNoBundle.html27
-rw-r--r--dom/media/webrtc/tests/mochitests/test_dataChannel_basicDataOnly.html24
-rw-r--r--dom/media/webrtc/tests/mochitests/test_dataChannel_basicVideo.html25
-rw-r--r--dom/media/webrtc/tests/mochitests/test_dataChannel_bug1013809.html27
-rw-r--r--dom/media/webrtc/tests/mochitests/test_dataChannel_dataOnlyBufferedAmountLow.html25
-rw-r--r--dom/media/webrtc/tests/mochitests/test_dataChannel_dtlsVersions.html38
-rw-r--r--dom/media/webrtc/tests/mochitests/test_dataChannel_hostnameObfuscation.html59
-rw-r--r--dom/media/webrtc/tests/mochitests/test_dataChannel_noOffer.html33
-rw-r--r--dom/media/webrtc/tests/mochitests/test_dataChannel_stats.html50
-rw-r--r--dom/media/webrtc/tests/mochitests/test_defaultAudioConstraints.html80
-rw-r--r--dom/media/webrtc/tests/mochitests/test_enumerateDevices.html141
-rw-r--r--dom/media/webrtc/tests/mochitests/test_enumerateDevices_getUserMediaFake.html63
-rw-r--r--dom/media/webrtc/tests/mochitests/test_enumerateDevices_iframe.html28
-rw-r--r--dom/media/webrtc/tests/mochitests/test_enumerateDevices_iframe_pre_gum.html22
-rw-r--r--dom/media/webrtc/tests/mochitests/test_enumerateDevices_legacy.html147
-rw-r--r--dom/media/webrtc/tests/mochitests/test_enumerateDevices_navigation.html54
-rw-r--r--dom/media/webrtc/tests/mochitests/test_fingerprinting_resistance.html112
-rw-r--r--dom/media/webrtc/tests/mochitests/test_forceSampleRate.html23
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_GC_MediaStream.html59
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_active_autoplay.html61
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_addTrackRemoveTrack.html169
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_addtrack_removetrack_events.html110
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_audioCapture.html104
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_audioConstraints.html93
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_audioConstraints_concurrentIframes.html157
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_audioConstraints_concurrentStreams.html123
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_basicAudio.html27
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_basicAudio_loopback.html99
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_basicScreenshare.html260
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_basicTabshare.html67
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_basicVideo.html30
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_basicVideoAudio.html30
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_basicVideo_playAfterLoadedmetadata.html42
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_basicWindowshare.html39
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_bug1223696.html54
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_callbacks.html35
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_constraints.html166
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_cubebDisabled.html42
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_cubebDisabledFakeStreams.html43
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_getTrackById.html50
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_gumWithinGum.html38
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_loadedmetadata.html39
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_mediaElementCapture_audio.html116
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_mediaElementCapture_tracks.html179
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_mediaElementCapture_video.html91
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_mediaStreamClone.html258
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_mediaStreamConstructors.html171
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_mediaStreamTrackClone.html170
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_nonDefaultRate.html37
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_peerIdentity.html51
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_permission.html104
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_permission_iframe.html30
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_playAudioTwice.html25
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_playVideoAudioTwice.html26
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_playVideoTwice.html26
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_scarySources.html51
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_spinEventLoop.html28
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_trackCloneCleanup.html32
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_trackEnded.html68
-rw-r--r--dom/media/webrtc/tests/mochitests/test_groupId.html53
-rw-r--r--dom/media/webrtc/tests/mochitests/test_multi_mics.html66
-rw-r--r--dom/media/webrtc/tests/mochitests/test_ondevicechange.html180
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_addAudioTrackToExistingVideoStream.html55
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_addDataChannel.html33
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_addDataChannelNoBundle.html44
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_addSecondAudioStream.html45
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_addSecondAudioStreamNoBundle.html53
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_addSecondVideoStream.html53
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_addSecondVideoStreamNoBundle.html60
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_addtrack_removetrack_events.html75
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_answererAddSecondAudioStream.html32
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_audioChannels.html102
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_audioCodecs.html81
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_audioContributingSources.html144
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_audioRenegotiationInactiveAnswer.html69
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_audioSynchronizationSources.html95
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_audioSynchronizationSourcesUnidirectional.html54
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudio.html25
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioDynamicPtMissingRtpmap.html36
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNATRelay.html47
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNATRelayTCP.html42
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNATRelayTCPWithStun300.html54
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNATRelayTLS.html41
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNATRelayWithStun300.html53
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNATSrflx.html44
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNoisyUDPBlock.html41
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioPcmaPcmuOnly.html39
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioRelayPolicy.html83
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioRequireEOC.html35
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVerifyRtpHeaderExtensions.html63
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideo.html24
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoCombined.html24
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoNoBundle.html25
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoNoBundleNoRtcpMux.html39
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoNoRtcpMux.html38
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoTransceivers.html31
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoVerifyExtmap.html97
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoVerifyExtmapSendonly.html97
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoVerifyTooLongMidFails.html47
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudio_forced_higher_rate.html19
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudio_forced_lower_rate.html19
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_basicH264Video.html26
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_basicScreenshare.html56
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_basicVideo.html23
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_basicVideoVerifyRtpHeaderExtensions.html82
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_basicWindowshare.html25
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_bug1013809.html25
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_bug1042791.html36
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_bug1227781.html37
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_bug1512281.html47
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_bug1773067.html32
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_bug822674.html26
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_bug825703.html140
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_bug827843.html50
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_bug834153.html36
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_callbacks.html86
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_captureStream_canvas_2d.html81
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_captureStream_canvas_2d_noSSRC.html83
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_captureStream_canvas_webgl.html130
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_capturedVideo.html81
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_certificates.html185
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_checkPacketDumpHook.html107
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_close.html134
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_closeDuringIce.html79
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_codecNegotiationFailure.html111
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_constructedStream.html67
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_disabledVideoPreNegotiation.html45
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_encodingsNegotiation.html85
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_errorCallbacks.html55
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_extmapRenegotiation.html325
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_forwarding_basicAudioVideoCombined.html41
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_gatherWithSetConfiguration.html450
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_gatherWithStun300.html269
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_gatherWithStun300IPv6.html283
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_glean.html488
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_iceFailure.html84
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_insertDTMF.html76
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_localReofferRollback.html44
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_localRollback.html47
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_maxFsConstraint.html112
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_multiple_captureStream_canvas_2d.html115
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_noTrickleAnswer.html25
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_noTrickleOffer.html25
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_noTrickleOfferAnswer.html26
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_nonDefaultRate.html200
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_offerRequiresReceiveAudio.html23
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_offerRequiresReceiveVideo.html23
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_offerRequiresReceiveVideoAudio.html23
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_portRestrictions.html63
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_promiseSendOnly.html61
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_recordReceiveTrack.html101
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_relayOnly.html60
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_remoteReofferRollback.html50
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_remoteRollback.html51
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_removeAudioTrack.html57
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_removeThenAddAudioTrack.html87
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_removeThenAddAudioTrackNoBundle.html76
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_removeThenAddVideoTrack.html98
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_removeThenAddVideoTrackNoBundle.html89
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_removeVideoTrack.html64
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_renderAfterRenegotiation.html89
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_replaceNullTrackThenRenegotiateAudio.html53
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_replaceNullTrackThenRenegotiateVideo.html63
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_replaceTrack.html187
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_replaceTrack_camera.html48
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_replaceTrack_disabled.html60
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_replaceTrack_microphone.html46
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_replaceVideoThenRenegotiate.html74
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_restartIce.html41
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceBadAnswer.html58
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceLocalAndRemoteRollback.html82
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceLocalAndRemoteRollbackNoSubsequentRestart.html77
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceLocalRollback.html76
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceLocalRollbackNoSubsequentRestart.html60
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceNoBundle.html43
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceNoBundleNoRtcpMux.html44
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceNoRtcpMux.html43
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_restrictBandwidthTargetBitrate.html29
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_restrictBandwidthWithTias.html30
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_rtcp_rsize.html81
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_scaleResolution.html119
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_scaleResolution_oldSetParameters.html122
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_sender_and_receiver_stats.html73
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_setLocalAnswerInHaveLocalOffer.html34
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_setLocalAnswerInStable.html34
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_setLocalOfferInHaveRemoteOffer.html31
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_setParameters.html470
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_setParameters_maxFramerate.html63
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_setParameters_maxFramerate_oldSetParameters.html60
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_setParameters_oldSetParameters.html86
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_setParameters_scaleResolutionDownBy.html98
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_setParameters_scaleResolutionDownBy_oldSetParameters.html96
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_setRemoteAnswerInHaveRemoteOffer.html34
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_setRemoteAnswerInStable.html34
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_setRemoteOfferInHaveLocalOffer.html37
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastAnswer.html121
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastAnswer_lowResFirst.html113
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastAnswer_lowResFirst_oldSetParameters.html115
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastAnswer_oldSetParameters.html115
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastOddResolution.html183
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastOddResolution_oldSetParameters.html172
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastOffer.html109
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastOffer_lowResFirst.html109
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastOffer_lowResFirst_oldSetParameters.html112
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastOffer_oldSetParameters.html112
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_stats.html42
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_stats_jitter.html58
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_stats_oneway.html65
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_stats_relayProtocol.html58
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_stereoFmtpPref.html61
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_syncSetDescription.html53
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_telephoneEventFirst.html56
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_threeUnbundledConnections.html134
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_throwInCallbacks.html83
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_toJSON.html39
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_trackDisabling.html108
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_trackDisabling_clones.html162
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_trackless_sender_stats.html56
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_twoAudioStreams.html23
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_twoAudioTracksInOneStream.html37
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_twoAudioVideoStreams.html26
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_twoAudioVideoStreamsCombined.html70
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_twoAudioVideoStreamsCombinedNoBundle.html107
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_twoVideoStreams.html23
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_twoVideoTracksInOneStream.html37
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_verifyAudioAfterRenegotiation.html99
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_verifyDescriptions.html58
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_verifyVideoAfterRenegotiation.html123
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_videoCodecs.html142
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_videoRenegotiationInactiveAnswer.html95
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_webAudio.html43
-rw-r--r--dom/media/webrtc/tests/mochitests/test_selftest.html37
-rw-r--r--dom/media/webrtc/tests/mochitests/test_setSinkId.html83
-rw-r--r--dom/media/webrtc/tests/mochitests/test_setSinkId_default_addTrack.html52
-rw-r--r--dom/media/webrtc/tests/mochitests/test_setSinkId_preMutedElement.html100
-rw-r--r--dom/media/webrtc/tests/mochitests/test_unfocused_pref.html49
-rw-r--r--dom/media/webrtc/tests/mochitests/turnConfig.js16
297 files changed, 29234 insertions, 0 deletions
diff --git a/dom/media/webrtc/tests/crashtests/1770075.html b/dom/media/webrtc/tests/crashtests/1770075.html
new file mode 100644
index 0000000000..4d451216bd
--- /dev/null
+++ b/dom/media/webrtc/tests/crashtests/1770075.html
@@ -0,0 +1,8 @@
+<script>
+window.addEventListener('load', () => {
+ let a = new RTCPeerConnection({}, {})
+ a.createOffer({'offerToReceiveVideo': true})
+ let b = new WeakRef(a.getTransceivers()[0])
+ setTimeout("self.close()", 200)
+})
+</script>
diff --git a/dom/media/webrtc/tests/crashtests/1789908.html b/dom/media/webrtc/tests/crashtests/1789908.html
new file mode 100644
index 0000000000..3d58d3dc6b
--- /dev/null
+++ b/dom/media/webrtc/tests/crashtests/1789908.html
@@ -0,0 +1,25 @@
+<script>
+window.addEventListener('load', () => {
+ const sdp = `v=0
+o=mozilla...THIS_IS_SDPARTA-99.0 4978061689314146455 0 IN IP4 0.0.0.0
+s=-
+t=0 0
+a=fingerprint:sha-256 1D:E5:0C:97:18:43:38:3D:FF:7D:6A:BF:E3:AC:CA:70:AB:53:5A:35:95:92:4F:98:86:61:CA:5D:D5:9D:5E:41
+a=group:BUNDLE 0
+a=ice-options:trickle
+a=msid-semantic:WMS *
+m=video 9 UDP/TLS/RTP/SAVPF 120
+c=IN IP4 0.0.0.0
+a=fmtp:120 max-fs=12288;max-fr=60
+a=ice-pwd:c3a5e05023a8c38f671aef91ed1802d6
+a=ice-ufrag:91e4526d
+a=setup:actpass
+
+a=rtpmap:120 VP8/90000
+`;
+
+ let a = new RTCPeerConnection()
+ a.setRemoteDescription({sdp, type: "offer"});
+ setTimeout("self.close()", 200)
+})
+</script>
diff --git a/dom/media/webrtc/tests/crashtests/1799168.html b/dom/media/webrtc/tests/crashtests/1799168.html
new file mode 100644
index 0000000000..6c5c9db237
--- /dev/null
+++ b/dom/media/webrtc/tests/crashtests/1799168.html
@@ -0,0 +1,16 @@
+<script>
+window.addEventListener('load', async () => {
+ const offerer = new RTCPeerConnection();
+ const answerer = new RTCPeerConnection();
+ offerer.addTransceiver('audio');
+ await offerer.setLocalDescription();
+ await answerer.setRemoteDescription(offerer.localDescription);
+ const answer = await answerer.createAnswer();
+ await offerer.setRemoteDescription(answer);
+ // relay candidate with TCP!
+ const candidate = 'candidate:3 1 tcp 18087935 20.253.151.225 3478 typ relay raddr 10.0.48.153 rport 3478 tcptype passive';
+ await offerer.addIceCandidate({candidate, sdpMLineIndex: 0});
+ await new Promise(r => setTimeout(r, 2000));
+ self.close();
+})
+</script>
diff --git a/dom/media/webrtc/tests/crashtests/1816708.html b/dom/media/webrtc/tests/crashtests/1816708.html
new file mode 100644
index 0000000000..c7ba824041
--- /dev/null
+++ b/dom/media/webrtc/tests/crashtests/1816708.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+<script>
+document.addEventListener('DOMContentLoaded', async () => {
+ const peer = new RTCPeerConnection()
+ const stream = await navigator.mediaDevices.getUserMedia({
+ video: true,
+ audio: true,
+ fake: true,
+ peerIdentity: 'name',
+ })
+ stream.getTracks().forEach((track) => peer.addTrack(track, stream))
+ const offer = await peer.createOffer({})
+ await peer.setLocalDescription(offer)
+ await peer.setRemoteDescription(offer)
+ document.documentElement.removeAttribute("class");
+})
+</script>
+</head>
+</html>
diff --git a/dom/media/webrtc/tests/crashtests/1821477.html b/dom/media/webrtc/tests/crashtests/1821477.html
new file mode 100644
index 0000000000..c37bd6bd02
--- /dev/null
+++ b/dom/media/webrtc/tests/crashtests/1821477.html
@@ -0,0 +1,16 @@
+<html class="reftest-wait">
+<script>
+document.addEventListener("DOMContentLoaded", async () => {
+ SpecialPowers.wrap(document).notifyUserGestureActivation();
+ try {
+ (await navigator.mediaDevices.getDisplayMedia({
+ "video": {
+ "frameRate": 2147483647,
+ },
+ })).stop();
+ } finally {
+ document.documentElement.removeAttribute("class");
+ }
+});
+</script>
+</html>
diff --git a/dom/media/webrtc/tests/crashtests/crashtests.list b/dom/media/webrtc/tests/crashtests/crashtests.list
new file mode 100644
index 0000000000..64434b28e2
--- /dev/null
+++ b/dom/media/webrtc/tests/crashtests/crashtests.list
@@ -0,0 +1,7 @@
+defaults pref(media.navigator.permission.disabled,true) pref(media.devices.insecure.enabled,true) pref(media.getusermedia.insecure.enabled,true)
+
+load 1770075.html
+load 1789908.html
+load 1799168.html
+load 1816708.html
+skip-if(/^Windows\x20NT\x206\.1/.test(http.oscpu)) load 1821477.html
diff --git a/dom/media/webrtc/tests/fuzztests/moz.build b/dom/media/webrtc/tests/fuzztests/moz.build
new file mode 100644
index 0000000000..fef388e6c9
--- /dev/null
+++ b/dom/media/webrtc/tests/fuzztests/moz.build
@@ -0,0 +1,22 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+Library("FuzzingSdp")
+
+LOCAL_INCLUDES += [
+ "/dom/media/webrtc",
+ "/ipc/chromium/src",
+ "/media/webrtc",
+]
+
+# Add libFuzzer configuration directives
+include("/tools/fuzzing/libfuzzer-config.mozbuild")
+
+SOURCES += [
+ "sdp_parser_libfuzz.cpp",
+]
+
+FINAL_LIBRARY = "xul-gtest"
diff --git a/dom/media/webrtc/tests/fuzztests/sdp_parser_libfuzz.cpp b/dom/media/webrtc/tests/fuzztests/sdp_parser_libfuzz.cpp
new file mode 100644
index 0000000000..3451d6fd21
--- /dev/null
+++ b/dom/media/webrtc/tests/fuzztests/sdp_parser_libfuzz.cpp
@@ -0,0 +1,30 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <string>
+
+#include "gtest/gtest.h"
+
+#include "FuzzingInterface.h"
+
+#include "sdp/SipccSdpParser.h"
+
+using namespace mozilla;
+
+static mozilla::UniquePtr<SdpParser::Results> sdpPtr;
+static SipccSdpParser mParser;
+
+int FuzzingInitSdpParser(int* argc, char*** argv) { return 0; }
+
+static int RunSdpParserFuzzing(const uint8_t* data, size_t size) {
+ std::string message(reinterpret_cast<const char*>(data), size);
+
+ sdpPtr = mParser.Parse(message);
+
+ return 0;
+}
+
+MOZ_FUZZING_INTERFACE_RAW(FuzzingInitSdpParser, RunSdpParserFuzzing, SdpParser);
diff --git a/dom/media/webrtc/tests/mochitests/NetworkPreparationChromeScript.js b/dom/media/webrtc/tests/mochitests/NetworkPreparationChromeScript.js
new file mode 100644
index 0000000000..d3872f1519
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/NetworkPreparationChromeScript.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var browser = Services.wm.getMostRecentWindow("navigator:browser");
+var connection = browser.navigator.mozMobileConnections[0];
+
+// provide a fake APN and enable data connection.
+// enable 3G radio
+function enableRadio() {
+ if (connection.radioState !== "enabled") {
+ connection.setRadioEnabled(true);
+ }
+}
+
+// disable 3G radio
+function disableRadio() {
+ if (connection.radioState === "enabled") {
+ connection.setRadioEnabled(false);
+ }
+}
+
+addMessageListener("prepare-network", function (message) {
+ connection.addEventListener("datachange", function onDataChange() {
+ if (connection.data.connected) {
+ connection.removeEventListener("datachange", onDataChange);
+ Services.prefs.setIntPref("network.proxy.type", 2);
+ sendAsyncMessage("network-ready", true);
+ }
+ });
+
+ enableRadio();
+});
+
+addMessageListener("network-cleanup", function (message) {
+ connection.addEventListener("datachange", function onDataChange() {
+ if (!connection.data.connected) {
+ connection.removeEventListener("datachange", onDataChange);
+ Services.prefs.setIntPref("network.proxy.type", 2);
+ sendAsyncMessage("network-disabled", true);
+ }
+ });
+ disableRadio();
+});
diff --git a/dom/media/webrtc/tests/mochitests/addTurnsSelfsignedCert.js b/dom/media/webrtc/tests/mochitests/addTurnsSelfsignedCert.js
new file mode 100644
index 0000000000..1e8be3a397
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/addTurnsSelfsignedCert.js
@@ -0,0 +1,32 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* eslint-env mozilla/chrome-script */
+
+"use strict";
+
+// This is only usable from the parent process, even for doing simple stuff like
+// serializing a cert.
+var gCertMaker = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+);
+
+var gCertOverrides = Cc["@mozilla.org/security/certoverride;1"].getService(
+ Ci.nsICertOverrideService
+);
+
+addMessageListener("add-turns-certs", certs => {
+ var port = 5349;
+ certs.forEach(certDescription => {
+ var cert = gCertMaker.constructX509FromBase64(certDescription.cert);
+ gCertOverrides.rememberValidityOverride(
+ certDescription.hostname,
+ port,
+ {},
+ cert,
+ false
+ );
+ });
+ sendAsyncMessage("certs-added");
+});
diff --git a/dom/media/webrtc/tests/mochitests/blacksilence.js b/dom/media/webrtc/tests/mochitests/blacksilence.js
new file mode 100644
index 0000000000..5ea35f8a7f
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/blacksilence.js
@@ -0,0 +1,134 @@
+(function (global) {
+ "use strict";
+
+ // an invertible check on the condition.
+ // if the constraint is applied, then the check is direct
+ // if not applied, then the result should be reversed
+ function check(constraintApplied, condition, message) {
+ var good = constraintApplied ? condition : !condition;
+ message =
+ (constraintApplied ? "with" : "without") +
+ " constraint: should " +
+ (constraintApplied ? "" : "not ") +
+ message +
+ " = " +
+ (good ? "OK" : "waiting...");
+ info(message);
+ return good;
+ }
+
+ function mkElement(type) {
+ // This makes an unattached element.
+ // It's not rendered to save the cycles that costs on b2g emulator
+ // and it gets dropped (and GC'd) when the test is done.
+ var e = document.createElement(type);
+ e.width = 32;
+ e.height = 24;
+ document.getElementById("display").appendChild(e);
+ return e;
+ }
+
+ // Runs checkFunc until it reports success.
+ // This is kludgy, but you have to wait for media to start flowing, and it
+ // can't be any old media, it has to include real data, for which we have no
+ // reliable signals to use as a trigger.
+ function periodicCheck(checkFunc) {
+ var resolve;
+ var done = false;
+ // This returns a function so that we create 10 closures in the loop, not
+ // one; and so that the timers don't all start straight away
+ var waitAndCheck = counter => () => {
+ if (done) {
+ return Promise.resolve();
+ }
+ return new Promise(r => setTimeout(r, 200 << counter)).then(() => {
+ if (checkFunc()) {
+ done = true;
+ resolve();
+ }
+ });
+ };
+
+ var chain = Promise.resolve();
+ for (var i = 0; i < 10; ++i) {
+ chain = chain.then(waitAndCheck(i));
+ }
+ return new Promise(r => (resolve = r));
+ }
+
+ function isSilence(audioData) {
+ var silence = true;
+ for (var i = 0; i < audioData.length; ++i) {
+ if (audioData[i] !== 128) {
+ silence = false;
+ }
+ }
+ return silence;
+ }
+
+ function checkAudio(constraintApplied, stream) {
+ var audio = mkElement("audio");
+ audio.srcObject = stream;
+ audio.play();
+
+ var context = new AudioContext();
+ var source = context.createMediaStreamSource(stream);
+ var analyser = context.createAnalyser();
+ source.connect(analyser);
+ analyser.connect(context.destination);
+
+ return periodicCheck(() => {
+ var sampleCount = analyser.frequencyBinCount;
+ info("got some audio samples: " + sampleCount);
+ var buffer = new Uint8Array(sampleCount);
+ analyser.getByteTimeDomainData(buffer);
+
+ var silent = check(
+ constraintApplied,
+ isSilence(buffer),
+ "be silence for audio"
+ );
+ return sampleCount > 0 && silent;
+ }).then(() => {
+ source.disconnect();
+ analyser.disconnect();
+ audio.pause();
+ ok(true, "audio is " + (constraintApplied ? "" : "not ") + "silent");
+ });
+ }
+
+ function checkVideo(constraintApplied, stream) {
+ var video = mkElement("video");
+ video.srcObject = stream;
+ video.play();
+
+ return periodicCheck(() => {
+ try {
+ var canvas = mkElement("canvas");
+ var ctx = canvas.getContext("2d");
+ // Have to guard drawImage with the try as well, due to bug 879717. If
+ // we get an error, this round fails, but that failure is usually just
+ // transitory.
+ ctx.drawImage(video, 0, 0);
+ ctx.getImageData(0, 0, 1, 1);
+ return check(
+ constraintApplied,
+ false,
+ "throw on getImageData for video"
+ );
+ } catch (e) {
+ return check(
+ constraintApplied,
+ e.name === "SecurityError",
+ "get a security error: " + e.name
+ );
+ }
+ }).then(() => {
+ video.pause();
+ ok(true, "video is " + (constraintApplied ? "" : "not ") + "protected");
+ });
+ }
+
+ global.audioIsSilence = checkAudio;
+ global.videoIsBlack = checkVideo;
+})(this);
diff --git a/dom/media/webrtc/tests/mochitests/dataChannel.js b/dom/media/webrtc/tests/mochitests/dataChannel.js
new file mode 100644
index 0000000000..eac52f96ab
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/dataChannel.js
@@ -0,0 +1,352 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Returns the contents of a blob as text
+ *
+ * @param {Blob} blob
+ The blob to retrieve the contents from
+ */
+function getBlobContent(blob) {
+ return new Promise(resolve => {
+ var reader = new FileReader();
+ // Listen for 'onloadend' which will always be called after a success or failure
+ reader.onloadend = event => resolve(event.target.result);
+ reader.readAsText(blob);
+ });
+}
+
+var commandsCreateDataChannel = [
+ function PC_REMOTE_EXPECT_DATA_CHANNEL(test) {
+ test.pcRemote.expectDataChannel();
+ },
+
+ function PC_LOCAL_CREATE_DATA_CHANNEL(test) {
+ var channel = test.pcLocal.createDataChannel({});
+ is(channel.binaryType, "blob", channel + " is of binary type 'blob'");
+
+ is(
+ test.pcLocal.signalingState,
+ STABLE,
+ "Create datachannel does not change signaling state"
+ );
+ return test.pcLocal.observedNegotiationNeeded;
+ },
+];
+
+var commandsWaitForDataChannel = [
+ function PC_LOCAL_VERIFY_DATA_CHANNEL_STATE(test) {
+ return test.pcLocal.dataChannels[0].opened;
+ },
+
+ function PC_REMOTE_VERIFY_DATA_CHANNEL_STATE(test) {
+ return test.pcRemote.nextDataChannel.then(channel => channel.opened);
+ },
+];
+
+var commandsCheckDataChannel = [
+ function SEND_MESSAGE(test) {
+ var message = "Lorem ipsum dolor sit amet";
+
+ info("Sending message:" + message);
+ return test.send(message).then(result => {
+ is(
+ result.data,
+ message,
+ "Message correctly transmitted from pcLocal to pcRemote."
+ );
+ });
+ },
+
+ function SEND_BLOB(test) {
+ var contents = "At vero eos et accusam et justo duo dolores et ea rebum.";
+ var blob = new Blob([contents], { type: "text/plain" });
+
+ info("Sending blob");
+ return test
+ .send(blob)
+ .then(result => {
+ ok(result.data instanceof Blob, "Received data is of instance Blob");
+ is(result.data.size, blob.size, "Received data has the correct size.");
+
+ return getBlobContent(result.data);
+ })
+ .then(recv_contents =>
+ is(recv_contents, contents, "Received data has the correct content.")
+ );
+ },
+
+ function CREATE_SECOND_DATA_CHANNEL(test) {
+ return test.createDataChannel({}).then(result => {
+ is(
+ result.remote.binaryType,
+ "blob",
+ "remote data channel is of binary type 'blob'"
+ );
+ });
+ },
+
+ function SEND_MESSAGE_THROUGH_LAST_OPENED_CHANNEL(test) {
+ var channels = test.pcRemote.dataChannels;
+ var message = "I am the Omega";
+
+ info("Sending message:" + message);
+ return test.send(message).then(result => {
+ is(
+ channels.indexOf(result.channel),
+ channels.length - 1,
+ "Last channel used"
+ );
+ is(result.data, message, "Received message has the correct content.");
+ });
+ },
+
+ function SEND_MESSAGE_THROUGH_FIRST_CHANNEL(test) {
+ var message = "Message through 1st channel";
+ var options = {
+ sourceChannel: test.pcLocal.dataChannels[0],
+ targetChannel: test.pcRemote.dataChannels[0],
+ };
+
+ info("Sending message:" + message);
+ return test.send(message, options).then(result => {
+ is(
+ test.pcRemote.dataChannels.indexOf(result.channel),
+ 0,
+ "1st channel used"
+ );
+ is(result.data, message, "Received message has the correct content.");
+ });
+ },
+
+ function SEND_MESSAGE_BACK_THROUGH_FIRST_CHANNEL(test) {
+ var message = "Return a message also through 1st channel";
+ var options = {
+ sourceChannel: test.pcRemote.dataChannels[0],
+ targetChannel: test.pcLocal.dataChannels[0],
+ };
+
+ info("Sending message:" + message);
+ return test.send(message, options).then(result => {
+ is(
+ test.pcLocal.dataChannels.indexOf(result.channel),
+ 0,
+ "1st channel used"
+ );
+ is(result.data, message, "Return message has the correct content.");
+ });
+ },
+
+ function CREATE_NEGOTIATED_DATA_CHANNEL_MAX_RETRANSMITS(test) {
+ var options = {
+ negotiated: true,
+ id: 5,
+ protocol: "foo/bar",
+ ordered: false,
+ maxRetransmits: 500,
+ };
+ return test.createDataChannel(options).then(result => {
+ is(
+ result.local.binaryType,
+ "blob",
+ result.remote + " is of binary type 'blob'"
+ );
+ is(
+ result.local.id,
+ options.id,
+ result.local + " id is:" + result.local.id
+ );
+ is(
+ result.local.protocol,
+ options.protocol,
+ result.local + " protocol is:" + result.local.protocol
+ );
+ is(
+ result.local.reliable,
+ false,
+ result.local + " reliable is:" + result.local.reliable
+ );
+ is(
+ result.local.ordered,
+ options.ordered,
+ result.local + " ordered is:" + result.local.ordered
+ );
+ is(
+ result.local.maxRetransmits,
+ options.maxRetransmits,
+ result.local + " maxRetransmits is:" + result.local.maxRetransmits
+ );
+ is(
+ result.local.maxPacketLifeTime,
+ null,
+ result.local + " maxPacketLifeTime is:" + result.local.maxPacketLifeTime
+ );
+
+ is(
+ result.remote.binaryType,
+ "blob",
+ result.remote + " is of binary type 'blob'"
+ );
+ is(
+ result.remote.id,
+ options.id,
+ result.remote + " id is:" + result.remote.id
+ );
+ is(
+ result.remote.protocol,
+ options.protocol,
+ result.remote + " protocol is:" + result.remote.protocol
+ );
+ is(
+ result.remote.reliable,
+ false,
+ result.remote + " reliable is:" + result.remote.reliable
+ );
+ is(
+ result.remote.ordered,
+ options.ordered,
+ result.remote + " ordered is:" + result.remote.ordered
+ );
+ is(
+ result.remote.maxRetransmits,
+ options.maxRetransmits,
+ result.remote + " maxRetransmits is:" + result.remote.maxRetransmits
+ );
+ is(
+ result.remote.maxPacketLifeTime,
+ null,
+ result.remote +
+ " maxPacketLifeTime is:" +
+ result.remote.maxPacketLifeTime
+ );
+ });
+ },
+
+ function SEND_MESSAGE_THROUGH_LAST_OPENED_CHANNEL2(test) {
+ var channels = test.pcRemote.dataChannels;
+ var message = "I am the walrus; Goo goo g'joob";
+
+ info("Sending message:" + message);
+ return test.send(message).then(result => {
+ is(
+ channels.indexOf(result.channel),
+ channels.length - 1,
+ "Last channel used"
+ );
+ is(result.data, message, "Received message has the correct content.");
+ });
+ },
+
+ function CREATE_NEGOTIATED_DATA_CHANNEL_MAX_PACKET_LIFE_TIME(test) {
+ var options = {
+ ordered: false,
+ maxPacketLifeTime: 10,
+ };
+ return test.createDataChannel(options).then(result => {
+ is(
+ result.local.binaryType,
+ "blob",
+ result.local + " is of binary type 'blob'"
+ );
+ is(
+ result.local.protocol,
+ "",
+ result.local + " protocol is:" + result.local.protocol
+ );
+ is(
+ result.local.reliable,
+ false,
+ result.local + " reliable is:" + result.local.reliable
+ );
+ is(
+ result.local.ordered,
+ options.ordered,
+ result.local + " ordered is:" + result.local.ordered
+ );
+ is(
+ result.local.maxRetransmits,
+ null,
+ result.local + " maxRetransmits is:" + result.local.maxRetransmits
+ );
+ is(
+ result.local.maxPacketLifeTime,
+ options.maxPacketLifeTime,
+ result.local + " maxPacketLifeTime is:" + result.local.maxPacketLifeTime
+ );
+
+ is(
+ result.remote.binaryType,
+ "blob",
+ result.remote + " is of binary type 'blob'"
+ );
+ is(
+ result.remote.protocol,
+ "",
+ result.remote + " protocol is:" + result.remote.protocol
+ );
+ is(
+ result.remote.reliable,
+ false,
+ result.remote + " reliable is:" + result.remote.reliable
+ );
+ is(
+ result.remote.ordered,
+ options.ordered,
+ result.remote + " ordered is:" + result.remote.ordered
+ );
+ is(
+ result.remote.maxRetransmits,
+ null,
+ result.remote + " maxRetransmits is:" + result.remote.maxRetransmits
+ );
+ is(
+ result.remote.maxPacketLifeTime,
+ options.maxPacketLifeTime,
+ result.remote +
+ " maxPacketLifeTime is:" +
+ result.remote.maxPacketLifeTime
+ );
+ });
+ },
+
+ function SEND_MESSAGE_THROUGH_LAST_OPENED_CHANNEL3(test) {
+ var channels = test.pcRemote.dataChannels;
+ var message = "Nice to see you working maxPacketLifeTime";
+
+ info("Sending message:" + message);
+ return test.send(message).then(result => {
+ is(
+ channels.indexOf(result.channel),
+ channels.length - 1,
+ "Last channel used"
+ );
+ is(result.data, message, "Received message has the correct content.");
+ });
+ },
+];
+
+var commandsCheckLargeXfer = [
+ function SEND_BIG_BUFFER(test) {
+ var size = 2 * 1024 * 1024; // SCTP internal buffer is now 1MB, so use 2MB to ensure the buffer gets full
+ var buffer = new ArrayBuffer(size);
+ // note: type received is always blob for binary data
+ var options = {};
+ options.bufferedAmountLowThreshold = 64 * 1024;
+ info("Sending arraybuffer");
+ return test.send(buffer, options).then(result => {
+ ok(result.data instanceof Blob, "Received data is of instance Blob");
+ is(result.data.size, size, "Received data has the correct size.");
+ });
+ },
+];
+
+function addInitialDataChannel(chain) {
+ chain.insertBefore("PC_LOCAL_CREATE_OFFER", commandsCreateDataChannel);
+ chain.insertBefore(
+ "PC_LOCAL_WAIT_FOR_MEDIA_FLOW",
+ commandsWaitForDataChannel
+ );
+ chain.removeAfter("PC_REMOTE_CHECK_ICE_CONNECTIONS");
+ chain.append(commandsCheckDataChannel);
+}
diff --git a/dom/media/webrtc/tests/mochitests/head.js b/dom/media/webrtc/tests/mochitests/head.js
new file mode 100644
index 0000000000..7c9f6d52de
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/head.js
@@ -0,0 +1,1445 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+var Cc = SpecialPowers.Cc;
+var Ci = SpecialPowers.Ci;
+
+// Specifies if we want fake audio streams for this run
+let WANT_FAKE_AUDIO = true;
+// Specifies if we want fake video streams for this run
+let WANT_FAKE_VIDEO = true;
+let TEST_AUDIO_FREQ = 1000;
+
+/**
+ * Reads the current values of preferences affecting fake and loopback devices
+ * and sets the WANT_FAKE_AUDIO and WANT_FAKE_VIDEO gloabals appropriately.
+ */
+function updateConfigFromFakeAndLoopbackPrefs() {
+ let audioDevice = SpecialPowers.getCharPref("media.audio_loopback_dev", "");
+ if (audioDevice) {
+ WANT_FAKE_AUDIO = false;
+ dump("TEST DEVICES: Got loopback audio: " + audioDevice + "\n");
+ } else {
+ WANT_FAKE_AUDIO = true;
+ dump(
+ "TEST DEVICES: No test device found in media.audio_loopback_dev, using fake audio streams.\n"
+ );
+ }
+ let videoDevice = SpecialPowers.getCharPref("media.video_loopback_dev", "");
+ if (videoDevice) {
+ WANT_FAKE_VIDEO = false;
+ dump("TEST DEVICES: Got loopback video: " + videoDevice + "\n");
+ } else {
+ WANT_FAKE_VIDEO = true;
+ dump(
+ "TEST DEVICES: No test device found in media.video_loopback_dev, using fake video streams.\n"
+ );
+ }
+}
+
+updateConfigFromFakeAndLoopbackPrefs();
+
+/**
+ * Global flag to skip LoopbackTone
+ */
+let DISABLE_LOOPBACK_TONE = false;
+/**
+ * Helper class to setup a sine tone of a given frequency.
+ */
+class LoopbackTone {
+ constructor(audioContext, frequency) {
+ if (!audioContext) {
+ throw new Error("You must provide a valid AudioContext");
+ }
+ this.oscNode = audioContext.createOscillator();
+ var gainNode = audioContext.createGain();
+ gainNode.gain.value = 0.5;
+ this.oscNode.connect(gainNode);
+ gainNode.connect(audioContext.destination);
+ this.changeFrequency(frequency);
+ }
+
+ // Method should be used when WANT_FAKE_AUDIO is false.
+ start() {
+ if (!this.oscNode) {
+ throw new Error("Attempt to start a stopped LoopbackTone");
+ }
+ info(`Start loopback tone at ${this.oscNode.frequency.value}`);
+ this.oscNode.start();
+ }
+
+ // Change the frequency of the tone. It can be used after start.
+ // Frequency will change on the fly. No need to stop and create a new instance.
+ changeFrequency(frequency) {
+ if (!this.oscNode) {
+ throw new Error("Attempt to change frequency on a stopped LoopbackTone");
+ }
+ this.oscNode.frequency.value = frequency;
+ }
+
+ stop() {
+ if (!this.oscNode) {
+ throw new Error("Attempt to stop a stopped LoopbackTone");
+ }
+ this.oscNode.stop();
+ this.oscNode = null;
+ }
+}
+// Object that holds the default loopback tone.
+var DefaultLoopbackTone = null;
+
+/**
+ * This class provides helpers around analysing the audio content in a stream
+ * using WebAudio AnalyserNodes.
+ *
+ * @constructor
+ * @param {object} stream
+ * A MediaStream object whose audio track we shall analyse.
+ */
+function AudioStreamAnalyser(ac, stream) {
+ this.audioContext = ac;
+ this.stream = stream;
+ this.sourceNodes = [];
+ this.analyser = this.audioContext.createAnalyser();
+ // Setting values lower than default for speedier testing on emulators
+ this.analyser.smoothingTimeConstant = 0.2;
+ this.analyser.fftSize = 1024;
+ this.connectTrack = t => {
+ let source = this.audioContext.createMediaStreamSource(
+ new MediaStream([t])
+ );
+ this.sourceNodes.push(source);
+ source.connect(this.analyser);
+ };
+ this.stream.getAudioTracks().forEach(t => this.connectTrack(t));
+ this.onaddtrack = ev => this.connectTrack(ev.track);
+ this.stream.addEventListener("addtrack", this.onaddtrack);
+ this.data = new Uint8Array(this.analyser.frequencyBinCount);
+}
+
+AudioStreamAnalyser.prototype = {
+ /**
+ * Get an array of frequency domain data for our stream's audio track.
+ *
+ * @returns {array} A Uint8Array containing the frequency domain data.
+ */
+ getByteFrequencyData() {
+ this.analyser.getByteFrequencyData(this.data);
+ return this.data;
+ },
+
+ /**
+ * Append a canvas to the DOM where the frequency data are drawn.
+ * Useful to debug tests.
+ */
+ enableDebugCanvas() {
+ var cvs = (this.debugCanvas = document.createElement("canvas"));
+ const content = document.getElementById("content");
+ content.insertBefore(cvs, content.children[0]);
+
+ // Easy: 1px per bin
+ cvs.width = this.analyser.frequencyBinCount;
+ cvs.height = 128;
+ cvs.style.border = "1px solid red";
+
+ var c = cvs.getContext("2d");
+ c.fillStyle = "black";
+
+ var self = this;
+ function render() {
+ c.clearRect(0, 0, cvs.width, cvs.height);
+ var array = self.getByteFrequencyData();
+ for (var i = 0; i < array.length; i++) {
+ c.fillRect(i, cvs.height - array[i] / 2, 1, cvs.height);
+ }
+ if (!cvs.stopDrawing) {
+ requestAnimationFrame(render);
+ }
+ }
+ requestAnimationFrame(render);
+ },
+
+ /**
+ * Stop drawing of and remove the debug canvas from the DOM if it was
+ * previously added.
+ */
+ disableDebugCanvas() {
+ if (!this.debugCanvas || !this.debugCanvas.parentElement) {
+ return;
+ }
+
+ this.debugCanvas.stopDrawing = true;
+ this.debugCanvas.parentElement.removeChild(this.debugCanvas);
+ },
+
+ /**
+ * Disconnects the input stream from our internal analyser node.
+ * Call this to reduce main thread processing, mostly necessary on slow
+ * devices.
+ */
+ disconnect() {
+ this.disableDebugCanvas();
+ this.sourceNodes.forEach(n => n.disconnect());
+ this.sourceNodes = [];
+ this.stream.removeEventListener("addtrack", this.onaddtrack);
+ },
+
+ /**
+ * Return a Promise, that will be resolved when the function passed as
+ * argument, when called, returns true (meaning the analysis was a
+ * success). The promise is rejected if the cancel promise resolves first.
+ *
+ * @param {function} analysisFunction
+ * A function that performs an analysis, and resolves with true if the
+ * analysis was a success (i.e. it found what it was looking for)
+ * @param {promise} cancel
+ * A promise that on resolving will reject the promise we returned.
+ */
+ async waitForAnalysisSuccess(
+ analysisFunction,
+ cancel = wait(60000, new Error("Audio analysis timed out"))
+ ) {
+ let aborted = false;
+ cancel.then(() => (aborted = true));
+
+ // We need to give the Analyser some time to start gathering data.
+ await wait(200);
+
+ do {
+ await new Promise(resolve => requestAnimationFrame(resolve));
+ if (aborted) {
+ throw await cancel;
+ }
+ } while (!analysisFunction(this.getByteFrequencyData()));
+ },
+
+ /**
+ * Return the FFT bin index for a given frequency.
+ *
+ * @param {double} frequency
+ * The frequency for whicht to return the bin number.
+ * @returns {integer} the index of the bin in the FFT array.
+ */
+ binIndexForFrequency(frequency) {
+ return (
+ 1 +
+ Math.round(
+ (frequency * this.analyser.fftSize) / this.audioContext.sampleRate
+ )
+ );
+ },
+
+ /**
+ * Reverse operation, get the frequency for a bin index.
+ *
+ * @param {integer} index an index in an FFT array
+ * @returns {double} the frequency for this bin
+ */
+ frequencyForBinIndex(index) {
+ return ((index - 1) * this.audioContext.sampleRate) / this.analyser.fftSize;
+ },
+};
+
+/**
+ * Creates a MediaStream with an audio track containing a sine tone at the
+ * given frequency.
+ *
+ * @param {AudioContext} ac
+ * AudioContext in which to create the OscillatorNode backing the stream
+ * @param {double} frequency
+ * The frequency in Hz of the generated sine tone
+ * @returns {MediaStream} the MediaStream containing sine tone audio track
+ */
+function createOscillatorStream(ac, frequency) {
+ var osc = ac.createOscillator();
+ osc.frequency.value = frequency;
+
+ var oscDest = ac.createMediaStreamDestination();
+ osc.connect(oscDest);
+ osc.start();
+ return oscDest.stream;
+}
+
+/**
+ * Create the necessary HTML elements for head and body as used by Mochitests
+ *
+ * @param {object} meta
+ * Meta information of the test
+ * @param {string} meta.title
+ * Description of the test
+ * @param {string} [meta.bug]
+ * Bug the test was created for
+ * @param {boolean} [meta.visible=false]
+ * Visibility of the media elements
+ */
+function realCreateHTML(meta) {
+ var test = document.getElementById("test");
+
+ // Create the head content
+ var elem = document.createElement("meta");
+ elem.setAttribute("charset", "utf-8");
+ document.head.appendChild(elem);
+
+ var title = document.createElement("title");
+ title.textContent = meta.title;
+ document.head.appendChild(title);
+
+ // Create the body content
+ var anchor = document.createElement("a");
+ anchor.textContent = meta.title;
+ if (meta.bug) {
+ anchor.setAttribute(
+ "href",
+ "https://bugzilla.mozilla.org/show_bug.cgi?id=" + meta.bug
+ );
+ } else {
+ anchor.setAttribute("target", "_blank");
+ }
+
+ document.body.insertBefore(anchor, test);
+
+ var display = document.createElement("p");
+ display.setAttribute("id", "display");
+ document.body.insertBefore(display, test);
+
+ var content = document.createElement("div");
+ content.setAttribute("id", "content");
+ content.style.display = meta.visible ? "block" : "none";
+ document.body.appendChild(content);
+}
+
+/**
+ * Creates an element of the given type, assigns the given id, sets the controls
+ * and autoplay attributes and adds it to the content node.
+ *
+ * @param {string} type
+ * Defining if we should create an "audio" or "video" element
+ * @param {string} id
+ * A string to use as the element id.
+ */
+function createMediaElement(type, id) {
+ const element = document.createElement(type);
+ element.setAttribute("id", id);
+ element.setAttribute("height", 100);
+ element.setAttribute("width", 150);
+ element.setAttribute("controls", "controls");
+ element.setAttribute("autoplay", "autoplay");
+ element.setAttribute("muted", "muted");
+ element.muted = true;
+ document.getElementById("content").appendChild(element);
+
+ return element;
+}
+
+/**
+ * Returns an existing element for the given track with the given idPrefix,
+ * as it was added by createMediaElementForTrack().
+ *
+ * @param {MediaStreamTrack} track
+ * Track used as the element's source.
+ * @param {string} idPrefix
+ * A string to use as the element id. The track id will also be appended.
+ */
+function getMediaElementForTrack(track, idPrefix) {
+ return document.getElementById(idPrefix + "_" + track.id);
+}
+
+/**
+ * Create a media element with a track as source and attach it to the content
+ * node.
+ *
+ * @param {MediaStreamTrack} track
+ * Track for use as source.
+ * @param {string} idPrefix
+ * A string to use as the element id. The track id will also be appended.
+ * @return {HTMLMediaElement} The created HTML media element
+ */
+function createMediaElementForTrack(track, idPrefix) {
+ const id = idPrefix + "_" + track.id;
+ const element = createMediaElement(track.kind, id);
+ element.srcObject = new MediaStream([track]);
+
+ return element;
+}
+
+/**
+ * Wrapper function for mediaDevices.getUserMedia used by some tests. Whether
+ * to use fake devices or not is now determined in pref further below instead.
+ *
+ * @param {Dictionary} constraints
+ * The constraints for this mozGetUserMedia callback
+ */
+function getUserMedia(constraints) {
+ // Tests may have changed the values of prefs, so recheck
+ updateConfigFromFakeAndLoopbackPrefs();
+ if (
+ !WANT_FAKE_AUDIO &&
+ !constraints.fake &&
+ constraints.audio &&
+ !DISABLE_LOOPBACK_TONE
+ ) {
+ // Loopback device is configured, start the default loopback tone
+ if (!DefaultLoopbackTone) {
+ TEST_AUDIO_FREQ = 440;
+ DefaultLoopbackTone = new LoopbackTone(
+ new AudioContext(),
+ TEST_AUDIO_FREQ
+ );
+ DefaultLoopbackTone.start();
+ }
+ // Disable input processing mode when it's not explicity enabled.
+ // This is to avoid distortion of the loopback tone
+ constraints.audio = Object.assign(
+ {},
+ { autoGainControl: false },
+ { echoCancellation: false },
+ { noiseSuppression: false },
+ constraints.audio
+ );
+ } else {
+ // Fake device configured, ensure our test freq is correct.
+ TEST_AUDIO_FREQ = 1000;
+ }
+ info("Call getUserMedia for " + JSON.stringify(constraints));
+ return navigator.mediaDevices
+ .getUserMedia(constraints)
+ .then(stream => (checkMediaStreamTracks(constraints, stream), stream));
+}
+
+// These are the promises we use to track that the prerequisites for the test
+// are in place before running it.
+var setTestOptions;
+var testConfigured = new Promise(r => (setTestOptions = r));
+
+function pushPrefs(...p) {
+ return SpecialPowers.pushPrefEnv({ set: p });
+}
+
+async function withPrefs(prefs, func) {
+ await SpecialPowers.pushPrefEnv({ set: prefs });
+ try {
+ return await func();
+ } finally {
+ await SpecialPowers.popPrefEnv();
+ }
+}
+
+function setupEnvironment() {
+ var defaultMochitestPrefs = {
+ set: [
+ ["media.peerconnection.enabled", true],
+ ["media.peerconnection.identity.enabled", true],
+ ["media.peerconnection.identity.timeout", 120000],
+ ["media.peerconnection.ice.stun_client_maximum_transmits", 14],
+ ["media.peerconnection.ice.trickle_grace_period", 30000],
+ ["media.navigator.permission.disabled", true],
+ // If either fake audio or video is desired we enable fake streams.
+ // If loopback devices are set they will be chosen instead of fakes in gecko.
+ ["media.navigator.streams.fake", WANT_FAKE_AUDIO || WANT_FAKE_VIDEO],
+ ["media.getusermedia.audiocapture.enabled", true],
+ ["media.getusermedia.screensharing.enabled", true],
+ ["media.getusermedia.window.focus_source.enabled", false],
+ ["media.recorder.audio_node.enabled", true],
+ ["media.peerconnection.ice.obfuscate_host_addresses", false],
+ ["media.peerconnection.nat_simulator.filtering_type", ""],
+ ["media.peerconnection.nat_simulator.mapping_type", ""],
+ ["media.peerconnection.nat_simulator.block_tcp", false],
+ ["media.peerconnection.nat_simulator.block_udp", false],
+ ["media.peerconnection.nat_simulator.redirect_address", ""],
+ ["media.peerconnection.nat_simulator.redirect_targets", ""],
+ ],
+ };
+
+ if (navigator.userAgent.includes("Android")) {
+ defaultMochitestPrefs.set.push(
+ ["media.navigator.video.default_width", 320],
+ ["media.navigator.video.default_height", 240],
+ ["media.navigator.video.max_fr", 10],
+ ["media.autoplay.default", Ci.nsIAutoplay.ALLOWED]
+ );
+ }
+
+ // Platform codec prefs should be matched because fake H.264 GMP codec doesn't
+ // produce/consume real bitstreams. [TODO] remove after bug 1509012 is fixed.
+ const platformEncoderEnabled = SpecialPowers.getBoolPref(
+ "media.webrtc.platformencoder"
+ );
+ defaultMochitestPrefs.set.push([
+ "media.navigator.mediadatadecoder_h264_enabled",
+ platformEncoderEnabled,
+ ]);
+
+ // Running as a Mochitest.
+ SimpleTest.requestFlakyTimeout("WebRTC inherently depends on timeouts");
+ window.finish = () => SimpleTest.finish();
+ SpecialPowers.pushPrefEnv(defaultMochitestPrefs, setTestOptions);
+
+ // We don't care about waiting for this to complete, we just want to ensure
+ // that we don't build up a huge backlog of GC work.
+ SpecialPowers.exactGC();
+}
+
+// [TODO] remove after bug 1509012 is fixed.
+async function matchPlatformH264CodecPrefs() {
+ const hasHW264 =
+ SpecialPowers.getBoolPref("media.webrtc.platformencoder") &&
+ !SpecialPowers.getBoolPref("media.webrtc.platformencoder.sw_only") &&
+ (navigator.userAgent.includes("Android") ||
+ navigator.userAgent.includes("Mac OS X"));
+
+ await pushPrefs(
+ ["media.webrtc.platformencoder", hasHW264],
+ ["media.navigator.mediadatadecoder_h264_enabled", hasHW264]
+ );
+}
+
+async function runTestWhenReady(testFunc) {
+ setupEnvironment();
+ const options = await testConfigured;
+ try {
+ await testFunc(options);
+ } catch (e) {
+ ok(
+ false,
+ `Error executing test: ${e}
+${e.stack ? e.stack : ""}`
+ );
+ } finally {
+ SimpleTest.finish();
+ }
+}
+
+/**
+ * Checks that the media stream tracks have the expected amount of tracks
+ * with the correct attributes based on the type and constraints given.
+ *
+ * @param {Object} constraints specifies whether the stream should have
+ * audio, video, or both
+ * @param {String} type the type of media stream tracks being checked
+ * @param {sequence<MediaStreamTrack>} mediaStreamTracks the media stream
+ * tracks being checked
+ */
+function checkMediaStreamTracksByType(constraints, type, mediaStreamTracks) {
+ if (constraints[type]) {
+ is(mediaStreamTracks.length, 1, "One " + type + " track shall be present");
+
+ if (mediaStreamTracks.length) {
+ is(mediaStreamTracks[0].kind, type, "Track kind should be " + type);
+ ok(mediaStreamTracks[0].id, "Track id should be defined");
+ ok(!mediaStreamTracks[0].muted, "Track should not be muted");
+ }
+ } else {
+ is(mediaStreamTracks.length, 0, "No " + type + " tracks shall be present");
+ }
+}
+
+/**
+ * Check that the given media stream contains the expected media stream
+ * tracks given the associated audio & video constraints provided.
+ *
+ * @param {Object} constraints specifies whether the stream should have
+ * audio, video, or both
+ * @param {MediaStream} mediaStream the media stream being checked
+ */
+function checkMediaStreamTracks(constraints, mediaStream) {
+ checkMediaStreamTracksByType(
+ constraints,
+ "audio",
+ mediaStream.getAudioTracks()
+ );
+ checkMediaStreamTracksByType(
+ constraints,
+ "video",
+ mediaStream.getVideoTracks()
+ );
+}
+
+/**
+ * Check that a media stream contains exactly a set of media stream tracks.
+ *
+ * @param {MediaStream} mediaStream the media stream being checked
+ * @param {Array} tracks the tracks that should exist in mediaStream
+ * @param {String} [message] an optional message to pass to asserts
+ */
+function checkMediaStreamContains(mediaStream, tracks, message) {
+ message = message ? message + ": " : "";
+ tracks.forEach(t =>
+ ok(
+ mediaStream.getTrackById(t.id),
+ message + "MediaStream " + mediaStream.id + " contains track " + t.id
+ )
+ );
+ is(
+ mediaStream.getTracks().length,
+ tracks.length,
+ message + "MediaStream " + mediaStream.id + " contains no extra tracks"
+ );
+}
+
+function checkMediaStreamCloneAgainstOriginal(clone, original) {
+ isnot(clone.id.length, 0, "Stream clone should have an id string");
+ isnot(clone, original, "Stream clone should be different from the original");
+ isnot(
+ clone.id,
+ original.id,
+ "Stream clone's id should be different from the original's"
+ );
+ is(
+ clone.getAudioTracks().length,
+ original.getAudioTracks().length,
+ "All audio tracks should get cloned"
+ );
+ is(
+ clone.getVideoTracks().length,
+ original.getVideoTracks().length,
+ "All video tracks should get cloned"
+ );
+ is(clone.active, original.active, "Active state should be preserved");
+ original
+ .getTracks()
+ .forEach(t =>
+ ok(!clone.getTrackById(t.id), "The clone's tracks should be originals")
+ );
+}
+
+function checkMediaStreamTrackCloneAgainstOriginal(clone, original) {
+ isnot(clone.id.length, 0, "Track clone should have an id string");
+ isnot(clone, original, "Track clone should be different from the original");
+ isnot(
+ clone.id,
+ original.id,
+ "Track clone's id should be different from the original's"
+ );
+ is(
+ clone.kind,
+ original.kind,
+ "Track clone's kind should be same as the original's"
+ );
+ is(
+ clone.enabled,
+ original.enabled,
+ "Track clone's kind should be same as the original's"
+ );
+ is(
+ clone.readyState,
+ original.readyState,
+ "Track clone's readyState should be same as the original's"
+ );
+ is(
+ clone.muted,
+ original.muted,
+ "Track clone's muted state should be same as the original's"
+ );
+}
+
+/*** Utility methods */
+
+/** The dreadful setTimeout, use sparingly */
+function wait(time, message) {
+ return new Promise(r => setTimeout(() => r(message), time));
+}
+
+/** The even more dreadful setInterval, use even more sparingly */
+function waitUntil(func, time) {
+ return new Promise(resolve => {
+ var interval = setInterval(() => {
+ if (func()) {
+ clearInterval(interval);
+ resolve();
+ }
+ }, time || 200);
+ });
+}
+
+/** Time out while waiting for a promise to get resolved or rejected. */
+var timeout = (promise, time, msg) =>
+ Promise.race([
+ promise,
+ wait(time).then(() => Promise.reject(new Error(msg))),
+ ]);
+
+/** Adds a |finally| function to a promise whose argument is invoked whether the
+ * promise is resolved or rejected, and that does not interfere with chaining.*/
+var addFinallyToPromise = promise => {
+ promise.finally = func => {
+ return promise.then(
+ result => {
+ func();
+ return Promise.resolve(result);
+ },
+ error => {
+ func();
+ return Promise.reject(error);
+ }
+ );
+ };
+ return promise;
+};
+
+/** Use event listener to call passed-in function on fire until it returns true */
+var listenUntil = (target, eventName, onFire) => {
+ return new Promise(resolve =>
+ target.addEventListener(eventName, function callback(event) {
+ var result = onFire(event);
+ if (result) {
+ target.removeEventListener(eventName, callback);
+ resolve(result);
+ }
+ })
+ );
+};
+
+/* Test that a function throws the right error */
+function mustThrowWith(msg, reason, f) {
+ try {
+ f();
+ ok(false, msg + " must throw");
+ } catch (e) {
+ is(e.name, reason, msg + " must throw: " + e.message);
+ }
+}
+
+/* Get a dummy audio track */
+function getSilentTrack() {
+ let ctx = new AudioContext(),
+ oscillator = ctx.createOscillator();
+ let dst = oscillator.connect(ctx.createMediaStreamDestination());
+ oscillator.start();
+ return Object.assign(dst.stream.getAudioTracks()[0], { enabled: false });
+}
+
+function getBlackTrack({ width = 640, height = 480 } = {}) {
+ let canvas = Object.assign(document.createElement("canvas"), {
+ width,
+ height,
+ });
+ canvas.getContext("2d").fillRect(0, 0, width, height);
+ let stream = canvas.captureStream();
+ return Object.assign(stream.getVideoTracks()[0], { enabled: false });
+}
+
+/*** Test control flow methods */
+
+/**
+ * Generates a callback function fired only under unexpected circumstances
+ * while running the tests. The generated function kills off the test as well
+ * gracefully.
+ *
+ * @param {String} [message]
+ * An optional message to show if no object gets passed into the
+ * generated callback method.
+ */
+function generateErrorCallback(message) {
+ var stack = new Error().stack.split("\n");
+ stack.shift(); // Don't include this instantiation frame
+
+ /**
+ * @param {object} aObj
+ * The object fired back from the callback
+ */
+ return aObj => {
+ if (aObj) {
+ if (aObj.name && aObj.message) {
+ ok(
+ false,
+ "Unexpected callback for '" +
+ aObj.name +
+ "' with message = '" +
+ aObj.message +
+ "' at " +
+ JSON.stringify(stack)
+ );
+ } else {
+ ok(
+ false,
+ "Unexpected callback with = '" +
+ aObj +
+ "' at: " +
+ JSON.stringify(stack)
+ );
+ }
+ } else {
+ ok(
+ false,
+ "Unexpected callback with message = '" +
+ message +
+ "' at: " +
+ JSON.stringify(stack)
+ );
+ }
+ throw new Error("Unexpected callback");
+ };
+}
+
+var unexpectedEventArrived;
+var rejectOnUnexpectedEvent = new Promise((x, reject) => {
+ unexpectedEventArrived = reject;
+});
+
+/**
+ * Generates a callback function fired only for unexpected events happening.
+ *
+ * @param {String} description
+ Description of the object for which the event has been fired
+ * @param {String} eventName
+ Name of the unexpected event
+ */
+function unexpectedEvent(message, eventName) {
+ var stack = new Error().stack.split("\n");
+ stack.shift(); // Don't include this instantiation frame
+
+ return e => {
+ var details =
+ "Unexpected event '" +
+ eventName +
+ "' fired with message = '" +
+ message +
+ "' at: " +
+ JSON.stringify(stack);
+ ok(false, details);
+ unexpectedEventArrived(new Error(details));
+ };
+}
+
+/**
+ * Implements the one-shot event pattern used throughout. Each of the 'onxxx'
+ * attributes on the wrappers can be set with a custom handler. Prior to the
+ * handler being set, if the event fires, it causes the test execution to halt.
+ * That handler is used exactly once, after which the original, error-generating
+ * handler is re-installed. Thus, each event handler is used at most once.
+ *
+ * @param {object} wrapper
+ * The wrapper on which the psuedo-handler is installed
+ * @param {object} obj
+ * The real source of events
+ * @param {string} event
+ * The name of the event
+ */
+function createOneShotEventWrapper(wrapper, obj, event) {
+ var onx = "on" + event;
+ var unexpected = unexpectedEvent(wrapper, event);
+ wrapper[onx] = unexpected;
+ obj[onx] = e => {
+ info(wrapper + ': "on' + event + '" event fired');
+ e.wrapper = wrapper;
+ wrapper[onx](e);
+ wrapper[onx] = unexpected;
+ };
+}
+
+/**
+ * Returns a promise that resolves when `target` has raised an event with the
+ * given name the given number of times. Cancel the returned promise by passing
+ * in a `cancel` promise and resolving it.
+ *
+ * @param {object} target
+ * The target on which the event should occur.
+ * @param {string} name
+ * The name of the event that should occur.
+ * @param {integer} count
+ * Optional number of times the event should be raised before resolving.
+ * @param {promise} cancel
+ * Optional promise that on resolving rejects the returned promise,
+ * so we can avoid logging results after a test has finished.
+ * @returns {promise} A promise that resolves to the last of the seen events.
+ */
+function haveEvents(target, name, count, cancel) {
+ var listener;
+ var counter = count || 1;
+ return Promise.race([
+ (cancel || new Promise(() => {})).then(e => Promise.reject(e)),
+ new Promise(resolve =>
+ target.addEventListener(
+ name,
+ (listener = e => --counter < 1 && resolve(e))
+ )
+ ),
+ ]).then(e => (target.removeEventListener(name, listener), e));
+}
+
+/**
+ * Returns a promise that resolves when `target` has raised an event with the
+ * given name. Cancel the returned promise by passing in a `cancel` promise and
+ * resolving it.
+ *
+ * @param {object} target
+ * The target on which the event should occur.
+ * @param {string} name
+ * The name of the event that should occur.
+ * @param {promise} cancel
+ * Optional promise that on resolving rejects the returned promise,
+ * so we can avoid logging results after a test has finished.
+ * @returns {promise} A promise that resolves to the seen event.
+ */
+function haveEvent(target, name, cancel) {
+ return haveEvents(target, name, 1, cancel);
+}
+
+/**
+ * Returns a promise that resolves if the target has not seen the given event
+ * after one crank (or until the given timeoutPromise resolves) of the event
+ * loop.
+ *
+ * @param {object} target
+ * The target on which the event should not occur.
+ * @param {string} name
+ * The name of the event that should not occur.
+ * @param {promise} timeoutPromise
+ * Optional promise defining how long we should wait before resolving.
+ * @returns {promise} A promise that is rejected if we see the given event, or
+ * resolves after a timeout otherwise.
+ */
+function haveNoEvent(target, name, timeoutPromise) {
+ return haveEvent(target, name, timeoutPromise || wait(0)).then(
+ () => Promise.reject(new Error("Too many " + name + " events")),
+ () => {}
+ );
+}
+
+/**
+ * Returns a promise that resolves after the target has seen the given number
+ * of events but no such event in a following crank of the event loop.
+ *
+ * @param {object} target
+ * The target on which the events should occur.
+ * @param {string} name
+ * The name of the event that should occur.
+ * @param {integer} count
+ * Optional number of times the event should be raised before resolving.
+ * @param {promise} cancel
+ * Optional promise that on resolving rejects the returned promise,
+ * so we can avoid logging results after a test has finished.
+ * @returns {promise} A promise that resolves to the last of the seen events.
+ */
+function haveEventsButNoMore(target, name, count, cancel) {
+ return haveEvents(target, name, count, cancel).then(e =>
+ haveNoEvent(target, name).then(() => e)
+ );
+}
+
+/*
+ * Resolves the returned promise with an object with usage and reportCount
+ * properties. `usage` is in the same units as reported by the reporter for
+ * `path`.
+ */
+const collectMemoryUsage = async path => {
+ const MemoryReporterManager = Cc[
+ "@mozilla.org/memory-reporter-manager;1"
+ ].getService(Ci.nsIMemoryReporterManager);
+
+ let usage = 0;
+ let reportCount = 0;
+ await new Promise(resolve =>
+ MemoryReporterManager.getReports(
+ (aProcess, aPath, aKind, aUnits, aAmount, aDesc) => {
+ if (aPath != path) {
+ return;
+ }
+ ++reportCount;
+ usage += aAmount;
+ },
+ null,
+ resolve,
+ null,
+ /* anonymized = */ false
+ )
+ );
+ return { usage, reportCount };
+};
+
+// Some DNS helper functions
+const dnsLookup = async hostname => {
+ // Convenience API for various networking related stuff. _Almost_ convenient
+ // enough.
+ const neckoDashboard = SpecialPowers.Cc[
+ "@mozilla.org/network/dashboard;1"
+ ].getService(Ci.nsIDashboard);
+
+ const results = await new Promise(r => {
+ neckoDashboard.requestDNSLookup(hostname, results => {
+ r(SpecialPowers.wrap(results));
+ });
+ });
+
+ // |address| is an array-like dictionary (ie; keys are all integers).
+ // We convert to an array to make it less unwieldy.
+ const addresses = [...results.address];
+ info(`DNS results for ${hostname}: ${JSON.stringify(addresses)}`);
+ return addresses;
+};
+
+const dnsLookupV4 = async hostname => {
+ const addresses = await dnsLookup(hostname);
+ return addresses.filter(address => !address.includes(":"));
+};
+
+const dnsLookupV6 = async hostname => {
+ const addresses = await dnsLookup(hostname);
+ return addresses.filter(address => address.includes(":"));
+};
+
+const getTurnHostname = turnUrl => {
+ const urlNoParams = turnUrl.split("?")[0];
+ // Strip off scheme
+ const hostAndMaybePort = urlNoParams.split(":", 2)[1];
+ if (hostAndMaybePort[0] == "[") {
+ // IPV6 literal, strip out '[', and split at closing ']'
+ return hostAndMaybePort.substring(1).split("]")[0];
+ }
+ return hostAndMaybePort.split(":")[0];
+};
+
+// Yo dawg I heard you like yo dawg I heard you like Proxies
+// Example: let value = await GleanTest.category.metric.testGetValue();
+// For labeled metrics:
+// let value = await GleanTest.category.metric["label"].testGetValue();
+// Please don't try to use the string "testGetValue" as a label.
+const GleanTest = new Proxy(
+ {},
+ {
+ get(target, categoryName, receiver) {
+ return new Proxy(
+ {},
+ {
+ get(target, metricName, receiver) {
+ return new Proxy(
+ {
+ async testGetValue() {
+ return SpecialPowers.spawnChrome(
+ [categoryName, metricName],
+ async (categoryName, metricName) => {
+ await Services.fog.testFlushAllChildren();
+ const window = this.browsingContext.topChromeWindow;
+ return window.Glean[categoryName][
+ metricName
+ ].testGetValue();
+ }
+ );
+ },
+ },
+ {
+ get(target, prop, receiver) {
+ // The only prop that will be there is testGetValue, but we
+ // might add more later.
+ if (prop in target) {
+ return target[prop];
+ }
+
+ // |prop| must be a label?
+ const label = prop;
+ return {
+ async testGetValue() {
+ return SpecialPowers.spawnChrome(
+ [categoryName, metricName, label],
+ async (categoryName, metricName, label) => {
+ await Services.fog.testFlushAllChildren();
+ const window = this.browsingContext.topChromeWindow;
+ return window.Glean[categoryName][metricName][
+ label
+ ].testGetValue();
+ }
+ );
+ },
+ };
+ },
+ }
+ );
+ },
+ }
+ );
+ },
+ }
+);
+
+/**
+ * This class executes a series of functions in a continuous sequence.
+ * Promise-bearing functions are executed after the previous promise completes.
+ *
+ * @constructor
+ * @param {object} framework
+ * A back reference to the framework which makes use of the class. It is
+ * passed to each command callback.
+ * @param {function[]} commandList
+ * Commands to set during initialization
+ */
+function CommandChain(framework, commandList) {
+ this._framework = framework;
+ this.commands = commandList || [];
+}
+
+CommandChain.prototype = {
+ /**
+ * Start the command chain. This returns a promise that always resolves
+ * cleanly (this catches errors and fails the test case).
+ */
+ execute() {
+ return this.commands
+ .reduce((prev, next, i) => {
+ if (typeof next !== "function" || !next.name) {
+ throw new Error("registered non-function" + next);
+ }
+
+ return prev.then(() => {
+ info("Run step " + (i + 1) + ": " + next.name);
+ return Promise.race([next(this._framework), rejectOnUnexpectedEvent]);
+ });
+ }, Promise.resolve())
+ .catch(e =>
+ ok(
+ false,
+ "Error in test execution: " +
+ e +
+ (typeof e.stack === "string"
+ ? " " + e.stack.split("\n").join(" ... ")
+ : "")
+ )
+ );
+ },
+
+ /**
+ * Add new commands to the end of the chain
+ */
+ append(commands) {
+ this.commands = this.commands.concat(commands);
+ },
+
+ /**
+ * Returns the index of the specified command in the chain.
+ * @param {occurrence} Optional param specifying which occurrence to match,
+ * with 0 representing the first occurrence.
+ */
+ indexOf(functionOrName, occurrence) {
+ occurrence = occurrence || 0;
+ return this.commands.findIndex(func => {
+ if (typeof functionOrName === "string") {
+ if (func.name !== functionOrName) {
+ return false;
+ }
+ } else if (func !== functionOrName) {
+ return false;
+ }
+ if (occurrence) {
+ --occurrence;
+ return false;
+ }
+ return true;
+ });
+ },
+
+ mustHaveIndexOf(functionOrName, occurrence) {
+ var index = this.indexOf(functionOrName, occurrence);
+ if (index == -1) {
+ throw new Error("Unknown test: " + functionOrName);
+ }
+ return index;
+ },
+
+ /**
+ * Inserts the new commands after the specified command.
+ */
+ insertAfter(functionOrName, commands, all, occurrence) {
+ this._insertHelper(functionOrName, commands, 1, all, occurrence);
+ },
+
+ /**
+ * Inserts the new commands after every occurrence of the specified command
+ */
+ insertAfterEach(functionOrName, commands) {
+ this._insertHelper(functionOrName, commands, 1, true);
+ },
+
+ /**
+ * Inserts the new commands before the specified command.
+ */
+ insertBefore(functionOrName, commands, all, occurrence) {
+ this._insertHelper(functionOrName, commands, 0, all, occurrence);
+ },
+
+ _insertHelper(functionOrName, commands, delta, all, occurrence) {
+ occurrence = occurrence || 0;
+ for (
+ var index = this.mustHaveIndexOf(functionOrName, occurrence);
+ index !== -1;
+ index = this.indexOf(functionOrName, ++occurrence)
+ ) {
+ this.commands = [].concat(
+ this.commands.slice(0, index + delta),
+ commands,
+ this.commands.slice(index + delta)
+ );
+ if (!all) {
+ break;
+ }
+ }
+ },
+
+ /**
+ * Removes the specified command, returns what was removed.
+ */
+ remove(functionOrName, occurrence) {
+ return this.commands.splice(
+ this.mustHaveIndexOf(functionOrName, occurrence),
+ 1
+ );
+ },
+
+ /**
+ * Removes all commands after the specified one, returns what was removed.
+ */
+ removeAfter(functionOrName, occurrence) {
+ return this.commands.splice(
+ this.mustHaveIndexOf(functionOrName, occurrence) + 1
+ );
+ },
+
+ /**
+ * Removes all commands before the specified one, returns what was removed.
+ */
+ removeBefore(functionOrName, occurrence) {
+ return this.commands.splice(
+ 0,
+ this.mustHaveIndexOf(functionOrName, occurrence)
+ );
+ },
+
+ /**
+ * Replaces a single command, returns what was removed.
+ */
+ replace(functionOrName, commands) {
+ this.insertBefore(functionOrName, commands);
+ return this.remove(functionOrName);
+ },
+
+ /**
+ * Replaces all commands after the specified one, returns what was removed.
+ */
+ replaceAfter(functionOrName, commands, occurrence) {
+ var oldCommands = this.removeAfter(functionOrName, occurrence);
+ this.append(commands);
+ return oldCommands;
+ },
+
+ /**
+ * Replaces all commands before the specified one, returns what was removed.
+ */
+ replaceBefore(functionOrName, commands) {
+ var oldCommands = this.removeBefore(functionOrName);
+ this.insertBefore(functionOrName, commands);
+ return oldCommands;
+ },
+
+ /**
+ * Remove all commands whose name match the specified regex.
+ */
+ filterOut(id_match) {
+ this.commands = this.commands.filter(c => !id_match.test(c.name));
+ },
+};
+
+function AudioStreamHelper() {
+ this._context = new AudioContext();
+}
+
+AudioStreamHelper.prototype = {
+ checkAudio(stream, analyser, fun) {
+ /*
+ analyser.enableDebugCanvas();
+ return analyser.waitForAnalysisSuccess(fun)
+ .then(() => analyser.disableDebugCanvas());
+ */
+ return analyser.waitForAnalysisSuccess(fun);
+ },
+
+ checkAudioFlowing(stream) {
+ var analyser = new AudioStreamAnalyser(this._context, stream);
+ var freq = analyser.binIndexForFrequency(TEST_AUDIO_FREQ);
+ return this.checkAudio(stream, analyser, array => array[freq] > 200);
+ },
+
+ checkAudioNotFlowing(stream) {
+ var analyser = new AudioStreamAnalyser(this._context, stream);
+ var freq = analyser.binIndexForFrequency(TEST_AUDIO_FREQ);
+ return this.checkAudio(stream, analyser, array => array[freq] < 50);
+ },
+};
+
+class VideoFrameEmitter {
+ constructor(color1, color2, width, height) {
+ if (!width) {
+ width = 50;
+ }
+ if (!height) {
+ height = width;
+ }
+ this._helper = new CaptureStreamTestHelper2D(width, height);
+ this._canvas = this._helper.createAndAppendElement(
+ "canvas",
+ "source_canvas"
+ );
+ this._canvas.width = width;
+ this._canvas.height = height;
+ this._color1 = color1 ? color1 : this._helper.green;
+ this._color2 = color2 ? color2 : this._helper.red;
+ // Make sure this is initted
+ this._helper.drawColor(this._canvas, this._color1);
+ this._stream = this._canvas.captureStream();
+ this._started = false;
+ }
+
+ stream() {
+ return this._stream;
+ }
+
+ helper() {
+ return this._helper;
+ }
+
+ colors(color1, color2) {
+ this._color1 = color1 ? color1 : this._helper.green;
+ this._color2 = color2 ? color2 : this._helper.red;
+ try {
+ this._helper.drawColor(this._canvas, this._color1);
+ } catch (e) {
+ // ignore; stream might have shut down
+ }
+ }
+
+ size(width, height) {
+ this._canvas.width = width;
+ this._canvas.height = height;
+ }
+
+ start() {
+ if (this._started) {
+ info("*** emitter already started");
+ return;
+ }
+
+ let i = 0;
+ this._started = true;
+ this._intervalId = setInterval(() => {
+ try {
+ this._helper.drawColor(this._canvas, i ? this._color1 : this._color2);
+ i = 1 - i;
+ } catch (e) {
+ // ignore; stream might have shut down, and we don't bother clearing
+ // the setInterval.
+ }
+ }, 500);
+ }
+
+ stop() {
+ if (this._started) {
+ clearInterval(this._intervalId);
+ this._started = false;
+ }
+ }
+}
+
+class VideoStreamHelper {
+ constructor() {
+ this._helper = new CaptureStreamTestHelper2D(50, 50);
+ }
+
+ async checkHasFrame(video, { offsetX, offsetY, threshold } = {}) {
+ const h = this._helper;
+ await h.waitForPixel(
+ video,
+ px => {
+ let result = h.isOpaquePixelNot(px, h.black, threshold);
+ info(
+ "Checking that we have a frame, got [" +
+ Array.from(px) +
+ "]. Ref=[" +
+ Array.from(h.black.data) +
+ "]. Threshold=" +
+ threshold +
+ ". Pass=" +
+ result
+ );
+ return result;
+ },
+ { offsetX, offsetY }
+ );
+ }
+
+ async checkVideoPlaying(
+ video,
+ { offsetX = 10, offsetY = 10, threshold = 16 } = {}
+ ) {
+ const h = this._helper;
+ await this.checkHasFrame(video, { offsetX, offsetY, threshold });
+ let startPixel = {
+ data: h.getPixel(video, offsetX, offsetY),
+ name: "startcolor",
+ };
+ await h.waitForPixel(
+ video,
+ px => {
+ let result = h.isPixelNot(px, startPixel, threshold);
+ info(
+ "Checking playing, [" +
+ Array.from(px) +
+ "] vs [" +
+ Array.from(startPixel.data) +
+ "]. Threshold=" +
+ threshold +
+ " Pass=" +
+ result
+ );
+ return result;
+ },
+ { offsetX, offsetY }
+ );
+ }
+
+ async checkVideoPaused(
+ video,
+ { offsetX = 10, offsetY = 10, threshold = 16, time = 5000 } = {}
+ ) {
+ const h = this._helper;
+ await this.checkHasFrame(video, { offsetX, offsetY, threshold });
+ let startPixel = {
+ data: h.getPixel(video, offsetX, offsetY),
+ name: "startcolor",
+ };
+ try {
+ await h.waitForPixel(
+ video,
+ px => {
+ let result = h.isOpaquePixelNot(px, startPixel, threshold);
+ info(
+ "Checking paused, [" +
+ Array.from(px) +
+ "] vs [" +
+ Array.from(startPixel.data) +
+ "]. Threshold=" +
+ threshold +
+ " Pass=" +
+ result
+ );
+ return result;
+ },
+ { offsetX, offsetY, cancel: wait(time, "timeout") }
+ );
+ ok(false, "Frame changed within " + time / 1000 + " seconds");
+ } catch (e) {
+ is(
+ e,
+ "timeout",
+ "Frame shouldn't change for " + time / 1000 + " seconds"
+ );
+ }
+ }
+}
+
+(function () {
+ var el = document.createElement("link");
+ el.rel = "stylesheet";
+ el.type = "text/css";
+ el.href = "/tests/SimpleTest/test.css";
+ document.head.appendChild(el);
+})();
diff --git a/dom/media/webrtc/tests/mochitests/helpers_from_wpt/sdp.js b/dom/media/webrtc/tests/mochitests/helpers_from_wpt/sdp.js
new file mode 100644
index 0000000000..6460f64a44
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/helpers_from_wpt/sdp.js
@@ -0,0 +1,889 @@
+/* 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,
+ };
+ }
+ 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,
+ };
+ }
+};
+
+// 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/dom/media/webrtc/tests/mochitests/iceTestUtils.js b/dom/media/webrtc/tests/mochitests/iceTestUtils.js
new file mode 100644
index 0000000000..d4d1f5c4b4
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/iceTestUtils.js
@@ -0,0 +1,302 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// This is mostly so test_peerConnection_gatherWithStun300.html and
+// test_peerConnection_gatherWithStun300IPv6 can share this code. I would have
+// put the ipv6 test code in the same file, but our ipv6 tester support is
+// inconsistent enough that we need to be able to track the ipv6 test
+// separately.
+
+async function findStatsRelayCandidates(pc, protocol) {
+ const stats = await pc.getStats();
+ return [...stats.values()].filter(
+ v =>
+ v.type == "local-candidate" &&
+ v.candidateType == "relay" &&
+ v.relayProtocol == protocol
+ );
+}
+
+// Trickles candidates if pcDst is set, and resolves the candidate list
+async function trickleIce(pc, pcDst) {
+ const candidates = [],
+ addCandidatePromises = [];
+ while (true) {
+ const { candidate } = await new Promise(r =>
+ pc.addEventListener("icecandidate", r, { once: true })
+ );
+ if (!candidate) {
+ break;
+ }
+ candidates.push(candidate);
+ if (pcDst) {
+ addCandidatePromises.push(pcDst.addIceCandidate(candidate));
+ }
+ }
+ await Promise.all(addCandidatePromises);
+ return candidates;
+}
+
+async function gather(pc) {
+ if (pc.signalingState == "stable") {
+ await pc.setLocalDescription(
+ await pc.createOffer({ offerToReceiveAudio: true })
+ );
+ } else if (pc.signalingState == "have-remote-offer") {
+ await pc.setLocalDescription();
+ }
+
+ return trickleIce(pc);
+}
+
+async function gatherWithTimeout(pc, timeout, context) {
+ const throwOnTimeout = async () => {
+ await wait(timeout);
+ throw new Error(
+ `Gathering did not complete within ${timeout} ms with ${context}`
+ );
+ };
+
+ return Promise.race([gather(pc), throwOnTimeout()]);
+}
+
+async function iceConnected(pc) {
+ return new Promise((resolve, reject) => {
+ pc.addEventListener("iceconnectionstatechange", () => {
+ if (["connected", "completed"].includes(pc.iceConnectionState)) {
+ resolve();
+ } else if (pc.iceConnectionState == "failed") {
+ reject(new Error(`ICE failed`));
+ }
+ });
+ });
+}
+
+// Set up trickle, but does not wait for it to complete. Can be used by itself
+// in cases where we do not expect any new candidates, but want to still set up
+// the signal handling in case new candidates _do_ show up.
+async function connectNoTrickleWait(offerer, answerer, timeout, context) {
+ return connect(offerer, answerer, timeout, context, true);
+}
+
+async function connect(
+ offerer,
+ answerer,
+ timeout,
+ context,
+ noTrickleWait = false
+) {
+ const trickle1 = trickleIce(offerer, answerer);
+ const trickle2 = trickleIce(answerer, offerer);
+ try {
+ const offer = await offerer.createOffer({ offerToReceiveAudio: true });
+ await offerer.setLocalDescription(offer);
+ await answerer.setRemoteDescription(offer);
+ const answer = await answerer.createAnswer();
+ await Promise.all([
+ offerer.setRemoteDescription(answer),
+ answerer.setLocalDescription(answer),
+ ]);
+
+ const throwOnTimeout = async () => {
+ if (timeout) {
+ await wait(timeout);
+ throw new Error(
+ `ICE did not complete within ${timeout} ms with ${context}`
+ );
+ }
+ };
+
+ await Promise.race([
+ Promise.all([iceConnected(offerer), iceConnected(answerer)]),
+ throwOnTimeout(timeout, context),
+ ]);
+ } finally {
+ if (!noTrickleWait) {
+ // TODO(bug 1751509): For now, we need to let gathering finish before we
+ // proceed, because there are races in ICE restart wrt gathering state.
+ await Promise.all([trickle1, trickle2]);
+ }
+ }
+}
+
+function isV6HostCandidate(candidate) {
+ const fields = candidate.candidate.split(" ");
+ const type = fields[7];
+ const ipAddress = fields[4];
+ return type == "host" && ipAddress.includes(":");
+}
+
+async function ipv6Supported() {
+ const pc = new RTCPeerConnection();
+ const candidates = await gatherWithTimeout(pc, 8000);
+ info(`baseline candidates: ${JSON.stringify(candidates)}`);
+ pc.close();
+ return candidates.some(isV6HostCandidate);
+}
+
+function makeContextString(iceServers) {
+ const currentRedirectAddress = SpecialPowers.getCharPref(
+ "media.peerconnection.nat_simulator.redirect_address",
+ ""
+ );
+ const currentRedirectTargets = SpecialPowers.getCharPref(
+ "media.peerconnection.nat_simulator.redirect_targets",
+ ""
+ );
+ return `redirect rule: ${currentRedirectAddress}=>${currentRedirectTargets} iceServers: ${JSON.stringify(
+ iceServers
+ )}`;
+}
+
+async function checkSrflx(iceServers) {
+ const context = makeContextString(iceServers);
+ info(`checkSrflx ${context}`);
+ const pc = new RTCPeerConnection({
+ iceServers,
+ bundlePolicy: "max-bundle", // Avoids extra candidates
+ });
+ const candidates = await gatherWithTimeout(pc, 8000, context);
+ const srflxCandidates = candidates.filter(c => c.candidate.includes("srflx"));
+ info(`candidates: ${JSON.stringify(srflxCandidates)}`);
+ // TODO(bug 1339203): Once we support rtcpMuxPolicy, set it to "require" to
+ // result in a single srflx candidate
+ is(
+ srflxCandidates.length,
+ 2,
+ `Should have two srflx candidates with ${context}`
+ );
+ pc.close();
+}
+
+async function checkNoSrflx(iceServers) {
+ const context = makeContextString(iceServers);
+ info(`checkNoSrflx ${context}`);
+ const pc = new RTCPeerConnection({
+ iceServers,
+ bundlePolicy: "max-bundle", // Avoids extra candidates
+ });
+ const candidates = await gatherWithTimeout(pc, 8000, context);
+ const srflxCandidates = candidates.filter(c => c.candidate.includes("srflx"));
+ info(`candidates: ${JSON.stringify(srflxCandidates)}`);
+ is(
+ srflxCandidates.length,
+ 0,
+ `Should have no srflx candidates with ${context}`
+ );
+ pc.close();
+}
+
+async function checkRelayUdp(iceServers) {
+ const context = makeContextString(iceServers);
+ info(`checkRelayUdp ${context}`);
+ const pc = new RTCPeerConnection({
+ iceServers,
+ bundlePolicy: "max-bundle", // Avoids extra candidates
+ });
+ const candidates = await gatherWithTimeout(pc, 8000, context);
+ const relayCandidates = candidates.filter(c => c.candidate.includes("relay"));
+ info(`candidates: ${JSON.stringify(relayCandidates)}`);
+ // TODO(bug 1339203): Once we support rtcpMuxPolicy, set it to "require" to
+ // result in a single relay candidate
+ is(
+ relayCandidates.length,
+ 2,
+ `Should have two relay candidates with ${context}`
+ );
+ // It would be nice if RTCIceCandidate had a field telling us what the
+ // "related protocol" is (similar to relatedAddress and relatedPort).
+ // Because there is no such thing, we need to go through the stats API,
+ // which _does_ have that information.
+ is(
+ (await findStatsRelayCandidates(pc, "tcp")).length,
+ 0,
+ `No TCP relay candidates should be present with ${context}`
+ );
+ pc.close();
+}
+
+async function checkRelayTcp(iceServers) {
+ const context = makeContextString(iceServers);
+ info(`checkRelayTcp ${context}`);
+ const pc = new RTCPeerConnection({
+ iceServers,
+ bundlePolicy: "max-bundle", // Avoids extra candidates
+ });
+ const candidates = await gatherWithTimeout(pc, 8000, context);
+ const relayCandidates = candidates.filter(c => c.candidate.includes("relay"));
+ info(`candidates: ${JSON.stringify(relayCandidates)}`);
+ // TODO(bug 1339203): Once we support rtcpMuxPolicy, set it to "require" to
+ // result in a single relay candidate
+ is(
+ relayCandidates.length,
+ 2,
+ `Should have two relay candidates with ${context}`
+ );
+ // It would be nice if RTCIceCandidate had a field telling us what the
+ // "related protocol" is (similar to relatedAddress and relatedPort).
+ // Because there is no such thing, we need to go through the stats API,
+ // which _does_ have that information.
+ is(
+ (await findStatsRelayCandidates(pc, "udp")).length,
+ 0,
+ `No UDP relay candidates should be present with ${context}`
+ );
+ pc.close();
+}
+
+async function checkRelayUdpTcp(iceServers) {
+ const context = makeContextString(iceServers);
+ info(`checkRelayUdpTcp ${context}`);
+ const pc = new RTCPeerConnection({
+ iceServers,
+ bundlePolicy: "max-bundle", // Avoids extra candidates
+ });
+ const candidates = await gatherWithTimeout(pc, 8000, context);
+ const relayCandidates = candidates.filter(c => c.candidate.includes("relay"));
+ info(`candidates: ${JSON.stringify(relayCandidates)}`);
+ // TODO(bug 1339203): Once we support rtcpMuxPolicy, set it to "require" to
+ // result in a single relay candidate each for UDP and TCP
+ is(
+ relayCandidates.length,
+ 4,
+ `Should have two relay candidates for each protocol with ${context}`
+ );
+ // It would be nice if RTCIceCandidate had a field telling us what the
+ // "related protocol" is (similar to relatedAddress and relatedPort).
+ // Because there is no such thing, we need to go through the stats API,
+ // which _does_ have that information.
+ is(
+ (await findStatsRelayCandidates(pc, "udp")).length,
+ 2,
+ `Two UDP relay candidates should be present with ${context}`
+ );
+ // TODO(bug 1705563): This is 1 because of bug 1705563
+ is(
+ (await findStatsRelayCandidates(pc, "tcp")).length,
+ 1,
+ `One TCP relay candidates should be present with ${context}`
+ );
+ pc.close();
+}
+
+async function checkNoRelay(iceServers) {
+ const context = makeContextString(iceServers);
+ info(`checkNoRelay ${context}`);
+ const pc = new RTCPeerConnection({
+ iceServers,
+ bundlePolicy: "max-bundle", // Avoids extra candidates
+ });
+ const candidates = await gatherWithTimeout(pc, 8000, context);
+ const relayCandidates = candidates.filter(c => c.candidate.includes("relay"));
+ info(`candidates: ${JSON.stringify(relayCandidates)}`);
+ is(
+ relayCandidates.length,
+ 0,
+ `Should have no relay candidates with ${context}`
+ );
+ pc.close();
+}
diff --git a/dom/media/webrtc/tests/mochitests/identity/identityPcTest.js b/dom/media/webrtc/tests/mochitests/identity/identityPcTest.js
new file mode 100644
index 0000000000..1381873f9d
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/identity/identityPcTest.js
@@ -0,0 +1,79 @@
+function identityPcTest(remoteOptions) {
+ var user = "someone";
+ var domain1 = "test1.example.com";
+ var domain2 = "test2.example.com";
+ var id1 = user + "@" + domain1;
+ var id2 = user + "@" + domain2;
+
+ test = new PeerConnectionTest({
+ config_local: {
+ peerIdentity: id2,
+ },
+ config_remote: {
+ peerIdentity: id1,
+ },
+ });
+ test.setMediaConstraints(
+ [
+ {
+ audio: true,
+ video: true,
+ peerIdentity: id2,
+ },
+ ],
+ [
+ remoteOptions || {
+ audio: true,
+ video: true,
+ peerIdentity: id1,
+ },
+ ]
+ );
+ test.pcLocal.setIdentityProvider("test1.example.com", { protocol: "idp.js" });
+ test.pcRemote.setIdentityProvider("test2.example.com", {
+ protocol: "idp.js",
+ });
+ test.chain.append([
+ function PEER_IDENTITY_IS_SET_CORRECTLY(test) {
+ // no need to wait to check identity in this case,
+ // setRemoteDescription should wait for the IdP to complete
+ function checkIdentity(pc, pfx, idp, name) {
+ return pc.peerIdentity.then(peerInfo => {
+ is(peerInfo.idp, idp, pfx + "IdP check");
+ is(peerInfo.name, name + "@" + idp, pfx + "identity check");
+ });
+ }
+
+ return Promise.all([
+ checkIdentity(
+ test.pcLocal._pc,
+ "local: ",
+ "test2.example.com",
+ "someone"
+ ),
+ checkIdentity(
+ test.pcRemote._pc,
+ "remote: ",
+ "test1.example.com",
+ "someone"
+ ),
+ ]);
+ },
+
+ function REMOTE_STREAMS_ARE_RESTRICTED(test) {
+ var remoteStream = test.pcLocal._pc.getRemoteStreams()[0];
+ for (const track of remoteStream.getTracks()) {
+ mustThrowWith(
+ `Freshly received ${track.kind} track with peerIdentity`,
+ "SecurityError",
+ () => new MediaRecorder(new MediaStream([track])).start()
+ );
+ }
+ return Promise.all([
+ audioIsSilence(true, remoteStream),
+ videoIsBlack(true, remoteStream),
+ ]);
+ },
+ ]);
+ return test.run();
+}
diff --git a/dom/media/webrtc/tests/mochitests/identity/idp-bad.js b/dom/media/webrtc/tests/mochitests/identity/idp-bad.js
new file mode 100644
index 0000000000..86e1cb7a34
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/identity/idp-bad.js
@@ -0,0 +1 @@
+<This isn't valid JS>
diff --git a/dom/media/webrtc/tests/mochitests/identity/idp-min.js b/dom/media/webrtc/tests/mochitests/identity/idp-min.js
new file mode 100644
index 0000000000..a4b2c55cee
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/identity/idp-min.js
@@ -0,0 +1,24 @@
+(function (global) {
+ "use strict";
+ // A minimal implementation of the interface.
+ // Though this isn't particularly functional.
+ // This is needed so that we can have a "working" IdP served
+ // from two different locations in the tree.
+ global.rtcIdentityProvider.register({
+ generateAssertion(payload, origin, usernameHint) {
+ dump("idp: generateAssertion(" + payload + ")\n");
+ return Promise.resolve({
+ idp: { domain: "example.com", protocol: "idp.js" },
+ assertion: "bogus",
+ });
+ },
+
+ validateAssertion(assertion, origin) {
+ dump("idp: validateAssertion(" + assertion + ")\n");
+ return Promise.resolve({
+ identity: "user@example.com",
+ contents: "bogus",
+ });
+ },
+ });
+})(this);
diff --git a/dom/media/webrtc/tests/mochitests/identity/idp-redirect-http-trick.js b/dom/media/webrtc/tests/mochitests/identity/idp-redirect-http-trick.js
new file mode 100644
index 0000000000..75390cbf4f
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/identity/idp-redirect-http-trick.js
@@ -0,0 +1,3 @@
+(function () {
+ dump("ERROR\n");
+})();
diff --git a/dom/media/webrtc/tests/mochitests/identity/idp-redirect-http-trick.js^headers^ b/dom/media/webrtc/tests/mochitests/identity/idp-redirect-http-trick.js^headers^
new file mode 100644
index 0000000000..b3a2afd90a
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/identity/idp-redirect-http-trick.js^headers^
@@ -0,0 +1,2 @@
+HTTP 301 Moved Permanently
+Location: http://example.com/.well-known/idp-proxy/idp-redirect-https.js
diff --git a/dom/media/webrtc/tests/mochitests/identity/idp-redirect-http.js b/dom/media/webrtc/tests/mochitests/identity/idp-redirect-http.js
new file mode 100644
index 0000000000..75390cbf4f
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/identity/idp-redirect-http.js
@@ -0,0 +1,3 @@
+(function () {
+ dump("ERROR\n");
+})();
diff --git a/dom/media/webrtc/tests/mochitests/identity/idp-redirect-http.js^headers^ b/dom/media/webrtc/tests/mochitests/identity/idp-redirect-http.js^headers^
new file mode 100644
index 0000000000..d2380984e7
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/identity/idp-redirect-http.js^headers^
@@ -0,0 +1,2 @@
+HTTP 301 Moved Permanently
+Location: http://example.com/.well-known/idp-proxy/idp.js
diff --git a/dom/media/webrtc/tests/mochitests/identity/idp-redirect-https-double.js b/dom/media/webrtc/tests/mochitests/identity/idp-redirect-https-double.js
new file mode 100644
index 0000000000..75390cbf4f
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/identity/idp-redirect-https-double.js
@@ -0,0 +1,3 @@
+(function () {
+ dump("ERROR\n");
+})();
diff --git a/dom/media/webrtc/tests/mochitests/identity/idp-redirect-https-double.js^headers^ b/dom/media/webrtc/tests/mochitests/identity/idp-redirect-https-double.js^headers^
new file mode 100644
index 0000000000..3fb8a35ae7
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/identity/idp-redirect-https-double.js^headers^
@@ -0,0 +1,2 @@
+HTTP 301 Moved Permanently
+Location: https://example.com/.well-known/idp-proxy/idp-redirect-https.js
diff --git a/dom/media/webrtc/tests/mochitests/identity/idp-redirect-https-odd-path.js b/dom/media/webrtc/tests/mochitests/identity/idp-redirect-https-odd-path.js
new file mode 100644
index 0000000000..75390cbf4f
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/identity/idp-redirect-https-odd-path.js
@@ -0,0 +1,3 @@
+(function () {
+ dump("ERROR\n");
+})();
diff --git a/dom/media/webrtc/tests/mochitests/identity/idp-redirect-https-odd-path.js^headers^ b/dom/media/webrtc/tests/mochitests/identity/idp-redirect-https-odd-path.js^headers^
new file mode 100644
index 0000000000..6e2931eda9
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/identity/idp-redirect-https-odd-path.js^headers^
@@ -0,0 +1,2 @@
+HTTP 301 Moved Permanently
+Location: https://example.com/.well-known/idp-min.js
diff --git a/dom/media/webrtc/tests/mochitests/identity/idp-redirect-https.js b/dom/media/webrtc/tests/mochitests/identity/idp-redirect-https.js
new file mode 100644
index 0000000000..75390cbf4f
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/identity/idp-redirect-https.js
@@ -0,0 +1,3 @@
+(function () {
+ dump("ERROR\n");
+})();
diff --git a/dom/media/webrtc/tests/mochitests/identity/idp-redirect-https.js^headers^ b/dom/media/webrtc/tests/mochitests/identity/idp-redirect-https.js^headers^
new file mode 100644
index 0000000000..77d56ac442
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/identity/idp-redirect-https.js^headers^
@@ -0,0 +1,2 @@
+HTTP 301 Moved Permanently
+Location: https://example.com/.well-known/idp-proxy/idp.js
diff --git a/dom/media/webrtc/tests/mochitests/identity/idp.js b/dom/media/webrtc/tests/mochitests/identity/idp.js
new file mode 100644
index 0000000000..557740657f
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/identity/idp.js
@@ -0,0 +1,119 @@
+(function (global) {
+ "use strict";
+
+ // rather than create a million different IdP configurations and litter the
+ // world with files all containing near-identical code, let's use the hash/URL
+ // fragment as a way of generating instructions for the IdP
+ var instructions = global.location.hash.replace("#", "").split(":");
+ function is(target) {
+ return function (instruction) {
+ return instruction === target;
+ };
+ }
+
+ function IDPJS() {
+ this.domain = global.location.host;
+ var path = global.location.pathname;
+ this.protocol =
+ path.substring(path.lastIndexOf("/") + 1) + global.location.hash;
+ this.id = crypto.getRandomValues(new Uint8Array(10)).join(".");
+ }
+
+ IDPJS.prototype = {
+ getLogin() {
+ return fetch(
+ "https://example.com/.well-known/idp-proxy/idp.sjs?" + this.id
+ ).then(response => response.status === 200);
+ },
+ checkLogin(result) {
+ return this.getLogin().then(loggedIn => {
+ if (loggedIn) {
+ return result;
+ }
+ return Promise.reject({
+ name: "IdpLoginError",
+ loginUrl:
+ "https://example.com/.well-known/idp-proxy/login.html#" + this.id,
+ });
+ });
+ },
+
+ borkResult(result) {
+ if (instructions.some(is("throw"))) {
+ throw new Error("Throwing!");
+ }
+ if (instructions.some(is("fail"))) {
+ return Promise.reject(new Error("Failing!"));
+ }
+ if (instructions.some(is("login"))) {
+ return this.checkLogin(result);
+ }
+ if (instructions.some(is("hang"))) {
+ return new Promise(r => {});
+ }
+ dump("idp: result=" + JSON.stringify(result) + "\n");
+ return Promise.resolve(result);
+ },
+
+ _selectUsername(usernameHint) {
+ dump("_selectUsername: usernameHint(" + usernameHint + ")\n");
+ var username = "someone@" + this.domain;
+ if (usernameHint) {
+ var at = usernameHint.indexOf("@");
+ if (at < 0) {
+ username = usernameHint + "@" + this.domain;
+ } else if (usernameHint.substring(at + 1) === this.domain) {
+ username = usernameHint;
+ }
+ }
+ return username;
+ },
+
+ generateAssertion(payload, origin, options) {
+ dump(
+ "idp: generateAssertion(" +
+ payload +
+ ", " +
+ origin +
+ ", " +
+ JSON.stringify(options) +
+ ")\n"
+ );
+ var idpDetails = {
+ domain: this.domain,
+ protocol: this.protocol,
+ };
+ if (instructions.some(is("bad-assert"))) {
+ idpDetails = {};
+ }
+ return this.borkResult({
+ idp: idpDetails,
+ assertion: JSON.stringify({
+ username: this._selectUsername(options.usernameHint),
+ contents: payload,
+ }),
+ });
+ },
+
+ validateAssertion(assertion, origin) {
+ dump("idp: validateAssertion(" + assertion + ")\n");
+ var assertion = JSON.parse(assertion);
+ if (instructions.some(is("bad-validate"))) {
+ assertion.contents = {};
+ }
+ return this.borkResult({
+ identity: assertion.username,
+ contents: assertion.contents,
+ });
+ },
+ };
+
+ if (!instructions.some(is("not_ready"))) {
+ dump("registering idp.js" + global.location.hash + "\n");
+ var idp = new IDPJS();
+ global.rtcIdentityProvider.register({
+ generateAssertion: idp.generateAssertion.bind(idp),
+ validateAssertion: idp.validateAssertion.bind(idp),
+ });
+ }
+})(this);
diff --git a/dom/media/webrtc/tests/mochitests/identity/idp.sjs b/dom/media/webrtc/tests/mochitests/identity/idp.sjs
new file mode 100644
index 0000000000..e1a245be78
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/identity/idp.sjs
@@ -0,0 +1,18 @@
+function handleRequest(request, response) {
+ var key = "/.well-known/idp-proxy/" + request.queryString;
+ dump(getState(key) + "\n");
+ if (request.method === "GET") {
+ if (getState(key)) {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ } else {
+ response.setStatusLine(request.httpVersion, 404, "Not Found");
+ }
+ } else if (request.method === "PUT") {
+ setState(key, "OK");
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ } else {
+ response.setStatusLine(request.httpVersion, 406, "Method Not Allowed");
+ }
+ response.setHeader("Content-Type", "text/plain;charset=UTF-8");
+ response.write("OK");
+}
diff --git a/dom/media/webrtc/tests/mochitests/identity/login.html b/dom/media/webrtc/tests/mochitests/identity/login.html
new file mode 100644
index 0000000000..eafba22f2d
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/identity/login.html
@@ -0,0 +1,31 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Identity Provider Login</title>
+ <script type="application/javascript">
+ window.onload = () => {
+ var xhr = new XMLHttpRequest();
+ xhr.open("PUT", "https://example.com/.well-known/idp-proxy/idp.sjs?" +
+ window.location.hash.replace('#', ''));
+ xhr.onload = () => {
+ var isFramed = (window !== window.top);
+ var parent = isFramed ? window.parent : window.opener;
+ // Using '*' is cheating, but that's OK.
+ parent.postMessage('LOGINDONE', '*');
+ var done = document.createElement('div');
+
+ done.textContent = 'Done';
+ document.body.appendChild(done);
+
+ if (!isFramed) {
+ window.close();
+ }
+ };
+ xhr.send();
+ };
+ </script>
+</head>
+<body>
+ <div>Logging in...</div>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/identity/mochitest.ini b/dom/media/webrtc/tests/mochitests/identity/mochitest.ini
new file mode 100644
index 0000000000..7a60cc6c6e
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/identity/mochitest.ini
@@ -0,0 +1,47 @@
+[DEFAULT]
+subsuite = media
+skip-if = (os == 'linux' && !debug)
+support-files =
+ /.well-known/idp-proxy/idp.js
+ identityPcTest.js
+ !/dom/media/webrtc/tests/mochitests/blacksilence.js
+ !/dom/media/webrtc/tests/mochitests/dataChannel.js
+ !/dom/media/webrtc/tests/mochitests/head.js
+ !/dom/media/webrtc/tests/mochitests/network.js
+ !/dom/media/webrtc/tests/mochitests/pc.js
+ !/dom/media/webrtc/tests/mochitests/sdpUtils.js
+ !/dom/media/webrtc/tests/mochitests/templates.js
+ !/dom/media/webrtc/tests/mochitests/turnConfig.js
+tags = mtg
+
+[test_fingerprints.html]
+scheme=https
+[test_getIdentityAssertion.html]
+[test_idpproxy.html]
+support-files =
+ /.well-known/idp-proxy/idp-redirect-http.js
+ /.well-known/idp-proxy/idp-redirect-http.js^headers^
+ /.well-known/idp-proxy/idp-redirect-http-trick.js
+ /.well-known/idp-proxy/idp-redirect-http-trick.js^headers^
+ /.well-known/idp-proxy/idp-redirect-https.js
+ /.well-known/idp-proxy/idp-redirect-https.js^headers^
+ /.well-known/idp-proxy/idp-redirect-https-double.js
+ /.well-known/idp-proxy/idp-redirect-https-double.js^headers^
+ /.well-known/idp-proxy/idp-redirect-https-odd-path.js
+ /.well-known/idp-proxy/idp-redirect-https-odd-path.js^headers^
+ /.well-known/idp-min.js
+ /.well-known/idp-proxy/idp-bad.js
+[test_loginNeeded.html]
+support-files =
+ /.well-known/idp-proxy/login.html
+ /.well-known/idp-proxy/idp.sjs
+[test_peerConnection_asymmetricIsolation.html]
+scheme=https
+skip-if = os == 'android'
+[test_peerConnection_peerIdentity.html]
+scheme=https
+skip-if = os == 'android'
+[test_setIdentityProvider.html]
+scheme=https
+[test_setIdentityProviderWithErrors.html]
+scheme=https
diff --git a/dom/media/webrtc/tests/mochitests/identity/test_fingerprints.html b/dom/media/webrtc/tests/mochitests/identity/test_fingerprints.html
new file mode 100644
index 0000000000..0a7f0a2033
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/identity/test_fingerprints.html
@@ -0,0 +1,91 @@
+<html>
+<head>
+<meta charset="utf-8" />
+<script type="application/javascript">var scriptRelativePath = "../";</script>
+<script type="application/javascript" src="../pc.js"></script>
+</head>
+<body>
+<script class="testbody" type="application/javascript">
+createHTML({ title: "Test multiple identity fingerprints", bug: "1005152" });
+
+// here we call the identity provider directly
+async function getIdentityAssertion(fingerprint) {
+ const { IdpSandbox } = SpecialPowers.ChromeUtils.import(
+ 'resource://gre/modules/media/IdpSandbox.jsm'
+ );
+ const sandbox = new IdpSandbox('example.com', 'idp.js', window);
+ const idp = SpecialPowers.wrap(await sandbox.start());
+ const assertion = SpecialPowers.wrap(await
+ idp.generateAssertion(JSON.stringify({ fingerprint }),
+ 'https://example.com',
+ {}));
+ const assertionString = btoa(JSON.stringify(assertion));
+ sandbox.stop();
+ return assertionString;
+}
+
+// This takes a real fingerprint and makes some extra bad ones.
+function makeFingerprints(algorithm, digest) {
+ const fingerprints = [];
+ fingerprints.push({ algorithm, digest });
+ for (var i = 0; i < 3; ++i) {
+ fingerprints.push({
+ algorithm,
+ digest: digest.replace(/:./g, ':' + i.toString(16))
+ });
+ }
+ return fingerprints;
+}
+
+const fingerprintRegex = /^a=fingerprint:(\S+) (\S+)/m;
+const identityRegex = /^a=identity:(\S+)/m;
+
+function fingerprintSdp(fingerprints) {
+ return fingerprints.map(fp => 'a=fInGeRpRiNt:' + fp.algorithm +
+ ' ' + fp.digest + '\n').join('');
+}
+
+// Firefox only uses a single fingerprint.
+// That doesn't mean we have it create SDP that describes two.
+// This function synthesizes that SDP and tries to set it.
+
+runNetworkTest(async () => {
+ // this one fails setRemoteDescription if the identity is not good
+ const pcStrict = new RTCPeerConnection({ peerIdentity: 'someone@example.com'});
+ // this one will be manually tweaked to have two fingerprints
+ const pcDouble = new RTCPeerConnection({});
+
+ const stream = await getUserMedia({ video: true });
+ ok(stream, 'Got test stream');
+ const [track] = stream.getTracks();
+ pcDouble.addTrack(track, stream);
+ try {
+ const offer = await pcDouble.createOffer();
+ ok(offer, 'Got offer');
+ const match = offer.sdp.match(fingerprintRegex);
+ if (!match) {
+ throw new Error('No fingerprint in offer SDP');
+ }
+ const fingerprints = makeFingerprints(match[1], match[2]);
+ const assertion = await getIdentityAssertion(fingerprints);
+ ok(assertion, 'Should have assertion');
+
+ const sdp = offer.sdp.slice(0, match.index) +
+ 'a=identity:' + assertion + '\n' +
+ fingerprintSdp(fingerprints.slice(1)) +
+ offer.sdp.slice(match.index);
+
+ await pcStrict.setRemoteDescription({ type: 'offer', sdp });
+ ok(true, 'Modified fingerprints were accepted');
+ } catch (error) {
+ const e = SpecialPowers.wrap(error);
+ ok(false, 'error in test: ' +
+ (e.message ? (e.message + '\n' + e.stack) : e));
+ }
+ pcStrict.close();
+ pcDouble.close();
+ track.stop();
+});
+</script>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/identity/test_getIdentityAssertion.html b/dom/media/webrtc/tests/mochitests/identity/test_getIdentityAssertion.html
new file mode 100644
index 0000000000..47e1cb1df6
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/identity/test_getIdentityAssertion.html
@@ -0,0 +1,101 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript">var scriptRelativePath = "../";</script>
+ <script type="application/javascript" src="../pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ title: "getIdentityAssertion Tests",
+ bug: "942367"
+ });
+
+function checkIdentity(assertion, identity) {
+ // here we dig into the payload, which means we need to know something
+ // about how the IdP actually works (not good in general, but OK here)
+ var assertion = JSON.parse(atob(assertion)).assertion;
+ var user = JSON.parse(assertion).username;
+ is(user, identity, 'id should be "' + identity + '" is "' + user + '"');
+}
+
+function getAssertion(t, instructions, userHint) {
+ dump('instructions: ' + instructions + '\n');
+ dump('userHint: ' + userHint + '\n');
+ t.pcLocal.setIdentityProvider('example.com',
+ { protocol: 'idp.js' + instructions,
+ usernameHint: userHint });
+ return t.pcLocal._pc.getIdentityAssertion();
+}
+
+var test;
+function theTest() {
+ test = new PeerConnectionTest();
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ test.chain.removeAfter('PC_REMOTE_CHECK_INITIAL_SIGNALINGSTATE');
+ test.chain.append([
+ function PC_LOCAL_IDENTITY_ASSERTION_FAILS_WITHOUT_PROVIDER(t) {
+ return t.pcLocal._pc.getIdentityAssertion()
+ .then(a => ok(false, 'should fail without provider'),
+ e => ok(e, 'should fail without provider'));
+ },
+
+ function PC_LOCAL_IDENTITY_ASSERTION_FAILS_WITH_BAD_PROVIDER(t) {
+ t.pcLocal._pc.setIdentityProvider('example.com',
+ { protocol: 'idp-bad.js',
+ usernameHint: '' });
+ return t.pcLocal._pc.getIdentityAssertion()
+ .then(a => ok(false, 'should fail with bad provider'),
+ e => {
+ is(e.name, 'IdpError', 'should fail with bad provider');
+ ok(e.message, 'should include a nice message');
+ });
+ },
+
+ function PC_LOCAL_GET_TWO_ASSERTIONS(t) {
+ return Promise.all([
+ getAssertion(t, ''),
+ getAssertion(t, '')
+ ]).then(assertions => {
+ is(assertions.length, 2, "Two assertions generated");
+ assertions.forEach(a => checkIdentity(a, 'someone@example.com'));
+ });
+ },
+
+ function PC_LOCAL_IDP_FAILS(t) {
+ return getAssertion(t, '#fail')
+ .then(a => ok(false, '#fail should not get an identity result'),
+ e => is(e.name, 'IdpError', '#fail should cause rejection'));
+ },
+
+ function PC_LOCAL_IDP_LOGIN_ERROR(t) {
+ return getAssertion(t, '#login')
+ .then(a => ok(false, '#login should not work'),
+ e => {
+ is(e.name, 'IdpLoginError', 'name is IdpLoginError');
+ is(t.pcLocal._pc.idpLoginUrl.split('#')[0],
+ 'https://example.com/.well-known/idp-proxy/login.html',
+ 'got the right login URL from the IdP');
+ });
+ },
+
+ function PC_LOCAL_IDP_NOT_READY(t) {
+ return getAssertion(t, '#not_ready')
+ .then(a => ok(false, '#not_ready should not get an identity result'),
+ e => is(e.name, 'IdpError', '#not_ready should cause rejection'));
+ },
+
+ function PC_LOCAL_ASSERTION_WITH_SPECIFIC_NAME(t) {
+ return getAssertion(t, '', 'user@example.com')
+ .then(a => checkIdentity(a, 'user@example.com'));
+ }
+ ]);
+ return test.run();
+}
+runNetworkTest(theTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/identity/test_idpproxy.html b/dom/media/webrtc/tests/mochitests/identity/test_idpproxy.html
new file mode 100644
index 0000000000..065501b8a4
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/identity/test_idpproxy.html
@@ -0,0 +1,178 @@
+<html>
+<head>
+<meta charset="utf-8" />
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+</head>
+<body>
+ <script class="testbody" type="application/javascript">
+"use strict";
+var { IdpSandbox } = SpecialPowers.ChromeUtils.import(
+ "resource://gre/modules/media/IdpSandbox.jsm"
+);
+var dummyPayload = JSON.stringify({
+ this: 'is',
+ a: ['stu', 6],
+ obj: null
+});
+
+function test_domain_sandbox() {
+ var diabolical = {
+ toString() {
+ return 'example.com/path';
+ }
+ };
+ var domains = [ 'ex/foo', 'user@ex', 'user:pass@ex', 'ex#foo', 'ex?foo',
+ '', 12, null, diabolical, true ];
+ domains.forEach(function(domain) {
+ try {
+ var idp = new IdpSandbox(domain, undefined, window);
+ ok(false, 'IdpSandbox allowed a bad domain: ' + domain);
+ } catch (e) {
+ var str = (typeof domain === 'string') ? domain : typeof domain;
+ ok(true, 'Evil domain "' + str + '" raises exception');
+ }
+ });
+}
+
+function test_protocol_sandbox() {
+ var protos = [ '../evil/proto', '..%2Fevil%2Fproto',
+ '\\evil', '%5cevil', 12, true, {} ];
+ protos.forEach(function(proto) {
+ try {
+ var idp = new IdpSandbox('example.com', proto, window);
+ ok(false, 'IdpSandbox allowed a bad protocol: ' + proto);
+ } catch (e) {
+ var str = (typeof proto === 'string') ? proto : typeof proto;
+ ok(true, 'Evil protocol "' + proto + '" raises exception');
+ }
+ });
+}
+
+function idpName(hash) {
+ return 'idp.js' + (hash ? ('#' + hash) : '');
+}
+
+function makeSandbox(js) {
+ var name = js || idpName();
+ info('Creating a sandbox for the protocol: ' + name);
+ var sandbox = new IdpSandbox('example.com', name, window);
+ return sandbox.start().then(idp => SpecialPowers.wrap(idp));
+}
+
+function test_generate_assertion() {
+ return makeSandbox()
+ .then(idp => idp.generateAssertion(dummyPayload,
+ 'https://example.net',
+ {}))
+ .then(response => {
+ response = SpecialPowers.wrap(response);
+ is(response.idp.domain, 'example.com', 'domain is correct');
+ is(response.idp.protocol, 'idp.js', 'protocol is correct');
+ ok(typeof response.assertion === 'string', 'assertion is present');
+ });
+}
+
+// test that the test IdP can eat its own dogfood; which is the only way to test
+// validateAssertion, since that consumes the output of generateAssertion (in
+// theory, generateAssertion could identify a different IdP domain).
+
+function test_validate_assertion() {
+ return makeSandbox()
+ .then(idp => idp.generateAssertion(dummyPayload,
+ 'https://example.net',
+ { usernameHint: 'user' }))
+ .then(assertion => {
+ var wrapped = SpecialPowers.wrap(assertion);
+ return makeSandbox()
+ .then(idp => idp.validateAssertion(wrapped.assertion,
+ 'https://example.net'));
+ }).then(response => {
+ response = SpecialPowers.wrap(response);
+ is(response.identity, 'user@example.com');
+ is(response.contents, dummyPayload);
+ });
+}
+
+// We don't want to test the #bad or the #hang instructions,
+// errors of the sort those generate aren't handled by the sandbox code.
+function test_assertion_failure(reason) {
+ return () => {
+ return makeSandbox(idpName(reason))
+ .then(idp => idp.generateAssertion('hello', 'example.net', {}))
+ .then(r => ok(false, 'should not succeed on ' + reason),
+ e => ok(true, 'failed correctly on ' + reason));
+ };
+}
+
+function test_load_failure() {
+ return makeSandbox('non-existent-file')
+ .then(() => ok(false, 'Should fail to load non-existent file'),
+ e => ok(e, 'Should fail to load non-existent file'));
+}
+
+function test_redirect_ok(from) {
+ return () => {
+ return makeSandbox(from)
+ .then(idp => idp.generateAssertion('hello', 'example.net'))
+ .then(r => ok(SpecialPowers.wrap(r).assertion,
+ 'Redirect to https should be OK'));
+ };
+}
+
+function test_redirect_fail(from) {
+ return () => {
+ return makeSandbox(from)
+ .then(() => ok(false, 'Redirect to https should fail'),
+ e => ok(e, 'Redirect to https should fail'));
+ };
+}
+
+function test_bad_js() {
+ return makeSandbox('idp-bad.js')
+ .then(() => ok(false, 'Bad JS should not load'),
+ e => ok(e, 'Bad JS should not load'));
+}
+
+function run_all_tests() {
+ [
+ test_domain_sandbox,
+ test_protocol_sandbox,
+ test_generate_assertion,
+ test_validate_assertion,
+
+ // fail of the IdP fails
+ test_assertion_failure('fail'),
+ // fail if the IdP throws
+ test_assertion_failure('throw'),
+ // fail if the IdP is not ready
+ test_assertion_failure('not_ready'),
+
+ test_load_failure(),
+ // Test a redirect to an HTTPS origin, which should be OK
+ test_redirect_ok('idp-redirect-https.js'),
+ // Two redirects is fine too
+ test_redirect_ok('idp-redirect-https-double.js'),
+ // A secure redirect to a path other than /.well-known/idp-proxy/* should
+ // also work fine.
+ test_redirect_ok('idp-redirect-https-odd-path.js'),
+ // A redirect to HTTP is not-cool
+ test_redirect_fail('idp-redirect-http.js'),
+ // Also catch tricks like https->http->https
+ test_redirect_fail('idp-redirect-http-trick.js'),
+
+ test_bad_js
+ ].reduce((p, test) => {
+ return p.then(test)
+ .catch(e => ok(false, test.name + ' failed: ' +
+ SpecialPowers.wrap(e).message + '\n' +
+ SpecialPowers.wrap(e).stack));
+ }, Promise.resolve())
+ .then(() => SimpleTest.finish());
+}
+
+SimpleTest.waitForExplicitFinish();
+run_all_tests();
+</script>
+ </body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/identity/test_loginNeeded.html b/dom/media/webrtc/tests/mochitests/identity/test_loginNeeded.html
new file mode 100644
index 0000000000..550dc20d92
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/identity/test_loginNeeded.html
@@ -0,0 +1,72 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript">var scriptRelativePath = "../";</script>
+ <script type="application/javascript" src="../pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ title: 'RTCPeerConnection identity with login',
+ bug: '1153314'
+ });
+
+function waitForLoginDone() {
+ return new Promise(resolve => {
+ window.addEventListener('message', function listener(e) {
+ is(e.origin, 'https://example.com', 'got the right message origin');
+ is(e.data, 'LOGINDONE', 'got the right message');
+ window.removeEventListener('message', listener);
+ resolve();
+ });
+ });
+}
+
+function checkLogin(t, name, onLoginNeeded) {
+ t.pcLocal.setIdentityProvider('example.com',
+ { protocol: 'idp.js#login:' + name });
+ return t.pcLocal._pc.getIdentityAssertion()
+ .then(a => ok(false, 'should request login'),
+ e => {
+ is(e.name, 'IdpLoginError', 'name is IdpLoginError');
+ is(t.pcLocal._pc.idpLoginUrl.split('#')[0],
+ 'https://example.com/.well-known/idp-proxy/login.html',
+ 'got the right login URL from the IdP');
+ return t.pcLocal._pc.idpLoginUrl;
+ })
+ .then(onLoginNeeded)
+ .then(waitForLoginDone)
+ .then(() => t.pcLocal._pc.getIdentityAssertion())
+ .then(a => ok(a, 'got assertion'));
+}
+
+function theTest() {
+ var test = new PeerConnectionTest();
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ test.chain.removeAfter('PC_REMOTE_CHECK_INITIAL_SIGNALINGSTATE');
+ test.chain.append([
+ function PC_LOCAL_IDENTITY_ASSERTION_WITH_IFRAME_LOGIN(t) {
+ return checkLogin(t, 'iframe', loginUrl => {
+ var iframe = document.createElement('iframe');
+ iframe.setAttribute('src', loginUrl);
+ iframe.frameBorder = 0;
+ iframe.width = 400;
+ iframe.height = 60;
+ document.getElementById('display').appendChild(iframe);
+ });
+ },
+ function PC_LOCAL_IDENTITY_ASSERTION_WITH_WINDOW_LOGIN(t) {
+ return checkLogin(t, 'openwin', loginUrl => {
+ window.open(loginUrl, 'login', 'width=400,height=60');
+ });
+ }
+ ]);
+ return test.run();
+}
+runNetworkTest(theTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/identity/test_peerConnection_asymmetricIsolation.html b/dom/media/webrtc/tests/mochitests/identity/test_peerConnection_asymmetricIsolation.html
new file mode 100644
index 0000000000..65a2fc5392
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/identity/test_peerConnection_asymmetricIsolation.html
@@ -0,0 +1,31 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript">var scriptRelativePath = "../";</script>
+ <script type="application/javascript" src="../pc.js"></script>
+ <script type="application/javascript" src="../blacksilence.js"></script>
+ <script type="application/javascript" src="identityPcTest.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+ title: "Non-isolated media entering an isolated session becomes isolated",
+ bug: "996238"
+});
+
+function theTest() {
+ // Override the remote media capture options to remove isolation for the
+ // remote party; the test verifies that the media it receives on the local
+ // side is isolated anyway.
+ return identityPcTest({
+ audio: true,
+ video: true
+ });
+}
+runNetworkTest(theTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/identity/test_peerConnection_peerIdentity.html b/dom/media/webrtc/tests/mochitests/identity/test_peerConnection_peerIdentity.html
new file mode 100644
index 0000000000..a8116cc451
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/identity/test_peerConnection_peerIdentity.html
@@ -0,0 +1,21 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript">var scriptRelativePath = "../";</script>
+ <script type="application/javascript" src="../pc.js"></script>
+ <script type="application/javascript" src="../blacksilence.js"></script>
+ <script type="application/javascript" src="identityPcTest.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+ title: "setIdentityProvider leads to peerIdentity and assertions in SDP",
+ bug: "942367"
+});
+
+runNetworkTest(identityPcTest);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/identity/test_setIdentityProvider.html b/dom/media/webrtc/tests/mochitests/identity/test_setIdentityProvider.html
new file mode 100644
index 0000000000..ac7cba6a5e
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/identity/test_setIdentityProvider.html
@@ -0,0 +1,67 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript">var scriptRelativePath = "../";</script>
+ <script type="application/javascript" src="../pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ title: "setIdentityProvider leads to peerIdentity and assertions in SDP",
+ bug: "942367"
+ });
+
+function checkIdentity(peer, prefix, idp, name) {
+ prefix = prefix + ": ";
+ return peer._pc.peerIdentity.then(peerIdentity => {
+ ok(peerIdentity, prefix + "peerIdentity is set");
+ is(peerIdentity.idp, idp, prefix + "IdP is correct");
+ is(peerIdentity.name, name + "@" + idp, prefix + "identity is correct");
+ });
+}
+
+function theTest() {
+ var test = new PeerConnectionTest();
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ test.pcLocal.setIdentityProvider("test1.example.com",
+ { protocol: "idp.js",
+ usernameHint: "someone" });
+ test.pcRemote.setIdentityProvider("test2.example.com",
+ { protocol: "idp.js",
+ usernameHinte: "someone"});
+
+ test.chain.append([
+ function PC_LOCAL_PEER_IDENTITY_IS_SET_CORRECTLY(test) {
+ return checkIdentity(test.pcLocal, "local", "test2.example.com", "someone");
+ },
+ function PC_REMOTE_PEER_IDENTITY_IS_SET_CORRECTLY(test) {
+ return checkIdentity(test.pcRemote, "remote", "test1.example.com", "someone");
+ },
+
+ function OFFER_AND_ANSWER_INCLUDES_IDENTITY(test) {
+ ok(test.originalOffer.sdp.includes("a=identity"), "a=identity is in the offer SDP");
+ ok(test.originalAnswer.sdp.includes("a=identity"), "a=identity is in the answer SDP");
+ },
+
+ function PC_LOCAL_DESCRIPTIONS_CONTAIN_IDENTITY(test) {
+ ok(test.pcLocal.localDescription.sdp.includes("a=identity"),
+ "a=identity is in the local copy of the offer");
+ ok(test.pcLocal.remoteDescription.sdp.includes("a=identity"),
+ "a=identity is in the local copy of the answer");
+ },
+ function PC_REMOTE_DESCRIPTIONS_CONTAIN_IDENTITY(test) {
+ ok(test.pcRemote.localDescription.sdp.includes("a=identity"),
+ "a=identity is in the remote copy of the offer");
+ ok(test.pcRemote.remoteDescription.sdp.includes("a=identity"),
+ "a=identity is in the remote copy of the answer");
+ }
+ ]);
+ return test.run();
+}
+runNetworkTest(theTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/identity/test_setIdentityProviderWithErrors.html b/dom/media/webrtc/tests/mochitests/identity/test_setIdentityProviderWithErrors.html
new file mode 100644
index 0000000000..ce6832d1e6
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/identity/test_setIdentityProviderWithErrors.html
@@ -0,0 +1,57 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript">var scriptRelativePath = "../";</script>
+ <script type="application/javascript" src="../pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+'use strict';
+ createHTML({
+ title: "Identity Provider returning errors is handled correctly",
+ bug: "942367"
+ });
+
+runNetworkTest(function () {
+ var test = new PeerConnectionTest();
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ // No IdP for local.
+ // Remote generates a bad assertion, but that only fails to validate
+ test.pcRemote.setIdentityProvider('example.com',
+ { protocol: 'idp.js#bad-validate',
+ usernameHint: 'nobody' });
+
+ // Save the peerIdentity promises now, since when they reject they are
+ // replaced and we expect them to be rejected this time
+ var peerIdentityLocal = test.pcLocal._pc.peerIdentity;
+ var peerIdentityRemote = test.pcRemote._pc.peerIdentity;
+
+ test.chain.append([
+ function ONLY_REMOTE_SDP_INCLUDES_IDENTITY_ASSERTION(t) {
+ ok(!t.originalOffer.sdp.includes('a=identity'),
+ 'a=identity not contained in the offer SDP');
+ ok(t.originalAnswer.sdp.includes('a=identity'),
+ 'a=identity is contained in the answer SDP');
+ },
+ function PEER_IDENTITY_IS_EMPTY(t) {
+ // we are only waiting for the local side to complete
+ // an error on the remote side is immediately fatal though
+ return Promise.race([
+ peerIdentityLocal.then(
+ () => ok(false, t.pcLocal + ' incorrectly received valid peer identity'),
+ e => ok(e, t.pcLocal + ' correctly failed to validate peer identity')),
+ peerIdentityRemote.then(
+ () => ok(false, t.pcRemote + ' incorrecly received a valid peer identity'),
+ e => ok(false, t.pcRemote + ' incorrectly rejected peer identity'))
+ ]);
+ }
+ ]);
+
+ return test.run();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/mediaStreamPlayback.js b/dom/media/webrtc/tests/mochitests/mediaStreamPlayback.js
new file mode 100644
index 0000000000..44c1c78ea0
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/mediaStreamPlayback.js
@@ -0,0 +1,241 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const ENDED_TIMEOUT_LENGTH = 30000;
+
+/* The time we wait depends primarily on the canplaythrough event firing
+ * Note: this needs to be at least 30s because the
+ * B2G emulator in VMs is really slow. */
+const VERIFYPLAYING_TIMEOUT_LENGTH = 60000;
+
+/**
+ * This class manages playback of a HTMLMediaElement with a MediaStream.
+ * When constructed by a caller, an object instance is created with
+ * a media element and a media stream object.
+ *
+ * @param {HTMLMediaElement} mediaElement the media element for playback
+ * @param {MediaStream} mediaStream the media stream used in
+ * the mediaElement for playback
+ */
+function MediaStreamPlayback(mediaElement, mediaStream) {
+ this.mediaElement = mediaElement;
+ this.mediaStream = mediaStream;
+}
+
+MediaStreamPlayback.prototype = {
+ /**
+ * Starts media element with a media stream, runs it until a canplaythrough
+ * and timeupdate event fires, and calls stop() on all its tracks.
+ *
+ * @param {Boolean} isResume specifies if this media element is being resumed
+ * from a previous run
+ */
+ playMedia(isResume) {
+ this.startMedia(isResume);
+ return this.verifyPlaying()
+ .then(() => this.stopTracksForStreamInMediaPlayback())
+ .then(() => this.detachFromMediaElement());
+ },
+
+ /**
+ * Stops the local media stream's tracks while it's currently in playback in
+ * a media element.
+ *
+ * Precondition: The media stream and element should both be actively
+ * being played. All the stream's tracks must be local.
+ */
+ stopTracksForStreamInMediaPlayback() {
+ var elem = this.mediaElement;
+ return Promise.all([
+ haveEvent(
+ elem,
+ "ended",
+ wait(ENDED_TIMEOUT_LENGTH, new Error("Timeout"))
+ ),
+ ...this.mediaStream
+ .getTracks()
+ .map(t => (t.stop(), haveNoEvent(t, "ended"))),
+ ]);
+ },
+
+ /**
+ * Starts media with a media stream, runs it until a canplaythrough and
+ * timeupdate event fires, and detaches from the element without stopping media.
+ *
+ * @param {Boolean} isResume specifies if this media element is being resumed
+ * from a previous run
+ */
+ playMediaWithoutStoppingTracks(isResume) {
+ this.startMedia(isResume);
+ return this.verifyPlaying().then(() => this.detachFromMediaElement());
+ },
+
+ /**
+ * Starts the media with the associated stream.
+ *
+ * @param {Boolean} isResume specifies if the media element playback
+ * is being resumed from a previous run
+ */
+ startMedia(isResume) {
+ // If we're playing media element for the first time, check that time is zero.
+ if (!isResume) {
+ is(
+ this.mediaElement.currentTime,
+ 0,
+ "Before starting the media element, currentTime = 0"
+ );
+ }
+ this.canPlayThroughFired = listenUntil(
+ this.mediaElement,
+ "canplaythrough",
+ () => true
+ );
+
+ // Hooks up the media stream to the media element and starts playing it
+ this.mediaElement.srcObject = this.mediaStream;
+ this.mediaElement.play();
+ },
+
+ /**
+ * Verifies that media is playing.
+ */
+ verifyPlaying() {
+ var lastElementTime = this.mediaElement.currentTime;
+
+ var mediaTimeProgressed = listenUntil(
+ this.mediaElement,
+ "timeupdate",
+ () => this.mediaElement.currentTime > lastElementTime
+ );
+
+ return timeout(
+ Promise.all([this.canPlayThroughFired, mediaTimeProgressed]),
+ VERIFYPLAYING_TIMEOUT_LENGTH,
+ "verifyPlaying timed out"
+ ).then(() => {
+ is(this.mediaElement.paused, false, "Media element should be playing");
+ is(
+ this.mediaElement.duration,
+ Number.POSITIVE_INFINITY,
+ "Duration should be infinity"
+ );
+
+ // When the media element is playing with a real-time stream, we
+ // constantly switch between having data to play vs. queuing up data,
+ // so we can only check that the ready state is one of those two values
+ ok(
+ this.mediaElement.readyState === HTMLMediaElement.HAVE_ENOUGH_DATA ||
+ this.mediaElement.readyState === HTMLMediaElement.HAVE_CURRENT_DATA,
+ "Ready state shall be HAVE_ENOUGH_DATA or HAVE_CURRENT_DATA"
+ );
+
+ is(this.mediaElement.seekable.length, 0, "Seekable length shall be zero");
+ is(this.mediaElement.buffered.length, 0, "Buffered length shall be zero");
+
+ is(
+ this.mediaElement.seeking,
+ false,
+ "MediaElement is not seekable with MediaStream"
+ );
+ ok(
+ isNaN(this.mediaElement.startOffsetTime),
+ "Start offset time shall not be a number"
+ );
+ is(
+ this.mediaElement.defaultPlaybackRate,
+ 1,
+ "DefaultPlaybackRate should be 1"
+ );
+ is(this.mediaElement.playbackRate, 1, "PlaybackRate should be 1");
+ is(this.mediaElement.preload, "none", 'Preload should be "none"');
+ is(this.mediaElement.src, "", "No src should be defined");
+ is(
+ this.mediaElement.currentSrc,
+ "",
+ "Current src should still be an empty string"
+ );
+ });
+ },
+
+ /**
+ * Detaches from the element without stopping the media.
+ *
+ * Precondition: The media stream and element should both be actively
+ * being played.
+ */
+ detachFromMediaElement() {
+ this.mediaElement.pause();
+ this.mediaElement.srcObject = null;
+ },
+};
+
+// haxx to prevent SimpleTest from failing at window.onload
+function addLoadEvent() {}
+
+/* import-globals-from /testing/mochitest/tests/SimpleTest/SimpleTest.js */
+/* import-globals-from head.js */
+const scriptsReady = Promise.all(
+ ["/tests/SimpleTest/SimpleTest.js", "head.js"].map(script => {
+ const el = document.createElement("script");
+ el.src = script;
+ document.head.appendChild(el);
+ return new Promise(r => (el.onload = r));
+ })
+);
+
+function createHTML(options) {
+ return scriptsReady.then(() => realCreateHTML(options));
+}
+
+async function runTest(testFunction) {
+ await Promise.all([
+ scriptsReady,
+ SpecialPowers.pushPrefEnv({
+ set: [["media.navigator.permission.fake", true]],
+ }),
+ ]);
+ await runTestWhenReady(async (...args) => {
+ await testFunction(...args);
+ await noGum();
+ });
+}
+
+// noGum - Helper to detect whether active guM tracks still exist.
+//
+// Note it relies on the permissions system to detect active tracks, so it won't
+// catch getUserMedia use while media.navigator.permission.disabled is true
+// (which is common in automation), UNLESS we set
+// media.navigator.permission.fake to true also, like runTest() does above.
+async function noGum() {
+ if (!navigator.mediaDevices) {
+ // No mediaDevices, then gUM cannot have been called either.
+ return;
+ }
+ const mediaManagerService = Cc[
+ "@mozilla.org/mediaManagerService;1"
+ ].getService(Ci.nsIMediaManagerService);
+
+ const hasCamera = {};
+ const hasMicrophone = {};
+ mediaManagerService.mediaCaptureWindowState(
+ window,
+ hasCamera,
+ hasMicrophone,
+ {},
+ {},
+ {},
+ {},
+ false
+ );
+ is(
+ hasCamera.value,
+ mediaManagerService.STATE_NOCAPTURE,
+ "Test must leave no active camera gUM tracks behind."
+ );
+ is(
+ hasMicrophone.value,
+ mediaManagerService.STATE_NOCAPTURE,
+ "Test must leave no active microphone gUM tracks behind."
+ );
+}
diff --git a/dom/media/webrtc/tests/mochitests/mochitest.ini b/dom/media/webrtc/tests/mochitests/mochitest.ini
new file mode 100644
index 0000000000..d1a9800984
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/mochitest.ini
@@ -0,0 +1,65 @@
+[DEFAULT]
+tags = mtg webrtc
+subsuite = media
+scheme = https
+support-files =
+ head.js
+ dataChannel.js
+ mediaStreamPlayback.js
+ network.js
+ nonTrickleIce.js
+ pc.js
+ stats.js
+ templates.js
+ test_enumerateDevices_iframe.html
+ test_enumerateDevices_iframe_pre_gum.html
+ test_getUserMedia_permission_iframe.html
+ NetworkPreparationChromeScript.js
+ blacksilence.js
+ turnConfig.js
+ sdpUtils.js
+ addTurnsSelfsignedCert.js
+ parser_rtp.js
+ peerconnection_audio_forced_sample_rate.js
+ iceTestUtils.js
+ simulcast.js
+ helpers_from_wpt/sdp.js
+ !/dom/canvas/test/captureStream_common.js
+ !/dom/canvas/test/webgl-mochitest/webgl-util.js
+ !/dom/media/test/manifest.js
+ !/dom/media/test/seek.webm
+ !/dom/media/test/gizmo.mp4
+ !/docshell/test/navigation/blank.html
+prefs =
+ focusmanager.testmode=true # emulate focus
+ privacy.partition.network_state=false
+ network.proxy.allow_hijacking_localhost=true
+ media.devices.enumerate.legacy.enabled=false
+
+[test_1488832.html]
+skip-if =
+ os == 'linux' # Bug 1714410
+[test_1717318.html]
+[test_a_noOp.html]
+scheme=http
+[test_enumerateDevices.html]
+[test_enumerateDevices_getUserMediaFake.html]
+[test_enumerateDevices_legacy.html]
+[test_enumerateDevices_navigation.html]
+skip-if = true # Disabled because it is a racy test and causes timeouts, see bug 1650932
+[test_fingerprinting_resistance.html]
+skip-if =
+ os == "linux" && asan # Bug 1646309 - low frequency intermittent
+[test_forceSampleRate.html]
+scheme=http
+[test_groupId.html]
+[test_multi_mics.html]
+skip-if = os == 'android'
+[test_ondevicechange.html]
+run-sequentially = sets prefs that may disrupt other tests
+[test_setSinkId.html]
+skip-if =
+ os != 'linux' # the only platform with real devices
+[test_setSinkId_default_addTrack.html]
+[test_setSinkId_preMutedElement.html]
+[test_unfocused_pref.html]
diff --git a/dom/media/webrtc/tests/mochitests/mochitest_datachannel.ini b/dom/media/webrtc/tests/mochitests/mochitest_datachannel.ini
new file mode 100644
index 0000000000..881421af0e
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/mochitest_datachannel.ini
@@ -0,0 +1,52 @@
+[DEFAULT]
+tags = mtg webrtc
+subsuite = media
+scheme = https
+support-files =
+ head.js
+ dataChannel.js
+ mediaStreamPlayback.js
+ network.js
+ nonTrickleIce.js
+ pc.js
+ stats.js
+ templates.js
+ test_enumerateDevices_iframe.html
+ test_getUserMedia_permission_iframe.html
+ NetworkPreparationChromeScript.js
+ blacksilence.js
+ turnConfig.js
+ sdpUtils.js
+ addTurnsSelfsignedCert.js
+ parser_rtp.js
+ peerconnection_audio_forced_sample_rate.js
+ iceTestUtils.js
+ simulcast.js
+ helpers_from_wpt/sdp.js
+ !/dom/canvas/test/captureStream_common.js
+ !/dom/canvas/test/webgl-mochitest/webgl-util.js
+ !/dom/media/test/manifest.js
+ !/dom/media/test/seek.webm
+ !/dom/media/test/gizmo.mp4
+ !/docshell/test/navigation/blank.html
+prefs =
+ focusmanager.testmode=true # emulate focus
+ privacy.partition.network_state=false
+ network.proxy.allow_hijacking_localhost=true
+
+[test_dataChannel_basicAudio.html]
+[test_dataChannel_basicAudioVideo.html]
+[test_dataChannel_basicAudioVideoCombined.html]
+[test_dataChannel_basicAudioVideoNoBundle.html]
+[test_dataChannel_basicDataOnly.html]
+[test_dataChannel_basicVideo.html]
+[test_dataChannel_bug1013809.html]
+[test_dataChannel_dataOnlyBufferedAmountLow.html]
+scheme=http
+[test_dataChannel_dtlsVersions.html]
+[test_dataChannel_hostnameObfuscation.html]
+scheme=http
+[test_dataChannel_noOffer.html]
+scheme=http
+[test_dataChannel_stats.html]
+scheme=http
diff --git a/dom/media/webrtc/tests/mochitests/mochitest_getusermedia.ini b/dom/media/webrtc/tests/mochitests/mochitest_getusermedia.ini
new file mode 100644
index 0000000000..02c8272b5b
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/mochitest_getusermedia.ini
@@ -0,0 +1,105 @@
+[DEFAULT]
+tags = mtg webrtc
+subsuite = media
+scheme = https
+support-files =
+ head.js
+ dataChannel.js
+ mediaStreamPlayback.js
+ network.js
+ nonTrickleIce.js
+ pc.js
+ stats.js
+ templates.js
+ test_enumerateDevices_iframe.html
+ test_getUserMedia_permission_iframe.html
+ NetworkPreparationChromeScript.js
+ blacksilence.js
+ turnConfig.js
+ sdpUtils.js
+ addTurnsSelfsignedCert.js
+ parser_rtp.js
+ peerconnection_audio_forced_sample_rate.js
+ iceTestUtils.js
+ simulcast.js
+ helpers_from_wpt/sdp.js
+ !/dom/canvas/test/captureStream_common.js
+ !/dom/canvas/test/webgl-mochitest/webgl-util.js
+ !/dom/media/test/manifest.js
+ !/dom/media/test/seek.webm
+ !/dom/media/test/gizmo.mp4
+ !/docshell/test/navigation/blank.html
+prefs =
+ focusmanager.testmode=true # emulate focus
+ privacy.partition.network_state=false
+ network.proxy.allow_hijacking_localhost=true
+ media.devices.enumerate.legacy.enabled=false
+
+[test_defaultAudioConstraints.html]
+skip-if = os == 'mac'
+ os == 'win'
+ toolkit == 'android' # Bug 1404995, no loopback devices on some platforms
+[test_getUserMedia_GC_MediaStream.html]
+[test_getUserMedia_active_autoplay.html]
+[test_getUserMedia_addTrackRemoveTrack.html]
+[test_getUserMedia_addtrack_removetrack_events.html]
+[test_getUserMedia_audioCapture.html]
+skip-if = toolkit == 'android'
+ (os == "win" && processor == "aarch64") # android(Bug 1189784, timeouts on 4.3 emulator), android(Bug 1264333), aarch64 due to 1538359
+[test_getUserMedia_audioConstraints.html]
+skip-if = os == 'mac'
+ os == 'win'
+ toolkit == 'android' # Bug 1404995, no loopback devices on some platforms
+[test_getUserMedia_audioConstraints_concurrentIframes.html]
+skip-if = os == 'mac'
+ os == 'win'
+ toolkit == 'android'
+ (os == 'linux' && debug) # Bug 1404995, no loopback devices on some platforms # Bug 1481101
+ os == "linux" && !debug && !fission # bug 1645930, lower frequency intermittent
+[test_getUserMedia_audioConstraints_concurrentStreams.html]
+skip-if = os == 'mac'
+ os == 'win'
+ toolkit == 'android' # Bug 1404995, no loopback devices on some platforms
+[test_getUserMedia_basicAudio.html]
+[test_getUserMedia_basicAudio_loopback.html]
+skip-if = os == 'mac'
+ os == 'win'
+ toolkit == 'android' # Bug 1404995, no loopback devices on some platforms
+[test_getUserMedia_basicScreenshare.html]
+skip-if =
+ toolkit == 'android' # no screenshare on android
+ apple_silicon # bug 1707742
+[test_getUserMedia_basicTabshare.html]
+skip-if =
+ toolkit == 'android' # no windowshare on android
+[test_getUserMedia_basicVideo.html]
+[test_getUserMedia_basicVideoAudio.html]
+[test_getUserMedia_basicVideo_playAfterLoadedmetadata.html]
+[test_getUserMedia_basicWindowshare.html]
+skip-if = toolkit == 'android' # no windowshare on android
+[test_getUserMedia_bug1223696.html]
+[test_getUserMedia_callbacks.html]
+[test_getUserMedia_constraints.html]
+[test_getUserMedia_cubebDisabled.html]
+[test_getUserMedia_cubebDisabledFakeStreams.html]
+[test_getUserMedia_getTrackById.html]
+[test_getUserMedia_gumWithinGum.html]
+[test_getUserMedia_loadedmetadata.html]
+[test_getUserMedia_mediaElementCapture_audio.html]
+[test_getUserMedia_mediaElementCapture_tracks.html]
+[test_getUserMedia_mediaElementCapture_video.html]
+[test_getUserMedia_mediaStreamClone.html]
+[test_getUserMedia_mediaStreamConstructors.html]
+[test_getUserMedia_mediaStreamTrackClone.html]
+[test_getUserMedia_nonDefaultRate.html]
+[test_getUserMedia_peerIdentity.html]
+[test_getUserMedia_permission.html]
+[test_getUserMedia_playAudioTwice.html]
+[test_getUserMedia_playVideoAudioTwice.html]
+[test_getUserMedia_playVideoTwice.html]
+[test_getUserMedia_scarySources.html]
+skip-if = toolkit == 'android' # no screenshare or windowshare on android
+[test_getUserMedia_spinEventLoop.html]
+[test_getUserMedia_trackCloneCleanup.html]
+[test_getUserMedia_trackEnded.html]
+
diff --git a/dom/media/webrtc/tests/mochitests/mochitest_peerconnection.ini b/dom/media/webrtc/tests/mochitests/mochitest_peerconnection.ini
new file mode 100644
index 0000000000..37fb551435
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/mochitest_peerconnection.ini
@@ -0,0 +1,311 @@
+[DEFAULT]
+tags = mtg webrtc
+subsuite = media
+scheme = https
+support-files =
+ head.js
+ dataChannel.js
+ mediaStreamPlayback.js
+ network.js
+ nonTrickleIce.js
+ pc.js
+ stats.js
+ templates.js
+ test_enumerateDevices_iframe.html
+ test_getUserMedia_permission_iframe.html
+ NetworkPreparationChromeScript.js
+ blacksilence.js
+ turnConfig.js
+ sdpUtils.js
+ addTurnsSelfsignedCert.js
+ parser_rtp.js
+ peerconnection_audio_forced_sample_rate.js
+ iceTestUtils.js
+ simulcast.js
+ helpers_from_wpt/sdp.js
+ !/dom/canvas/test/captureStream_common.js
+ !/dom/canvas/test/webgl-mochitest/webgl-util.js
+ !/dom/media/test/manifest.js
+ !/dom/media/test/seek.webm
+ !/dom/media/test/gizmo.mp4
+ !/docshell/test/navigation/blank.html
+prefs =
+ focusmanager.testmode=true # emulate focus
+ privacy.partition.network_state=false
+ network.proxy.allow_hijacking_localhost=true
+ media.devices.enumerate.legacy.enabled=false
+
+[test_peerConnection_addAudioTrackToExistingVideoStream.html]
+[test_peerConnection_addDataChannel.html]
+[test_peerConnection_addDataChannelNoBundle.html]
+[test_peerConnection_addSecondAudioStream.html]
+[test_peerConnection_addSecondAudioStreamNoBundle.html]
+[test_peerConnection_addSecondVideoStream.html]
+[test_peerConnection_addSecondVideoStreamNoBundle.html]
+[test_peerConnection_addtrack_removetrack_events.html]
+[test_peerConnection_answererAddSecondAudioStream.html]
+[test_peerConnection_audioChannels.html]
+[test_peerConnection_audioCodecs.html]
+[test_peerConnection_audioContributingSources.html]
+[test_peerConnection_audioRenegotiationInactiveAnswer.html]
+[test_peerConnection_audioSynchronizationSources.html]
+[test_peerConnection_audioSynchronizationSourcesUnidirectional.html]
+[test_peerConnection_basicAudio.html]
+[test_peerConnection_basicAudioDynamicPtMissingRtpmap.html]
+[test_peerConnection_basicAudioNATRelay.html]
+skip-if =
+ toolkit == 'android' # websockets don't work on android (bug 1266217)
+scheme=http
+[test_peerConnection_basicAudioNATRelayTCP.html]
+skip-if =
+ toolkit == 'android' # websockets don't work on android (bug 1266217)
+ (os == 'win' && os_version == '6.1') # WinError 10048
+scheme=http
+[test_peerConnection_basicAudioNATRelayTCPWithStun300.html]
+skip-if =
+ toolkit == 'android' # websockets don't work on android (bug 1266217)
+ (os == 'win' && os_version == '6.1') # WinError 10048
+scheme=http
+[test_peerConnection_basicAudioNATRelayTLS.html]
+skip-if =
+ toolkit == 'android' # websockets don't work on android (bug 1266217)
+ (os == 'win' && os_version == '6.1') # WinError 10048
+scheme=http
+[test_peerConnection_basicAudioNATRelayWithStun300.html]
+skip-if =
+ toolkit == 'android' # websockets don't work on android (bug 1266217)
+ (os == 'win' && os_version == '6.1') # WinError 10048
+scheme=http
+[test_peerConnection_basicAudioNATSrflx.html]
+skip-if =
+ toolkit == 'android' # websockets don't work on android (bug 1266217)
+ (os == 'win' && os_version == '6.1') # WinError 10048
+scheme=http
+[test_peerConnection_basicAudioNoisyUDPBlock.html]
+skip-if =
+ toolkit == 'android' # websockets don't work on android (bug 1266217)
+ (os == 'win' && os_version == '6.1') # WinError 10048
+scheme=http
+[test_peerConnection_basicAudioPcmaPcmuOnly.html]
+[test_peerConnection_basicAudioRelayPolicy.html]
+skip-if =
+ toolkit == 'android' # websockets don't work on android (bug 1266217)
+ (os == 'win' && os_version == '6.1') # WinError 10048
+scheme=http
+[test_peerConnection_basicAudioRequireEOC.html]
+[test_peerConnection_basicAudioVerifyRtpHeaderExtensions.html]
+[test_peerConnection_basicAudioVideo.html]
+[test_peerConnection_basicAudioVideoCombined.html]
+[test_peerConnection_basicAudioVideoNoBundle.html]
+[test_peerConnection_basicAudioVideoNoBundleNoRtcpMux.html]
+[test_peerConnection_basicAudioVideoNoRtcpMux.html]
+[test_peerConnection_basicAudioVideoTransceivers.html]
+[test_peerConnection_basicAudioVideoVerifyExtmap.html]
+[test_peerConnection_basicAudioVideoVerifyExtmapSendonly.html]
+[test_peerConnection_basicAudioVideoVerifyTooLongMidFails.html]
+[test_peerConnection_basicAudio_forced_higher_rate.html]
+[test_peerConnection_basicAudio_forced_lower_rate.html]
+[test_peerConnection_basicH264Video.html]
+skip-if =
+ toolkit == 'android' && is_emulator # Bug 1355786, No h264 support on android emulator
+[test_peerConnection_basicScreenshare.html]
+skip-if = toolkit == 'android' # no screenshare on android
+[test_peerConnection_basicVideo.html]
+[test_peerConnection_basicVideoVerifyRtpHeaderExtensions.html]
+[test_peerConnection_basicWindowshare.html]
+skip-if = toolkit == 'android' # no screenshare on android
+[test_peerConnection_bug1013809.html]
+[test_peerConnection_bug1042791.html]
+skip-if = (toolkit == 'android' && is_emulator) # Bug 1355786, No h264 support on android emulator
+[test_peerConnection_bug1227781.html]
+scheme=http
+[test_peerConnection_bug1512281.html]
+fail-if = 1
+[test_peerConnection_bug1773067.html]
+[test_peerConnection_bug822674.html]
+scheme=http
+[test_peerConnection_bug825703.html]
+scheme=http
+[test_peerConnection_bug827843.html]
+[test_peerConnection_bug834153.html]
+scheme=http
+[test_peerConnection_callbacks.html]
+[test_peerConnection_captureStream_canvas_2d.html]
+scheme=http
+[test_peerConnection_captureStream_canvas_2d_noSSRC.html]
+scheme=http
+[test_peerConnection_captureStream_canvas_webgl.html]
+scheme=http
+[test_peerConnection_capturedVideo.html]
+tags=capturestream
+skip-if = toolkit == 'android' # android(Bug 1189784, timeouts on 4.3 emulator), Bug 1264340
+[test_peerConnection_certificates.html]
+[test_peerConnection_checkPacketDumpHook.html]
+[test_peerConnection_close.html]
+scheme=http
+[test_peerConnection_closeDuringIce.html]
+[test_peerConnection_codecNegotiationFailure.html]
+[test_peerConnection_constructedStream.html]
+[test_peerConnection_disabledVideoPreNegotiation.html]
+[test_peerConnection_encodingsNegotiation.html]
+[test_peerConnection_errorCallbacks.html]
+scheme=http
+[test_peerConnection_extmapRenegotiation.html]
+[test_peerConnection_forwarding_basicAudioVideoCombined.html]
+skip-if = toolkit == 'android' # Bug 1189784
+[test_peerConnection_gatherWithSetConfiguration.html]
+skip-if =
+ toolkit == 'android' # websockets don't work on android (bug 1266217)
+ (os == 'win' && os_version == '6.1') # WinError 10048
+scheme=http
+[test_peerConnection_gatherWithStun300.html]
+skip-if =
+ toolkit == 'android' # websockets don't work on android (bug 1266217)
+ (os == 'win' && os_version == '6.1') # WinError 10048
+scheme=http
+[test_peerConnection_gatherWithStun300IPv6.html]
+skip-if =
+ toolkit == 'android' # websockets don't work on android (bug 1266217)
+ os == 'mac' # no ipv6 support on OS X testers (bug 1710706)
+ os == 'win' # no ipv6 support on windows testers (bug 1710706)
+scheme=http
+[test_peerConnection_glean.html]
+[test_peerConnection_iceFailure.html]
+skip-if = true # (Bug 1180388 for win, mac and linux), android(Bug 1189784), Bug 1180388
+scheme=http
+[test_peerConnection_insertDTMF.html]
+[test_peerConnection_localReofferRollback.html]
+[test_peerConnection_localRollback.html]
+[test_peerConnection_maxFsConstraint.html]
+[test_peerConnection_multiple_captureStream_canvas_2d.html]
+scheme=http
+[test_peerConnection_noTrickleAnswer.html]
+[test_peerConnection_noTrickleOffer.html]
+[test_peerConnection_noTrickleOfferAnswer.html]
+[test_peerConnection_nonDefaultRate.html]
+[test_peerConnection_offerRequiresReceiveAudio.html]
+[test_peerConnection_offerRequiresReceiveVideo.html]
+[test_peerConnection_offerRequiresReceiveVideoAudio.html]
+[test_peerConnection_portRestrictions.html]
+[test_peerConnection_promiseSendOnly.html]
+[test_peerConnection_recordReceiveTrack.html]
+[test_peerConnection_relayOnly.html]
+disabled=bug 1612063 # test is racy
+[test_peerConnection_remoteReofferRollback.html]
+[test_peerConnection_remoteRollback.html]
+[test_peerConnection_removeAudioTrack.html]
+[test_peerConnection_removeThenAddAudioTrack.html]
+[test_peerConnection_removeThenAddAudioTrackNoBundle.html]
+[test_peerConnection_removeThenAddVideoTrack.html]
+[test_peerConnection_removeThenAddVideoTrackNoBundle.html]
+[test_peerConnection_removeVideoTrack.html]
+[test_peerConnection_renderAfterRenegotiation.html]
+scheme=http
+[test_peerConnection_replaceNullTrackThenRenegotiateAudio.html]
+[test_peerConnection_replaceNullTrackThenRenegotiateVideo.html]
+[test_peerConnection_replaceTrack.html]
+[test_peerConnection_replaceTrack_camera.html]
+skip-if = toolkit == 'android' # Bug 1614460
+[test_peerConnection_replaceTrack_disabled.html]
+skip-if =
+ toolkit == 'android' # Bug 1614460
+[test_peerConnection_replaceTrack_microphone.html]
+[test_peerConnection_replaceVideoThenRenegotiate.html]
+[test_peerConnection_restartIce.html]
+[test_peerConnection_restartIceBadAnswer.html]
+[test_peerConnection_restartIceLocalAndRemoteRollback.html]
+[test_peerConnection_restartIceLocalAndRemoteRollbackNoSubsequentRestart.html]
+[test_peerConnection_restartIceLocalRollback.html]
+[test_peerConnection_restartIceLocalRollbackNoSubsequentRestart.html]
+[test_peerConnection_restartIceNoBundle.html]
+[test_peerConnection_restartIceNoBundleNoRtcpMux.html]
+[test_peerConnection_restartIceNoRtcpMux.html]
+[test_peerConnection_restrictBandwidthTargetBitrate.html]
+[test_peerConnection_restrictBandwidthWithTias.html]
+[test_peerConnection_rtcp_rsize.html]
+[test_peerConnection_scaleResolution.html]
+[test_peerConnection_scaleResolution_oldSetParameters.html]
+[test_peerConnection_sender_and_receiver_stats.html]
+[test_peerConnection_setLocalAnswerInHaveLocalOffer.html]
+[test_peerConnection_setLocalAnswerInStable.html]
+[test_peerConnection_setLocalOfferInHaveRemoteOffer.html]
+[test_peerConnection_setParameters.html]
+[test_peerConnection_setParameters_maxFramerate.html]
+[test_peerConnection_setParameters_maxFramerate_oldSetParameters.html]
+[test_peerConnection_setParameters_oldSetParameters.html]
+[test_peerConnection_setParameters_scaleResolutionDownBy.html]
+skip-if = (os == 'win' && processor == 'aarch64') # aarch64 due to bug 1537567
+[test_peerConnection_setParameters_scaleResolutionDownBy_oldSetParameters.html]
+skip-if = (os == 'win' && processor == 'aarch64') # aarch64 due to bug 1537567
+[test_peerConnection_setRemoteAnswerInHaveRemoteOffer.html]
+[test_peerConnection_setRemoteAnswerInStable.html]
+[test_peerConnection_setRemoteOfferInHaveLocalOffer.html]
+[test_peerConnection_simulcastAnswer.html]
+skip-if = toolkit == 'android' # no simulcast support on android
+[test_peerConnection_simulcastAnswer_lowResFirst.html]
+skip-if = toolkit == 'android' # no simulcast support on android
+[test_peerConnection_simulcastAnswer_lowResFirst_oldSetParameters.html]
+skip-if = toolkit == 'android' # no simulcast support on android
+[test_peerConnection_simulcastAnswer_oldSetParameters.html]
+skip-if = toolkit == 'android' # no simulcast support on android
+[test_peerConnection_simulcastOddResolution.html]
+skip-if = toolkit == 'android' # no simulcast support on android
+[test_peerConnection_simulcastOddResolution_oldSetParameters.html]
+skip-if = toolkit == 'android' # no simulcast support on android
+[test_peerConnection_simulcastOffer.html]
+skip-if = toolkit == 'android' # no simulcast support on android
+[test_peerConnection_simulcastOffer_lowResFirst.html]
+skip-if = toolkit == 'android' # no simulcast support on android
+[test_peerConnection_simulcastOffer_lowResFirst_oldSetParameters.html]
+skip-if = toolkit == 'android' # no simulcast support on android
+[test_peerConnection_simulcastOffer_oldSetParameters.html]
+skip-if = toolkit == 'android' # no simulcast support on android
+[test_peerConnection_stats.html]
+[test_peerConnection_stats_jitter.html]
+skip-if = tsan # Bug 1672590, TSan is just too slow to pass this test
+[test_peerConnection_stats_oneway.html]
+[test_peerConnection_stats_relayProtocol.html]
+skip-if =
+ toolkit == 'android' # android(Bug 1189784, timeouts on 4.3 emulator, Bug 1373858, Bug 1521117)
+ socketprocess_e10s
+ (os == 'win' && os_version == '6.1') # WinError 10048
+scheme=http
+[test_peerConnection_stereoFmtpPref.html]
+[test_peerConnection_syncSetDescription.html]
+[test_peerConnection_telephoneEventFirst.html]
+[test_peerConnection_threeUnbundledConnections.html]
+[test_peerConnection_throwInCallbacks.html]
+[test_peerConnection_toJSON.html]
+scheme=http
+[test_peerConnection_trackDisabling.html]
+skip-if = toolkit == 'android' # Bug 1614460
+[test_peerConnection_trackDisabling_clones.html]
+[test_peerConnection_trackless_sender_stats.html]
+[test_peerConnection_twoAudioStreams.html]
+[test_peerConnection_twoAudioTracksInOneStream.html]
+[test_peerConnection_twoAudioVideoStreams.html]
+[test_peerConnection_twoAudioVideoStreamsCombined.html]
+skip-if = (toolkit == 'android') || (os == 'linux' && asan) # android(Bug 1189784), Bug 1480942 for Linux asan
+[test_peerConnection_twoAudioVideoStreamsCombinedNoBundle.html]
+skip-if =
+ (toolkit == 'android') # Bug 1189784
+ (os == 'linux' && asan) # Bug 1480942
+ (os == 'win' && processor == 'aarch64') # Bug 1777081
+[test_peerConnection_twoVideoStreams.html]
+[test_peerConnection_twoVideoTracksInOneStream.html]
+[test_peerConnection_verifyAudioAfterRenegotiation.html]
+skip-if =
+ os == "android" && processor == "x86_64" && !debug # Bug 1783287
+[test_peerConnection_verifyDescriptions.html]
+[test_peerConnection_verifyVideoAfterRenegotiation.html]
+[test_peerConnection_videoCodecs.html]
+skip-if =
+ toolkit == 'android' # android(Bug 1614460)
+ win10_2004 && !debug # Bug 1777082
+[test_peerConnection_videoRenegotiationInactiveAnswer.html]
+[test_peerConnection_webAudio.html]
+tags = webaudio webrtc
+scheme=http
+[test_selftest.html]
+# Bug 1227781: Crash with bogus TURN server.
+scheme=http
diff --git a/dom/media/webrtc/tests/mochitests/network.js b/dom/media/webrtc/tests/mochitests/network.js
new file mode 100644
index 0000000000..223721b111
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/network.js
@@ -0,0 +1,16 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * A stub function for preparing the network if needed
+ *
+ */
+async function startNetworkAndTest() {}
+
+/**
+ * A stub function to shutdown the network if needed
+ */
+async function networkTestFinished() {}
diff --git a/dom/media/webrtc/tests/mochitests/nonTrickleIce.js b/dom/media/webrtc/tests/mochitests/nonTrickleIce.js
new file mode 100644
index 0000000000..9361944791
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/nonTrickleIce.js
@@ -0,0 +1,97 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function removeTrickleOption(desc) {
+ var sdp = desc.sdp.replace(/\r\na=ice-options:trickle\r\n/, "\r\n");
+ return new RTCSessionDescription({ type: desc.type, sdp });
+}
+
+function makeOffererNonTrickle(chain) {
+ chain.replace("PC_LOCAL_SETUP_ICE_HANDLER", [
+ function PC_LOCAL_SETUP_NOTRICKLE_ICE_HANDLER(test) {
+ // We need to install this callback before calling setLocalDescription
+ // otherwise we might miss callbacks
+ test.pcLocal.setupIceCandidateHandler(test, () => {});
+ // We ignore ICE candidates because we want the full offer
+ },
+ ]);
+ chain.replace("PC_REMOTE_GET_OFFER", [
+ function PC_REMOTE_GET_FULL_OFFER(test) {
+ return test.pcLocal.endOfTrickleIce.then(() => {
+ test._local_offer = removeTrickleOption(test.pcLocal.localDescription);
+ test._offer_constraints = test.pcLocal.constraints;
+ test._offer_options = test.pcLocal.offerOptions;
+ });
+ },
+ ]);
+ chain.insertAfter("PC_REMOTE_SANE_REMOTE_SDP", [
+ function PC_REMOTE_REQUIRE_REMOTE_SDP_CANDIDATES(test) {
+ info(
+ "test.pcLocal.localDescription.sdp: " +
+ JSON.stringify(test.pcLocal.localDescription.sdp)
+ );
+ info("test._local_offer.sdp" + JSON.stringify(test._local_offer.sdp));
+ is(
+ test.pcRemote._pc.canTrickleIceCandidates,
+ false,
+ "Remote thinks that trickle isn't supported"
+ );
+ ok(!test.localRequiresTrickleIce, "Local does NOT require trickle");
+ ok(
+ test._local_offer.sdp.includes("a=candidate"),
+ "offer has ICE candidates"
+ );
+ ok(
+ test._local_offer.sdp.includes("a=end-of-candidates"),
+ "offer has end-of-candidates"
+ );
+ },
+ ]);
+ chain.remove("PC_REMOTE_CHECK_CAN_TRICKLE_SYNC");
+}
+
+function makeAnswererNonTrickle(chain) {
+ chain.replace("PC_REMOTE_SETUP_ICE_HANDLER", [
+ function PC_REMOTE_SETUP_NOTRICKLE_ICE_HANDLER(test) {
+ // We need to install this callback before calling setLocalDescription
+ // otherwise we might miss callbacks
+ test.pcRemote.setupIceCandidateHandler(test, () => {});
+ // We ignore ICE candidates because we want the full offer
+ },
+ ]);
+ chain.replace("PC_LOCAL_GET_ANSWER", [
+ function PC_LOCAL_GET_FULL_ANSWER(test) {
+ return test.pcRemote.endOfTrickleIce.then(() => {
+ test._remote_answer = removeTrickleOption(
+ test.pcRemote.localDescription
+ );
+ test._answer_constraints = test.pcRemote.constraints;
+ });
+ },
+ ]);
+ chain.insertAfter("PC_LOCAL_SANE_REMOTE_SDP", [
+ function PC_LOCAL_REQUIRE_REMOTE_SDP_CANDIDATES(test) {
+ info(
+ "test.pcRemote.localDescription.sdp: " +
+ JSON.stringify(test.pcRemote.localDescription.sdp)
+ );
+ info("test._remote_answer.sdp" + JSON.stringify(test._remote_answer.sdp));
+ is(
+ test.pcLocal._pc.canTrickleIceCandidates,
+ false,
+ "Local thinks that trickle isn't supported"
+ );
+ ok(!test.remoteRequiresTrickleIce, "Remote does NOT require trickle");
+ ok(
+ test._remote_answer.sdp.includes("a=candidate"),
+ "answer has ICE candidates"
+ );
+ ok(
+ test._remote_answer.sdp.includes("a=end-of-candidates"),
+ "answer has end-of-candidates"
+ );
+ },
+ ]);
+ chain.remove("PC_LOCAL_CHECK_CAN_TRICKLE_SYNC");
+}
diff --git a/dom/media/webrtc/tests/mochitests/parser_rtp.js b/dom/media/webrtc/tests/mochitests/parser_rtp.js
new file mode 100644
index 0000000000..2275c1f787
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/parser_rtp.js
@@ -0,0 +1,131 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/*
+ * Parses an RTP packet
+ * @param buffer an ArrayBuffer that contains the packet
+ * @return { type: "rtp", header: {...}, payload: a DataView }
+ */
+var ParseRtpPacket = buffer => {
+ // DataView.getFooInt returns big endian numbers by default
+ let view = new DataView(buffer);
+
+ // Standard Header Fields
+ // https://tools.ietf.org/html/rfc3550#section-5.1
+ // 0 1 2 3
+ // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+ // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ // |V=2|P|X| CC |M| PT | sequence number |
+ // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ // | timestamp |
+ // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ // | synchronization source (SSRC) identifier |
+ // +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
+ // | contributing source (CSRC) identifiers |
+ // | .... |
+ // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+
+ let header = {};
+ let offset = 0;
+ // Note that incrementing the offset happens as close to reading the data as
+ // possible. This simplifies ensuring that the number of read bytes and the
+ // offset increment match. Data may be manipulated between when the offset is
+ // incremented and before the next read.
+ let byte = view.getUint8(offset);
+ offset++;
+ // Version 2 Bit
+ header.version = (0xc0 & byte) >> 6;
+ // Padding 1 Bit
+ header.padding = (0x30 & byte) >> 5;
+ // Extension 1 Bit
+ header.extensionsPresent = (0x10 & byte) >> 4 == 1;
+ // CSRC count 4 Bit
+ header.csrcCount = 0xf & byte;
+
+ byte = view.getUint8(offset);
+ offset++;
+ // Marker 1 Bit
+ header.marker = (0x80 & byte) >> 7;
+ // Payload Type 7 Bit
+ header.payloadType = 0x7f & byte;
+ // Sequence Number 16 Bit
+ header.sequenceNumber = view.getUint16(offset);
+ offset += 2;
+ // Timestamp 32 Bit
+ header.timestamp = view.getUint32(offset);
+ offset += 4;
+ // SSRC 32 Bit
+ header.ssrc = view.getUint32(offset);
+ offset += 4;
+
+ // CSRC 32 Bit
+ header.csrcs = [];
+ for (let c = 0; c < header.csrcCount; c++) {
+ header.csrcs.push(view.getUint32(offset));
+ offset += 4;
+ }
+
+ // Extensions
+ header.extensions = [];
+ header.extensionPaddingBytes = 0;
+ header.extensionsTotalLength = 0;
+ if (header.extensionsPresent) {
+ // https://tools.ietf.org/html/rfc3550#section-5.3.1
+ // 0 1 2 3
+ // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+ // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ // | defined by profile | length |
+ // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ // | header extension |
+ // | .... |
+ let addExtension = (id, len) =>
+ header.extensions.push({
+ id,
+ data: new DataView(buffer, offset, len),
+ });
+ let extensionId = view.getUint16(offset);
+ offset += 2;
+ // len is in 32 bit units, not bytes
+ header.extensionsTotalLength = view.getUint16(offset) * 4;
+ offset += 2;
+ // Check for https://tools.ietf.org/html/rfc5285
+ if (extensionId != 0xbede) {
+ // No rfc5285
+ addExtension(extensionId, header.extensionsTotalLength);
+ offset += header.extensionsTotalLength;
+ } else {
+ let expectedEnd = offset + header.extensionsTotalLength;
+ while (offset < expectedEnd) {
+ // We only support "one-byte" extension headers ATM
+ // https://tools.ietf.org/html/rfc5285#section-4.2
+ // 0
+ // 0 1 2 3 4 5 6 7
+ // +-+-+-+-+-+-+-+-+
+ // | ID | len |
+ // +-+-+-+-+-+-+-+-+
+ byte = view.getUint8(offset);
+ offset++;
+ // Check for padding which can occur between extensions or at the end
+ if (byte == 0) {
+ header.extensionPaddingBytes++;
+ continue;
+ }
+ let id = (byte & 0xf0) >> 4;
+ // Check for the FORBIDDEN id (15), dun dun dun
+ if (id == 15) {
+ // Ignore bytes until until the end of extensions
+ offset = expectedEnd;
+ break;
+ }
+ // the length of the extention is len + 1
+ let len = (byte & 0x0f) + 1;
+ addExtension(id, len);
+ offset += len;
+ }
+ }
+ }
+ return { type: "rtp", header, payload: new DataView(buffer, offset) };
+};
diff --git a/dom/media/webrtc/tests/mochitests/pc.js b/dom/media/webrtc/tests/mochitests/pc.js
new file mode 100644
index 0000000000..36a923fbed
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/pc.js
@@ -0,0 +1,2495 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const LOOPBACK_ADDR = "127.0.0.";
+
+const iceStateTransitions = {
+ new: ["checking", "closed"], //Note: 'failed' might need to added here
+ // even though it is not in the standard
+ checking: ["new", "connected", "failed", "closed"], //Note: do we need to
+ // allow 'completed' in
+ // here as well?
+ connected: ["new", "completed", "disconnected", "closed"],
+ completed: ["new", "disconnected", "closed"],
+ disconnected: ["new", "connected", "completed", "failed", "closed"],
+ failed: ["new", "disconnected", "closed"],
+ closed: [],
+};
+
+const signalingStateTransitions = {
+ stable: ["have-local-offer", "have-remote-offer", "closed"],
+ "have-local-offer": [
+ "have-remote-pranswer",
+ "stable",
+ "closed",
+ "have-local-offer",
+ ],
+ "have-remote-pranswer": ["stable", "closed", "have-remote-pranswer"],
+ "have-remote-offer": [
+ "have-local-pranswer",
+ "stable",
+ "closed",
+ "have-remote-offer",
+ ],
+ "have-local-pranswer": ["stable", "closed", "have-local-pranswer"],
+ closed: [],
+};
+
+var makeDefaultCommands = () => {
+ return [].concat(
+ commandsPeerConnectionInitial,
+ commandsGetUserMedia,
+ commandsPeerConnectionOfferAnswer
+ );
+};
+
+/**
+ * This class handles tests for peer connections.
+ *
+ * @constructor
+ * @param {object} [options={}]
+ * Optional options for the peer connection test
+ * @param {object} [options.commands=commandsPeerConnection]
+ * Commands to run for the test
+ * @param {bool} [options.is_local=true]
+ * true if this test should run the tests for the "local" side.
+ * @param {bool} [options.is_remote=true]
+ * true if this test should run the tests for the "remote" side.
+ * @param {object} [options.config_local=undefined]
+ * Configuration for the local peer connection instance
+ * @param {object} [options.config_remote=undefined]
+ * Configuration for the remote peer connection instance. If not defined
+ * the configuration from the local instance will be used
+ */
+function PeerConnectionTest(options) {
+ // If no options are specified make it an empty object
+ options = options || {};
+ options.commands = options.commands || makeDefaultCommands();
+ options.is_local = "is_local" in options ? options.is_local : true;
+ options.is_remote = "is_remote" in options ? options.is_remote : true;
+
+ options.h264 = "h264" in options ? options.h264 : false;
+ options.bundle = "bundle" in options ? options.bundle : true;
+ options.rtcpmux = "rtcpmux" in options ? options.rtcpmux : true;
+ options.opus = "opus" in options ? options.opus : true;
+ options.ssrc = "ssrc" in options ? options.ssrc : true;
+
+ options.config_local = options.config_local || {};
+ options.config_remote = options.config_remote || {};
+
+ if (!options.bundle) {
+ // Make sure neither end tries to use bundle-only!
+ options.config_local.bundlePolicy = "max-compat";
+ options.config_remote.bundlePolicy = "max-compat";
+ }
+
+ if (iceServersArray.length) {
+ if (!options.turn_disabled_local && !options.config_local.iceServers) {
+ options.config_local.iceServers = iceServersArray;
+ }
+ if (!options.turn_disabled_remote && !options.config_remote.iceServers) {
+ options.config_remote.iceServers = iceServersArray;
+ }
+ } else if (typeof turnServers !== "undefined") {
+ if (!options.turn_disabled_local && turnServers.local) {
+ if (!options.config_local.hasOwnProperty("iceServers")) {
+ options.config_local.iceServers = turnServers.local.iceServers;
+ }
+ }
+ if (!options.turn_disabled_remote && turnServers.remote) {
+ if (!options.config_remote.hasOwnProperty("iceServers")) {
+ options.config_remote.iceServers = turnServers.remote.iceServers;
+ }
+ }
+ }
+
+ if (options.is_local) {
+ this.pcLocal = new PeerConnectionWrapper("pcLocal", options.config_local);
+ } else {
+ this.pcLocal = null;
+ }
+
+ if (options.is_remote) {
+ this.pcRemote = new PeerConnectionWrapper(
+ "pcRemote",
+ options.config_remote || options.config_local
+ );
+ } else {
+ this.pcRemote = null;
+ }
+
+ // Create command chain instance and assign default commands
+ this.chain = new CommandChain(this, options.commands);
+
+ this.testOptions = options;
+}
+
+/** TODO: consider removing this dependency on timeouts */
+function timerGuard(p, time, message) {
+ return Promise.race([
+ p,
+ wait(time).then(() => {
+ throw new Error("timeout after " + time / 1000 + "s: " + message);
+ }),
+ ]);
+}
+
+/**
+ * Closes the peer connection if it is active
+ */
+PeerConnectionTest.prototype.closePC = function () {
+ info("Closing peer connections");
+
+ var closeIt = pc => {
+ if (!pc || pc.signalingState === "closed") {
+ return Promise.resolve();
+ }
+
+ var promise = Promise.all([
+ Promise.all(
+ pc._pc
+ .getReceivers()
+ .filter(receiver => receiver.track.readyState == "live")
+ .map(receiver => {
+ info(
+ "Waiting for track " +
+ receiver.track.id +
+ " (" +
+ receiver.track.kind +
+ ") to end."
+ );
+ return haveEvent(receiver.track, "ended", wait(50000)).then(
+ event => {
+ is(
+ event.target,
+ receiver.track,
+ "Event target should be the correct track"
+ );
+ info(pc + " ended fired for track " + receiver.track.id);
+ },
+ e =>
+ e
+ ? Promise.reject(e)
+ : ok(
+ false,
+ "ended never fired for track " + receiver.track.id
+ )
+ );
+ })
+ ),
+ ]);
+ pc.close();
+ return promise;
+ };
+
+ return timerGuard(
+ Promise.all([closeIt(this.pcLocal), closeIt(this.pcRemote)]),
+ 60000,
+ "failed to close peer connection"
+ );
+};
+
+/**
+ * Close the open data channels, followed by the underlying peer connection
+ */
+PeerConnectionTest.prototype.close = function () {
+ var allChannels = (this.pcLocal || this.pcRemote).dataChannels;
+ return timerGuard(
+ Promise.all(allChannels.map((channel, i) => this.closeDataChannels(i))),
+ 120000,
+ "failed to close data channels"
+ ).then(() => this.closePC());
+};
+
+/**
+ * Close the specified data channels
+ *
+ * @param {Number} index
+ * Index of the data channels to close on both sides
+ */
+PeerConnectionTest.prototype.closeDataChannels = function (index) {
+ info("closeDataChannels called with index: " + index);
+ var localChannel = null;
+ if (this.pcLocal) {
+ localChannel = this.pcLocal.dataChannels[index];
+ }
+ var remoteChannel = null;
+ if (this.pcRemote) {
+ remoteChannel = this.pcRemote.dataChannels[index];
+ }
+
+ // We need to setup all the close listeners before calling close
+ var setupClosePromise = channel => {
+ if (!channel) {
+ return Promise.resolve();
+ }
+ return new Promise(resolve => {
+ channel.onclose = () => {
+ is(
+ channel.readyState,
+ "closed",
+ name + " channel " + index + " closed"
+ );
+ resolve();
+ };
+ });
+ };
+
+ // make sure to setup close listeners before triggering any actions
+ var allClosed = Promise.all([
+ setupClosePromise(localChannel),
+ setupClosePromise(remoteChannel),
+ ]);
+ var complete = timerGuard(
+ allClosed,
+ 120000,
+ "failed to close data channel pair"
+ );
+
+ // triggering close on one side should suffice
+ if (remoteChannel) {
+ remoteChannel.close();
+ } else if (localChannel) {
+ localChannel.close();
+ }
+
+ return complete;
+};
+
+/**
+ * Send data (message or blob) to the other peer
+ *
+ * @param {String|Blob} data
+ * Data to send to the other peer. For Blobs the MIME type will be lost.
+ * @param {Object} [options={ }]
+ * Options to specify the data channels to be used
+ * @param {DataChannelWrapper} [options.sourceChannel=pcLocal.dataChannels[length - 1]]
+ * Data channel to use for sending the message
+ * @param {DataChannelWrapper} [options.targetChannel=pcRemote.dataChannels[length - 1]]
+ * Data channel to use for receiving the message
+ */
+PeerConnectionTest.prototype.send = async function (data, options) {
+ options = options || {};
+ const source =
+ options.sourceChannel ||
+ this.pcLocal.dataChannels[this.pcLocal.dataChannels.length - 1];
+ const target =
+ options.targetChannel ||
+ this.pcRemote.dataChannels[this.pcRemote.dataChannels.length - 1];
+ source.bufferedAmountLowThreshold = options.bufferedAmountLowThreshold || 0;
+
+ const getSizeInBytes = d => {
+ if (d instanceof Blob) {
+ return d.size;
+ } else if (d instanceof ArrayBuffer) {
+ return d.byteLength;
+ } else if (d instanceof String || typeof d === "string") {
+ return new TextEncoder().encode(d).length;
+ } else {
+ ok(false);
+ }
+ };
+
+ const expectedSizeInBytes = getSizeInBytes(data);
+ const bufferedAmount = source.bufferedAmount;
+
+ source.send(data);
+ is(
+ source.bufferedAmount,
+ expectedSizeInBytes + bufferedAmount,
+ `Buffered amount should be ${expectedSizeInBytes}`
+ );
+
+ await new Promise(resolve => (source.onbufferedamountlow = resolve));
+
+ return new Promise(resolve => {
+ // Register event handler for the target channel
+ target.onmessage = e => {
+ is(
+ getSizeInBytes(e.data),
+ expectedSizeInBytes,
+ `Expected to receive the same number of bytes as we sent (${expectedSizeInBytes})`
+ );
+ resolve({ channel: target, data: e.data });
+ };
+ });
+};
+
+/**
+ * Create a data channel
+ *
+ * @param {Dict} options
+ * Options for the data channel (see nsIPeerConnection)
+ */
+PeerConnectionTest.prototype.createDataChannel = function (options) {
+ var remotePromise;
+ if (!options.negotiated) {
+ this.pcRemote.expectDataChannel("pcRemote expected data channel");
+ remotePromise = this.pcRemote.nextDataChannel;
+ }
+
+ // Create the datachannel
+ var localChannel = this.pcLocal.createDataChannel(options);
+ var localPromise = localChannel.opened;
+
+ if (options.negotiated) {
+ remotePromise = localPromise.then(localChannel => {
+ // externally negotiated - we need to open from both ends
+ options.id = options.id || channel.id; // allow for no id on options
+ var remoteChannel = this.pcRemote.createDataChannel(options);
+ return remoteChannel.opened;
+ });
+ }
+
+ // pcRemote.observedNegotiationNeeded might be undefined if
+ // !options.negotiated, which means we just wait on pcLocal
+ return Promise.all([
+ this.pcLocal.observedNegotiationNeeded,
+ this.pcRemote.observedNegotiationNeeded,
+ ]).then(() => {
+ return Promise.all([localPromise, remotePromise]).then(result => {
+ return { local: result[0], remote: result[1] };
+ });
+ });
+};
+
+/**
+ * Creates an answer for the specified peer connection instance
+ * and automatically handles the failure case.
+ *
+ * @param {PeerConnectionWrapper} peer
+ * The peer connection wrapper to run the command on
+ */
+PeerConnectionTest.prototype.createAnswer = function (peer) {
+ return peer.createAnswer().then(answer => {
+ // make a copy so this does not get updated with ICE candidates
+ this.originalAnswer = new RTCSessionDescription(
+ JSON.parse(JSON.stringify(answer))
+ );
+ return answer;
+ });
+};
+
+/**
+ * Creates an offer for the specified peer connection instance
+ * and automatically handles the failure case.
+ *
+ * @param {PeerConnectionWrapper} peer
+ * The peer connection wrapper to run the command on
+ */
+PeerConnectionTest.prototype.createOffer = function (peer) {
+ return peer.createOffer().then(offer => {
+ // make a copy so this does not get updated with ICE candidates
+ this.originalOffer = new RTCSessionDescription(
+ JSON.parse(JSON.stringify(offer))
+ );
+ return offer;
+ });
+};
+
+/**
+ * Sets the local description for the specified peer connection instance
+ * and automatically handles the failure case.
+ *
+ * @param {PeerConnectionWrapper} peer
+ The peer connection wrapper to run the command on
+ * @param {RTCSessionDescriptionInit} desc
+ * Session description for the local description request
+ */
+PeerConnectionTest.prototype.setLocalDescription = function (
+ peer,
+ desc,
+ stateExpected
+) {
+ var eventFired = new Promise(resolve => {
+ peer.onsignalingstatechange = e => {
+ info(peer + ": 'signalingstatechange' event received");
+ var state = e.target.signalingState;
+ if (stateExpected === state) {
+ peer.setLocalDescStableEventDate = new Date();
+ resolve();
+ } else {
+ ok(
+ false,
+ "This event has either already fired or there has been a " +
+ "mismatch between event received " +
+ state +
+ " and event expected " +
+ stateExpected
+ );
+ }
+ };
+ });
+
+ var stateChanged = peer.setLocalDescription(desc).then(() => {
+ peer.setLocalDescDate = new Date();
+ });
+
+ peer.endOfTrickleSdp = peer.endOfTrickleIce
+ .then(() => {
+ return peer._pc.localDescription;
+ })
+ .catch(e => ok(false, "Sending EOC message failed: " + e));
+
+ return Promise.all([eventFired, stateChanged]);
+};
+
+/**
+ * Sets the media constraints for both peer connection instances.
+ *
+ * @param {object} constraintsLocal
+ * Media constrains for the local peer connection instance
+ * @param constraintsRemote
+ */
+PeerConnectionTest.prototype.setMediaConstraints = function (
+ constraintsLocal,
+ constraintsRemote
+) {
+ if (this.pcLocal) {
+ this.pcLocal.constraints = constraintsLocal;
+ }
+ if (this.pcRemote) {
+ this.pcRemote.constraints = constraintsRemote;
+ }
+};
+
+/**
+ * Sets the media options used on a createOffer call in the test.
+ *
+ * @param {object} options the media constraints to use on createOffer
+ */
+PeerConnectionTest.prototype.setOfferOptions = function (options) {
+ if (this.pcLocal) {
+ this.pcLocal.offerOptions = options;
+ }
+};
+
+/**
+ * Sets the remote description for the specified peer connection instance
+ * and automatically handles the failure case.
+ *
+ * @param {PeerConnectionWrapper} peer
+ The peer connection wrapper to run the command on
+ * @param {RTCSessionDescriptionInit} desc
+ * Session description for the remote description request
+ */
+PeerConnectionTest.prototype.setRemoteDescription = function (
+ peer,
+ desc,
+ stateExpected
+) {
+ var eventFired = new Promise(resolve => {
+ peer.onsignalingstatechange = e => {
+ info(peer + ": 'signalingstatechange' event received");
+ var state = e.target.signalingState;
+ if (stateExpected === state) {
+ peer.setRemoteDescStableEventDate = new Date();
+ resolve();
+ } else {
+ ok(
+ false,
+ "This event has either already fired or there has been a " +
+ "mismatch between event received " +
+ state +
+ " and event expected " +
+ stateExpected
+ );
+ }
+ };
+ });
+
+ var stateChanged = peer.setRemoteDescription(desc).then(() => {
+ peer.setRemoteDescDate = new Date();
+ peer.checkMediaTracks();
+ });
+
+ return Promise.all([eventFired, stateChanged]);
+};
+
+/**
+ * Adds and removes steps to/from the execution chain based on the configured
+ * testOptions.
+ */
+PeerConnectionTest.prototype.updateChainSteps = function () {
+ if (this.testOptions.h264) {
+ this.chain.insertAfterEach("PC_LOCAL_CREATE_OFFER", [
+ PC_LOCAL_REMOVE_ALL_BUT_H264_FROM_OFFER,
+ ]);
+ }
+ if (!this.testOptions.bundle) {
+ this.chain.insertAfterEach("PC_LOCAL_CREATE_OFFER", [
+ PC_LOCAL_REMOVE_BUNDLE_FROM_OFFER,
+ ]);
+ }
+ if (!this.testOptions.rtcpmux) {
+ this.chain.insertAfterEach("PC_LOCAL_CREATE_OFFER", [
+ PC_LOCAL_REMOVE_RTCPMUX_FROM_OFFER,
+ ]);
+ }
+ if (!this.testOptions.ssrc) {
+ this.chain.insertAfterEach("PC_LOCAL_CREATE_OFFER", [
+ PC_LOCAL_REMOVE_SSRC_FROM_OFFER,
+ ]);
+ this.chain.insertAfterEach("PC_REMOTE_CREATE_ANSWER", [
+ PC_REMOTE_REMOVE_SSRC_FROM_ANSWER,
+ ]);
+ }
+ if (!this.testOptions.is_local) {
+ this.chain.filterOut(/^PC_LOCAL/);
+ }
+ if (!this.testOptions.is_remote) {
+ this.chain.filterOut(/^PC_REMOTE/);
+ }
+};
+
+/**
+ * Start running the tests as assigned to the command chain.
+ */
+PeerConnectionTest.prototype.run = async function () {
+ /* We have to modify the chain here to allow tests which modify the default
+ * test chain instantiating a PeerConnectionTest() */
+ this.updateChainSteps();
+ try {
+ await this.chain.execute();
+ await this.close();
+ } catch (e) {
+ const stack =
+ typeof e.stack === "string"
+ ? ` ${e.stack.split("\n").join(" ... ")}`
+ : "";
+ ok(false, `Error in test execution: ${e} (${stack})`);
+ }
+};
+
+/**
+ * Routes ice candidates from one PCW to the other PCW
+ */
+PeerConnectionTest.prototype.iceCandidateHandler = function (
+ caller,
+ candidate
+) {
+ info("Received: " + JSON.stringify(candidate) + " from " + caller);
+
+ var target = null;
+ if (caller.includes("pcLocal")) {
+ if (this.pcRemote) {
+ target = this.pcRemote;
+ }
+ } else if (caller.includes("pcRemote")) {
+ if (this.pcLocal) {
+ target = this.pcLocal;
+ }
+ } else {
+ ok(false, "received event from unknown caller: " + caller);
+ return;
+ }
+
+ if (target) {
+ target.storeOrAddIceCandidate(candidate);
+ } else {
+ info("sending ice candidate to signaling server");
+ send_message({ type: "ice_candidate", ice_candidate: candidate });
+ }
+};
+
+/**
+ * Installs a polling function for the socket.io client to read
+ * all messages from the chat room into a message queue.
+ */
+PeerConnectionTest.prototype.setupSignalingClient = function () {
+ this.signalingMessageQueue = [];
+ this.signalingCallbacks = {};
+ this.signalingLoopRun = true;
+
+ var queueMessage = message => {
+ info("Received signaling message: " + JSON.stringify(message));
+ var fired = false;
+ Object.keys(this.signalingCallbacks).forEach(name => {
+ if (name === message.type) {
+ info("Invoking callback for message type: " + name);
+ this.signalingCallbacks[name](message);
+ fired = true;
+ }
+ });
+ if (!fired) {
+ this.signalingMessageQueue.push(message);
+ info(
+ "signalingMessageQueue.length: " + this.signalingMessageQueue.length
+ );
+ }
+ if (this.signalingLoopRun) {
+ wait_for_message().then(queueMessage);
+ } else {
+ info("Exiting signaling message event loop");
+ }
+ };
+ wait_for_message().then(queueMessage);
+};
+
+/**
+ * Sets a flag to stop reading further messages from the chat room.
+ */
+PeerConnectionTest.prototype.signalingMessagesFinished = function () {
+ this.signalingLoopRun = false;
+};
+
+/**
+ * Register a callback function to deliver messages from the chat room
+ * directly instead of storing them in the message queue.
+ *
+ * @param {string} messageType
+ * For which message types should the callback get invoked.
+ *
+ * @param {function} onMessage
+ * The function which gets invoked if a message of the messageType
+ * has been received from the chat room.
+ */
+PeerConnectionTest.prototype.registerSignalingCallback = function (
+ messageType,
+ onMessage
+) {
+ this.signalingCallbacks[messageType] = onMessage;
+};
+
+/**
+ * Searches the message queue for the first message of a given type
+ * and invokes the given callback function, or registers the callback
+ * function for future messages if the queue contains no such message.
+ *
+ * @param {string} messageType
+ * The type of message to search and register for.
+ */
+PeerConnectionTest.prototype.getSignalingMessage = function (messageType) {
+ var i = this.signalingMessageQueue.findIndex(m => m.type === messageType);
+ if (i >= 0) {
+ info(
+ "invoking callback on message " +
+ i +
+ " from message queue, for message type:" +
+ messageType
+ );
+ return Promise.resolve(this.signalingMessageQueue.splice(i, 1)[0]);
+ }
+ return new Promise(resolve =>
+ this.registerSignalingCallback(messageType, resolve)
+ );
+};
+
+/**
+ * This class acts as a wrapper around a DataChannel instance.
+ *
+ * @param dataChannel
+ * @param peerConnectionWrapper
+ * @constructor
+ */
+function DataChannelWrapper(dataChannel, peerConnectionWrapper) {
+ this._channel = dataChannel;
+ this._pc = peerConnectionWrapper;
+
+ info("Creating " + this);
+
+ /**
+ * Setup appropriate callbacks
+ */
+ createOneShotEventWrapper(this, this._channel, "close");
+ createOneShotEventWrapper(this, this._channel, "error");
+ createOneShotEventWrapper(this, this._channel, "message");
+ createOneShotEventWrapper(this, this._channel, "bufferedamountlow");
+
+ this.opened = timerGuard(
+ new Promise(resolve => {
+ this._channel.onopen = () => {
+ this._channel.onopen = unexpectedEvent(this, "onopen");
+ is(this.readyState, "open", "data channel is 'open' after 'onopen'");
+ resolve(this);
+ };
+ }),
+ 180000,
+ "channel didn't open in time"
+ );
+}
+
+DataChannelWrapper.prototype = {
+ /**
+ * Returns the binary type of the channel
+ *
+ * @returns {String} The binary type
+ */
+ get binaryType() {
+ return this._channel.binaryType;
+ },
+
+ /**
+ * Sets the binary type of the channel
+ *
+ * @param {String} type
+ * The new binary type of the channel
+ */
+ set binaryType(type) {
+ this._channel.binaryType = type;
+ },
+
+ /**
+ * Returns the label of the underlying data channel
+ *
+ * @returns {String} The label
+ */
+ get label() {
+ return this._channel.label;
+ },
+
+ /**
+ * Returns the protocol of the underlying data channel
+ *
+ * @returns {String} The protocol
+ */
+ get protocol() {
+ return this._channel.protocol;
+ },
+
+ /**
+ * Returns the id of the underlying data channel
+ *
+ * @returns {number} The stream id
+ */
+ get id() {
+ return this._channel.id;
+ },
+
+ /**
+ * Returns the reliable state of the underlying data channel
+ *
+ * @returns {bool} The stream's reliable state
+ */
+ get reliable() {
+ return this._channel.reliable;
+ },
+
+ /**
+ * Returns the ordered attribute of the data channel
+ *
+ * @returns {bool} The ordered attribute
+ */
+ get ordered() {
+ return this._channel.ordered;
+ },
+
+ /**
+ * Returns the maxPacketLifeTime attribute of the data channel
+ *
+ * @returns {number} The maxPacketLifeTime attribute
+ */
+ get maxPacketLifeTime() {
+ return this._channel.maxPacketLifeTime;
+ },
+
+ /**
+ * Returns the maxRetransmits attribute of the data channel
+ *
+ * @returns {number} The maxRetransmits attribute
+ */
+ get maxRetransmits() {
+ return this._channel.maxRetransmits;
+ },
+
+ /**
+ * Returns the readyState bit of the data channel
+ *
+ * @returns {String} The state of the channel
+ */
+ get readyState() {
+ return this._channel.readyState;
+ },
+
+ get bufferedAmount() {
+ return this._channel.bufferedAmount;
+ },
+
+ /**
+ * Sets the bufferlowthreshold of the channel
+ *
+ * @param {integer} amoutn
+ * The new threshold for the chanel
+ */
+ set bufferedAmountLowThreshold(amount) {
+ this._channel.bufferedAmountLowThreshold = amount;
+ },
+
+ /**
+ * Close the data channel
+ */
+ close() {
+ info(this + ": Closing channel");
+ this._channel.close();
+ },
+
+ /**
+ * Send data through the data channel
+ *
+ * @param {String|Object} data
+ * Data which has to be sent through the data channel
+ */
+ send(data) {
+ info(this + ": Sending data '" + data + "'");
+ this._channel.send(data);
+ },
+
+ /**
+ * Returns the string representation of the class
+ *
+ * @returns {String} The string representation
+ */
+ toString() {
+ return (
+ "DataChannelWrapper (" + this._pc.label + "_" + this._channel.label + ")"
+ );
+ },
+};
+
+/**
+ * This class acts as a wrapper around a PeerConnection instance.
+ *
+ * @constructor
+ * @param {string} label
+ * Description for the peer connection instance
+ * @param {object} configuration
+ * Configuration for the peer connection instance
+ */
+function PeerConnectionWrapper(label, configuration) {
+ this.configuration = configuration;
+ if (configuration && configuration.label_suffix) {
+ label = label + "_" + configuration.label_suffix;
+ }
+ this.label = label;
+
+ this.constraints = [];
+ this.offerOptions = {};
+
+ this.dataChannels = [];
+
+ this._local_ice_candidates = [];
+ this._remote_ice_candidates = [];
+ this.localRequiresTrickleIce = false;
+ this.remoteRequiresTrickleIce = false;
+ this.localMediaElements = [];
+ this.remoteMediaElements = [];
+ this.audioElementsOnly = false;
+
+ this._sendStreams = [];
+
+ this.expectedLocalTrackInfo = [];
+ this.remoteStreamsByTrackId = new Map();
+
+ this.disableRtpCountChecking = false;
+
+ this.iceConnectedResolve;
+ this.iceConnectedReject;
+ this.iceConnected = new Promise((resolve, reject) => {
+ this.iceConnectedResolve = resolve;
+ this.iceConnectedReject = reject;
+ });
+ this.iceCheckingRestartExpected = false;
+ this.iceCheckingIceRollbackExpected = false;
+
+ info("Creating " + this);
+ this._pc = new RTCPeerConnection(this.configuration);
+
+ /**
+ * Setup callback handlers
+ */
+ // This allows test to register their own callbacks for ICE connection state changes
+ this.ice_connection_callbacks = {};
+
+ this._pc.oniceconnectionstatechange = e => {
+ isnot(
+ typeof this._pc.iceConnectionState,
+ "undefined",
+ "iceConnectionState should not be undefined"
+ );
+ var iceState = this._pc.iceConnectionState;
+ info(
+ this + ": oniceconnectionstatechange fired, new state is: " + iceState
+ );
+ Object.keys(this.ice_connection_callbacks).forEach(name => {
+ this.ice_connection_callbacks[name]();
+ });
+ if (iceState === "connected") {
+ this.iceConnectedResolve();
+ } else if (iceState === "failed") {
+ this.iceConnectedReject(new Error("ICE failed"));
+ }
+ };
+
+ this._pc.onicegatheringstatechange = e => {
+ isnot(
+ typeof this._pc.iceGatheringState,
+ "undefined",
+ "iceGetheringState should not be undefined"
+ );
+ var gatheringState = this._pc.iceGatheringState;
+ info(
+ this +
+ ": onicegatheringstatechange fired, new state is: " +
+ gatheringState
+ );
+ };
+
+ createOneShotEventWrapper(this, this._pc, "datachannel");
+ this._pc.addEventListener("datachannel", e => {
+ var wrapper = new DataChannelWrapper(e.channel, this);
+ this.dataChannels.push(wrapper);
+ });
+
+ createOneShotEventWrapper(this, this._pc, "signalingstatechange");
+ createOneShotEventWrapper(this, this._pc, "negotiationneeded");
+}
+
+PeerConnectionWrapper.prototype = {
+ /**
+ * Returns the senders
+ *
+ * @returns {sequence<RTCRtpSender>} the senders
+ */
+ getSenders() {
+ return this._pc.getSenders();
+ },
+
+ /**
+ * Returns the getters
+ *
+ * @returns {sequence<RTCRtpReceiver>} the receivers
+ */
+ getReceivers() {
+ return this._pc.getReceivers();
+ },
+
+ /**
+ * Returns the local description.
+ *
+ * @returns {object} The local description
+ */
+ get localDescription() {
+ return this._pc.localDescription;
+ },
+
+ /**
+ * Returns the remote description.
+ *
+ * @returns {object} The remote description
+ */
+ get remoteDescription() {
+ return this._pc.remoteDescription;
+ },
+
+ /**
+ * Returns the signaling state.
+ *
+ * @returns {object} The local description
+ */
+ get signalingState() {
+ return this._pc.signalingState;
+ },
+ /**
+ * Returns the ICE connection state.
+ *
+ * @returns {object} The local description
+ */
+ get iceConnectionState() {
+ return this._pc.iceConnectionState;
+ },
+
+ setIdentityProvider(provider, options) {
+ this._pc.setIdentityProvider(provider, options);
+ },
+
+ elementPrefix: direction => {
+ return [this.label, direction].join("_");
+ },
+
+ getMediaElementForTrack(track, direction) {
+ var prefix = this.elementPrefix(direction);
+ return getMediaElementForTrack(track, prefix);
+ },
+
+ createMediaElementForTrack(track, direction) {
+ var prefix = this.elementPrefix(direction);
+ return createMediaElementForTrack(track, prefix);
+ },
+
+ ensureMediaElement(track, direction) {
+ var prefix = this.elementPrefix(direction);
+ var element = this.getMediaElementForTrack(track, direction);
+ if (!element) {
+ element = this.createMediaElementForTrack(track, direction);
+ if (direction == "local") {
+ this.localMediaElements.push(element);
+ } else if (direction == "remote") {
+ this.remoteMediaElements.push(element);
+ }
+ }
+
+ // We do this regardless, because sometimes we end up with a new stream with
+ // an old id (ie; the rollback tests cause the same stream to be added
+ // twice)
+ element.srcObject = new MediaStream([track]);
+ element.play();
+ },
+
+ addSendStream(stream) {
+ // The PeerConnection will not necessarily know about this stream
+ // automatically, because replaceTrack is not told about any streams the
+ // new track might be associated with. Only content really knows.
+ this._sendStreams.push(stream);
+ },
+
+ getStreamForSendTrack(track) {
+ return this._sendStreams.find(str => str.getTrackById(track.id));
+ },
+
+ getStreamForRecvTrack(track) {
+ return this._pc.getRemoteStreams().find(s => !!s.getTrackById(track.id));
+ },
+
+ /**
+ * Attaches a local track to this RTCPeerConnection using
+ * RTCPeerConnection.addTrack().
+ *
+ * Also creates a media element playing a MediaStream containing all
+ * tracks that have been added to `stream` using `attachLocalTrack()`.
+ *
+ * @param {MediaStreamTrack} track
+ * MediaStreamTrack to handle
+ * @param {MediaStream} stream
+ * MediaStream to use as container for `track` on remote side
+ */
+ attachLocalTrack(track, stream) {
+ info("Got a local " + track.kind + " track");
+
+ this.expectNegotiationNeeded();
+ var sender = this._pc.addTrack(track, stream);
+ is(sender.track, track, "addTrack returns sender");
+ is(
+ this._pc.getSenders().pop(),
+ sender,
+ "Sender should be the last element in getSenders()"
+ );
+
+ ok(track.id, "track has id");
+ ok(track.kind, "track has kind");
+ ok(stream.id, "stream has id");
+ this.expectedLocalTrackInfo.push({ track, sender, streamId: stream.id });
+ this.addSendStream(stream);
+
+ // This will create one media element per track, which might not be how
+ // we set up things with the RTCPeerConnection. It's the only way
+ // we can ensure all sent tracks are flowing however.
+ this.ensureMediaElement(track, "local");
+
+ return this.observedNegotiationNeeded;
+ },
+
+ /**
+ * Callback when we get local media. Also an appropriate HTML media element
+ * will be created and added to the content node.
+ *
+ * @param {MediaStream} stream
+ * Media stream to handle
+ */
+ attachLocalStream(stream, useAddTransceiver) {
+ info("Got local media stream: (" + stream.id + ")");
+
+ this.expectNegotiationNeeded();
+ if (useAddTransceiver) {
+ info("Using addTransceiver (on PC).");
+ stream.getTracks().forEach(track => {
+ var transceiver = this._pc.addTransceiver(track, { streams: [stream] });
+ is(transceiver.sender.track, track, "addTransceiver returns sender");
+ });
+ }
+ // In order to test both the addStream and addTrack APIs, we do half one
+ // way, half the other, at random.
+ else if (Math.random() < 0.5) {
+ info("Using addStream.");
+ this._pc.addStream(stream);
+ ok(
+ this._pc
+ .getSenders()
+ .find(sender => sender.track == stream.getTracks()[0]),
+ "addStream returns sender"
+ );
+ } else {
+ info("Using addTrack (on PC).");
+ stream.getTracks().forEach(track => {
+ var sender = this._pc.addTrack(track, stream);
+ is(sender.track, track, "addTrack returns sender");
+ });
+ }
+
+ this.addSendStream(stream);
+
+ stream.getTracks().forEach(track => {
+ ok(track.id, "track has id");
+ ok(track.kind, "track has kind");
+ const sender = this._pc.getSenders().find(s => s.track == track);
+ ok(sender, "track has a sender");
+ this.expectedLocalTrackInfo.push({ track, sender, streamId: stream.id });
+ this.ensureMediaElement(track, "local");
+ });
+
+ return this.observedNegotiationNeeded;
+ },
+
+ removeSender(index) {
+ var sender = this._pc.getSenders()[index];
+ this.expectedLocalTrackInfo = this.expectedLocalTrackInfo.filter(
+ i => i.sender != sender
+ );
+ this.expectNegotiationNeeded();
+ this._pc.removeTrack(sender);
+ return this.observedNegotiationNeeded;
+ },
+
+ senderReplaceTrack(sender, withTrack, stream) {
+ const info = this.expectedLocalTrackInfo.find(i => i.sender == sender);
+ if (!info) {
+ return; // replaceTrack on a null track, probably
+ }
+ info.track = withTrack;
+ this.addSendStream(stream);
+ this.ensureMediaElement(withTrack, "local");
+ return sender.replaceTrack(withTrack);
+ },
+
+ async getUserMedia(constraints) {
+ SpecialPowers.wrap(document).notifyUserGestureActivation();
+ var stream = await getUserMedia(constraints);
+ if (constraints.audio) {
+ stream.getAudioTracks().forEach(track => {
+ info(
+ this +
+ " gUM local stream " +
+ stream.id +
+ " with audio track " +
+ track.id
+ );
+ });
+ }
+ if (constraints.video) {
+ stream.getVideoTracks().forEach(track => {
+ info(
+ this +
+ " gUM local stream " +
+ stream.id +
+ " with video track " +
+ track.id
+ );
+ });
+ }
+ return stream;
+ },
+
+ /**
+ * Requests all the media streams as specified in the constrains property.
+ *
+ * @param {array} constraintsList
+ * Array of constraints for GUM calls
+ */
+ getAllUserMedia(constraintsList) {
+ if (constraintsList.length === 0) {
+ info("Skipping GUM: no UserMedia requested");
+ return Promise.resolve();
+ }
+
+ info("Get " + constraintsList.length + " local streams");
+ return Promise.all(
+ constraintsList.map(constraints => this.getUserMedia(constraints))
+ );
+ },
+
+ async getAllUserMediaAndAddStreams(constraintsList) {
+ var streams = await this.getAllUserMedia(constraintsList);
+ if (!streams) {
+ return;
+ }
+ return Promise.all(streams.map(stream => this.attachLocalStream(stream)));
+ },
+
+ async getAllUserMediaAndAddTransceivers(constraintsList) {
+ var streams = await this.getAllUserMedia(constraintsList);
+ if (!streams) {
+ return;
+ }
+ return Promise.all(
+ streams.map(stream => this.attachLocalStream(stream, true))
+ );
+ },
+
+ /**
+ * Create a new data channel instance. Also creates a promise called
+ * `this.nextDataChannel` that resolves when the next data channel arrives.
+ */
+ expectDataChannel(message) {
+ this.nextDataChannel = new Promise(resolve => {
+ this.ondatachannel = e => {
+ ok(e.channel, message);
+ is(
+ e.channel.readyState,
+ "open",
+ "data channel in 'open' after 'ondatachannel'"
+ );
+ resolve(e.channel);
+ };
+ });
+ },
+
+ /**
+ * Create a new data channel instance
+ *
+ * @param {Object} options
+ * Options which get forwarded to nsIPeerConnection.createDataChannel
+ * @returns {DataChannelWrapper} The created data channel
+ */
+ createDataChannel(options) {
+ var label = "channel_" + this.dataChannels.length;
+ info(this + ": Create data channel '" + label);
+
+ if (!this.dataChannels.length) {
+ this.expectNegotiationNeeded();
+ }
+ var channel = this._pc.createDataChannel(label, options);
+ is(channel.readyState, "connecting", "initial readyState is 'connecting'");
+ var wrapper = new DataChannelWrapper(channel, this);
+ this.dataChannels.push(wrapper);
+ return wrapper;
+ },
+
+ /**
+ * Creates an offer and automatically handles the failure case.
+ */
+ createOffer() {
+ return this._pc.createOffer(this.offerOptions).then(offer => {
+ info("Got offer: " + JSON.stringify(offer));
+ // note: this might get updated through ICE gathering
+ this._latest_offer = offer;
+ return offer;
+ });
+ },
+
+ /**
+ * Creates an answer and automatically handles the failure case.
+ */
+ createAnswer() {
+ return this._pc.createAnswer().then(answer => {
+ info(this + ": Got answer: " + JSON.stringify(answer));
+ this._last_answer = answer;
+ return answer;
+ });
+ },
+
+ /**
+ * Sets the local description and automatically handles the failure case.
+ *
+ * @param {object} desc
+ * RTCSessionDescriptionInit for the local description request
+ */
+ setLocalDescription(desc) {
+ this.observedNegotiationNeeded = undefined;
+ return this._pc.setLocalDescription(desc).then(() => {
+ info(this + ": Successfully set the local description");
+ });
+ },
+
+ /**
+ * Tries to set the local description and expect failure. Automatically
+ * causes the test case to fail if the call succeeds.
+ *
+ * @param {object} desc
+ * RTCSessionDescriptionInit for the local description request
+ * @returns {Promise}
+ * A promise that resolves to the expected error
+ */
+ setLocalDescriptionAndFail(desc) {
+ return this._pc
+ .setLocalDescription(desc)
+ .then(
+ generateErrorCallback("setLocalDescription should have failed."),
+ err => {
+ info(this + ": As expected, failed to set the local description");
+ return err;
+ }
+ );
+ },
+
+ /**
+ * Sets the remote description and automatically handles the failure case.
+ *
+ * @param {object} desc
+ * RTCSessionDescriptionInit for the remote description request
+ */
+ setRemoteDescription(desc) {
+ this.observedNegotiationNeeded = undefined;
+ // This has to be done before calling sRD, otherwise a candidate in flight
+ // could end up in the PC's operations queue before sRD resolves.
+ if (desc.type == "rollback") {
+ this.holdIceCandidates = new Promise(
+ r => (this.releaseIceCandidates = r)
+ );
+ }
+ return this._pc.setRemoteDescription(desc).then(() => {
+ info(this + ": Successfully set remote description");
+ if (desc.type != "rollback") {
+ this.releaseIceCandidates();
+ }
+ });
+ },
+
+ /**
+ * Tries to set the remote description and expect failure. Automatically
+ * causes the test case to fail if the call succeeds.
+ *
+ * @param {object} desc
+ * RTCSessionDescriptionInit for the remote description request
+ * @returns {Promise}
+ * a promise that resolve to the returned error
+ */
+ setRemoteDescriptionAndFail(desc) {
+ return this._pc
+ .setRemoteDescription(desc)
+ .then(
+ generateErrorCallback("setRemoteDescription should have failed."),
+ err => {
+ info(this + ": As expected, failed to set the remote description");
+ return err;
+ }
+ );
+ },
+
+ /**
+ * Registers a callback for the signaling state change and
+ * appends the new state to an array for logging it later.
+ */
+ logSignalingState() {
+ this.signalingStateLog = [this._pc.signalingState];
+ this._pc.addEventListener("signalingstatechange", e => {
+ var newstate = this._pc.signalingState;
+ var oldstate = this.signalingStateLog[this.signalingStateLog.length - 1];
+ if (Object.keys(signalingStateTransitions).includes(oldstate)) {
+ ok(
+ signalingStateTransitions[oldstate].includes(newstate),
+ this +
+ ": legal signaling state transition from " +
+ oldstate +
+ " to " +
+ newstate
+ );
+ } else {
+ ok(
+ false,
+ this +
+ ": old signaling state " +
+ oldstate +
+ " missing in signaling transition array"
+ );
+ }
+ this.signalingStateLog.push(newstate);
+ });
+ },
+
+ isTrackOnPC(track) {
+ return !!this.getStreamForRecvTrack(track);
+ },
+
+ allExpectedTracksAreObserved(expected, observed) {
+ return Object.keys(expected).every(trackId => observed[trackId]);
+ },
+
+ setupStreamEventHandlers(stream) {
+ const myTrackIds = new Set(stream.getTracks().map(t => t.id));
+
+ stream.addEventListener("addtrack", ({ track }) => {
+ ok(
+ !myTrackIds.has(track.id),
+ "Duplicate addtrack callback: " +
+ `stream id=${stream.id} track id=${track.id}`
+ );
+ myTrackIds.add(track.id);
+ // addtrack events happen before track events, so the track callback hasn't
+ // heard about this yet.
+ let streams = this.remoteStreamsByTrackId.get(track.id);
+ ok(
+ !streams || !streams.has(stream.id),
+ `In addtrack for stream id=${stream.id}` +
+ `there should not have been a track event for track id=${track.id} ` +
+ " containing this stream yet."
+ );
+ ok(
+ stream.getTracks().includes(track),
+ "In addtrack, stream id=" +
+ `${stream.id} should already contain track id=${track.id}`
+ );
+ });
+
+ stream.addEventListener("removetrack", ({ track }) => {
+ ok(
+ myTrackIds.has(track.id),
+ "Duplicate removetrack callback: " +
+ `stream id=${stream.id} track id=${track.id}`
+ );
+ myTrackIds.delete(track.id);
+ // Also remove the association from remoteStreamsByTrackId
+ const streams = this.remoteStreamsByTrackId.get(track.id);
+ ok(
+ streams,
+ `In removetrack for stream id=${stream.id}, track id=` +
+ `${track.id} should have had a track callback for the stream.`
+ );
+ streams.delete(stream.id);
+ ok(
+ !stream.getTracks().includes(track),
+ "In removetrack, stream id=" +
+ `${stream.id} should not contain track id=${track.id}`
+ );
+ });
+ },
+
+ setupTrackEventHandler() {
+ this._pc.addEventListener("track", ({ track, streams }) => {
+ info(`${this}: 'ontrack' event fired for ${track.id}`);
+ ok(this.isTrackOnPC(track), `Found track ${track.id}`);
+
+ let gratuitousEvent = true;
+ let streamsContainingTrack = this.remoteStreamsByTrackId.get(track.id);
+ if (!streamsContainingTrack) {
+ gratuitousEvent = false; // Told us about a new track
+ this.remoteStreamsByTrackId.set(track.id, new Set());
+ streamsContainingTrack = this.remoteStreamsByTrackId.get(track.id);
+ }
+
+ for (const stream of streams) {
+ ok(
+ stream.getTracks().includes(track),
+ `In track event, track id=${track.id}` +
+ ` should already be in stream id=${stream.id}`
+ );
+
+ if (!streamsContainingTrack.has(stream.id)) {
+ gratuitousEvent = false; // Told us about a new stream
+ streamsContainingTrack.add(stream.id);
+ this.setupStreamEventHandlers(stream);
+ }
+ }
+
+ ok(!gratuitousEvent, "track event told us something new");
+
+ // So far, we've verified consistency between the current state of the
+ // streams, addtrack/removetrack events on the streams, and track events
+ // on the peerconnection. We have also verified that we have not gotten
+ // any gratuitous events. We have not done anything to verify that the
+ // current state of affairs matches what we were expecting it to.
+
+ this.ensureMediaElement(track, "remote");
+ });
+ },
+
+ /**
+ * Either adds a given ICE candidate right away or stores it to be added
+ * later, depending on the state of the PeerConnection.
+ *
+ * @param {object} candidate
+ * The RTCIceCandidate to be added or stored
+ */
+ storeOrAddIceCandidate(candidate) {
+ this._remote_ice_candidates.push(candidate);
+ if (this.signalingState === "closed") {
+ info("Received ICE candidate for closed PeerConnection - discarding");
+ return;
+ }
+ this.holdIceCandidates
+ .then(() => {
+ info(this + ": adding ICE candidate " + JSON.stringify(candidate));
+ return this._pc.addIceCandidate(candidate);
+ })
+ .then(() => ok(true, this + " successfully added an ICE candidate"))
+ .catch(e =>
+ // The onicecandidate callback runs independent of the test steps
+ // and therefore errors thrown from in there don't get caught by the
+ // race of the Promises around our test steps.
+ // Note: as long as we are queuing ICE candidates until the success
+ // of sRD() this should never ever happen.
+ ok(false, this + " adding ICE candidate failed with: " + e.message)
+ );
+ },
+
+ /**
+ * Registers a callback for the ICE connection state change and
+ * appends the new state to an array for logging it later.
+ */
+ logIceConnectionState() {
+ this.iceConnectionLog = [this._pc.iceConnectionState];
+ this.ice_connection_callbacks.logIceStatus = () => {
+ var newstate = this._pc.iceConnectionState;
+ var oldstate = this.iceConnectionLog[this.iceConnectionLog.length - 1];
+ if (Object.keys(iceStateTransitions).includes(oldstate)) {
+ if (this.iceCheckingRestartExpected) {
+ is(
+ newstate,
+ "checking",
+ "iceconnectionstate event '" +
+ newstate +
+ "' matches expected state 'checking'"
+ );
+ this.iceCheckingRestartExpected = false;
+ } else if (this.iceCheckingIceRollbackExpected) {
+ is(
+ newstate,
+ "connected",
+ "iceconnectionstate event '" +
+ newstate +
+ "' matches expected state 'connected'"
+ );
+ this.iceCheckingIceRollbackExpected = false;
+ } else {
+ ok(
+ iceStateTransitions[oldstate].includes(newstate),
+ this +
+ ": legal ICE state transition from " +
+ oldstate +
+ " to " +
+ newstate
+ );
+ }
+ } else {
+ ok(
+ false,
+ this +
+ ": old ICE state " +
+ oldstate +
+ " missing in ICE transition array"
+ );
+ }
+ this.iceConnectionLog.push(newstate);
+ };
+ },
+
+ /**
+ * Resets the ICE connected Promise and allows ICE connection state monitoring
+ * to go backwards to 'checking'.
+ */
+ expectIceChecking() {
+ this.iceCheckingRestartExpected = true;
+ this.iceConnected = new Promise((resolve, reject) => {
+ this.iceConnectedResolve = resolve;
+ this.iceConnectedReject = reject;
+ });
+ },
+
+ /**
+ * Waits for ICE to either connect or fail.
+ *
+ * @returns {Promise}
+ * resolves when connected, rejects on failure
+ */
+ waitForIceConnected() {
+ return this.iceConnected;
+ },
+
+ /**
+ * Setup a onicecandidate handler
+ *
+ * @param {object} test
+ * A PeerConnectionTest object to which the ice candidates gets
+ * forwarded.
+ */
+ setupIceCandidateHandler(test, candidateHandler) {
+ candidateHandler = candidateHandler || test.iceCandidateHandler.bind(test);
+
+ var resolveEndOfTrickle;
+ this.endOfTrickleIce = new Promise(r => (resolveEndOfTrickle = r));
+ this.holdIceCandidates = new Promise(r => (this.releaseIceCandidates = r));
+
+ this._pc.onicecandidate = anEvent => {
+ if (!anEvent.candidate) {
+ this._pc.onicecandidate = () =>
+ ok(
+ false,
+ this.label + " received ICE candidate after end of trickle"
+ );
+ info(this.label + ": received end of trickle ICE event");
+ ok(
+ this._pc.iceGatheringState === "complete",
+ "ICE gathering state has reached complete"
+ );
+ resolveEndOfTrickle(this.label);
+ return;
+ }
+
+ info(
+ this.label + ": iceCandidate = " + JSON.stringify(anEvent.candidate)
+ );
+ ok(anEvent.candidate.sdpMid.length, "SDP mid not empty");
+ ok(
+ anEvent.candidate.usernameFragment.length,
+ "usernameFragment not empty"
+ );
+
+ ok(
+ typeof anEvent.candidate.sdpMLineIndex === "number",
+ "SDP MLine Index needs to exist"
+ );
+ this._local_ice_candidates.push(anEvent.candidate);
+ candidateHandler(this.label, anEvent.candidate);
+ };
+ },
+
+ checkLocalMediaTracks() {
+ info(
+ `${this}: Checking local tracks ${JSON.stringify(
+ this.expectedLocalTrackInfo
+ )}`
+ );
+ const sendersWithTrack = this._pc.getSenders().filter(({ track }) => track);
+ is(
+ sendersWithTrack.length,
+ this.expectedLocalTrackInfo.length,
+ "The number of senders with a track should be equal to the number of " +
+ "expected local tracks."
+ );
+
+ // expectedLocalTrackInfo is in the same order that the tracks were added, and
+ // so should the output of getSenders.
+ this.expectedLocalTrackInfo.forEach((info, i) => {
+ const sender = sendersWithTrack[i];
+ is(sender, info.sender, `Sender ${i} should match`);
+ is(sender.track, info.track, `Track ${i} should match`);
+ });
+ },
+
+ /**
+ * Checks that we are getting the media tracks we expect.
+ */
+ checkMediaTracks() {
+ this.checkLocalMediaTracks();
+ },
+
+ checkLocalMsids() {
+ const sdp = this.localDescription.sdp;
+ const msections = sdputils.getMSections(sdp);
+ const expectedStreamIdCounts = new Map();
+ for (const { track, sender, streamId } of this.expectedLocalTrackInfo) {
+ const transceiver = this._pc
+ .getTransceivers()
+ .find(t => t.sender == sender);
+ ok(transceiver, "There should be a transceiver for each sender");
+ if (transceiver.mid) {
+ const midFinder = new RegExp(`^a=mid:${transceiver.mid}$`, "m");
+ const msection = msections.find(m => m.match(midFinder));
+ ok(
+ msection,
+ `There should be a media section for mid = ${transceiver.mid}`
+ );
+ ok(
+ msection.startsWith(`m=${track.kind}`),
+ `Media section should be of type ${track.kind}`
+ );
+ const msidFinder = new RegExp(`^a=msid:${streamId} \\S+$`, "m");
+ ok(
+ msection.match(msidFinder),
+ `Should find a=msid:${streamId} in media section` +
+ " (with any track id for now)"
+ );
+ const count = expectedStreamIdCounts.get(streamId) || 0;
+ expectedStreamIdCounts.set(streamId, count + 1);
+ }
+ }
+
+ // Check for any unexpected msids.
+ const allMsids = sdp.match(new RegExp("^a=msid:\\S+", "mg"));
+ if (!allMsids) {
+ return;
+ }
+ const allStreamIds = allMsids.map(msidAttr =>
+ msidAttr.replace("a=msid:", "")
+ );
+ allStreamIds.forEach(id => {
+ const count = expectedStreamIdCounts.get(id);
+ ok(count, `Unexpected stream id ${id} found in local description.`);
+ if (count) {
+ expectedStreamIdCounts.set(id, count - 1);
+ }
+ });
+ },
+
+ /**
+ * Check that media flow is present for the given media element by checking
+ * that it reaches ready state HAVE_ENOUGH_DATA and progresses time further
+ * than the start of the check.
+ *
+ * This ensures, that the stream being played is producing
+ * data and, in case it contains a video track, that at least one video frame
+ * has been displayed.
+ *
+ * @param {HTMLMediaElement} track
+ * The media element to check
+ * @returns {Promise}
+ * A promise that resolves when media data is flowing.
+ */
+ waitForMediaElementFlow(element) {
+ info("Checking data flow for element: " + element.id);
+ is(
+ element.ended,
+ !element.srcObject.active,
+ "Element ended should be the inverse of the MediaStream's active state"
+ );
+ if (element.ended) {
+ is(
+ element.readyState,
+ element.HAVE_CURRENT_DATA,
+ "Element " + element.id + " is ended and should have had data"
+ );
+ return Promise.resolve();
+ }
+
+ const haveEnoughData = (
+ element.readyState == element.HAVE_ENOUGH_DATA
+ ? Promise.resolve()
+ : haveEvent(
+ element,
+ "canplay",
+ wait(60000, new Error("Timeout for element " + element.id))
+ )
+ ).then(_ => info("Element " + element.id + " has enough data."));
+
+ const startTime = element.currentTime;
+ const timeProgressed = timeout(
+ listenUntil(element, "timeupdate", _ => element.currentTime > startTime),
+ 60000,
+ "Element " + element.id + " should progress currentTime"
+ ).then();
+
+ return Promise.all([haveEnoughData, timeProgressed]);
+ },
+
+ /**
+ * Wait for RTP packet flow for the given MediaStreamTrack.
+ *
+ * @param {object} track
+ * A MediaStreamTrack to wait for data flow on.
+ * @returns {Promise}
+ * Returns a promise which yields a StatsReport object with RTP stats.
+ */
+ async _waitForRtpFlow(target, rtpType) {
+ const { track } = target;
+ info(`_waitForRtpFlow(${track.id}, ${rtpType})`);
+ const packets = `packets${rtpType == "outbound-rtp" ? "Sent" : "Received"}`;
+
+ const retryInterval = 500; // Time between stats checks
+ const timeout = 30000; // Timeout in ms
+ const retries = timeout / retryInterval;
+
+ for (let i = 0; i < retries; i++) {
+ info(`Checking ${rtpType} for ${track.kind} track ${track.id} try ${i}`);
+ for (const rtp of (await target.getStats()).values()) {
+ if (rtp.type != rtpType) {
+ continue;
+ }
+ if (rtp.kind != track.kind) {
+ continue;
+ }
+
+ const numPackets = rtp[packets];
+ info(`Track ${track.id} has ${numPackets} ${packets}.`);
+ if (!numPackets) {
+ continue;
+ }
+
+ ok(true, `RTP flowing for ${track.kind} track ${track.id}`);
+ return;
+ }
+ await wait(retryInterval);
+ }
+ throw new Error(
+ `Checking stats for track ${track.id} timed out after ${timeout} ms`
+ );
+ },
+
+ /**
+ * Wait for inbound RTP packet flow for the given MediaStreamTrack.
+ *
+ * @param {object} receiver
+ * An RTCRtpReceiver to wait for data flow on.
+ * @returns {Promise}
+ * Returns a promise that resolves once data is flowing.
+ */
+ async waitForInboundRtpFlow(receiver) {
+ return this._waitForRtpFlow(receiver, "inbound-rtp");
+ },
+
+ /**
+ * Wait for outbound RTP packet flow for the given MediaStreamTrack.
+ *
+ * @param {object} sender
+ * An RTCRtpSender to wait for data flow on.
+ * @returns {Promise}
+ * Returns a promise that resolves once data is flowing.
+ */
+ async waitForOutboundRtpFlow(sender) {
+ return this._waitForRtpFlow(sender, "outbound-rtp");
+ },
+
+ getExpectedActiveReceivers() {
+ return this._pc
+ .getTransceivers()
+ .filter(
+ t =>
+ !t.stopped &&
+ t.currentDirection &&
+ t.currentDirection != "inactive" &&
+ t.currentDirection != "sendonly"
+ )
+ .filter(({ receiver }) => receiver.track)
+ .map(({ mid, currentDirection, receiver }) => {
+ info(
+ `Found transceiver that should be receiving RTP: mid=${mid}` +
+ ` currentDirection=${currentDirection}` +
+ ` kind=${receiver.track.kind} track-id=${receiver.track.id}`
+ );
+ return receiver;
+ });
+ },
+
+ getExpectedSenders() {
+ return this._pc.getSenders().filter(({ track }) => track);
+ },
+
+ /**
+ * Wait for presence of video flow on all media elements and rtp flow on
+ * all sending and receiving track involved in this test.
+ *
+ * @returns {Promise}
+ * A promise that resolves when media flows for all elements and tracks
+ */
+ waitForMediaFlow() {
+ const receivers = this.getExpectedActiveReceivers();
+ return Promise.all([
+ ...this.localMediaElements.map(el => this.waitForMediaElementFlow(el)),
+ ...this.remoteMediaElements
+ .filter(({ srcObject }) =>
+ receivers.some(({ track }) =>
+ srcObject.getTracks().some(t => t == track)
+ )
+ )
+ .map(el => this.waitForMediaElementFlow(el)),
+ ...receivers.map(receiver => this.waitForInboundRtpFlow(receiver)),
+ ...this.getExpectedSenders().map(sender =>
+ this.waitForOutboundRtpFlow(sender)
+ ),
+ ]);
+ },
+
+ /**
+ * Check that correct audio (typically a flat tone) is flowing to this
+ * PeerConnection for each transceiver that should be receiving. Uses
+ * WebAudio AnalyserNodes to compare input and output audio data in the
+ * frequency domain.
+ *
+ * @param {object} from
+ * A PeerConnectionWrapper whose audio RTPSender we use as source for
+ * the audio flow check.
+ * @returns {Promise}
+ * A promise that resolves when we're receiving the tone/s from |from|.
+ */
+ async checkReceivingToneFrom(
+ audiocontext,
+ from,
+ cancel = wait(60000, new Error("Tone not detected"))
+ ) {
+ let localTransceivers = this._pc
+ .getTransceivers()
+ .filter(t => t.mid)
+ .filter(t => t.receiver.track.kind == "audio")
+ .sort((t1, t2) => t1.mid < t2.mid);
+ let remoteTransceivers = from._pc
+ .getTransceivers()
+ .filter(t => t.mid)
+ .filter(t => t.receiver.track.kind == "audio")
+ .sort((t1, t2) => t1.mid < t2.mid);
+
+ is(
+ localTransceivers.length,
+ remoteTransceivers.length,
+ "Same number of associated audio transceivers on remote and local."
+ );
+
+ for (let i = 0; i < localTransceivers.length; i++) {
+ is(
+ localTransceivers[i].mid,
+ remoteTransceivers[i].mid,
+ "Transceivers at index " + i + " have the same mid."
+ );
+
+ if (!remoteTransceivers[i].sender.track) {
+ continue;
+ }
+
+ if (
+ remoteTransceivers[i].currentDirection == "recvonly" ||
+ remoteTransceivers[i].currentDirection == "inactive"
+ ) {
+ continue;
+ }
+
+ let sendTrack = remoteTransceivers[i].sender.track;
+ let inputElem = from.getMediaElementForTrack(sendTrack, "local");
+ ok(
+ inputElem,
+ "Remote wrapper should have a media element for track id " +
+ sendTrack.id
+ );
+ let inputAudioStream = from.getStreamForSendTrack(sendTrack);
+ ok(
+ inputAudioStream,
+ "Remote wrapper should have a stream for track id " + sendTrack.id
+ );
+ let inputAnalyser = new AudioStreamAnalyser(
+ audiocontext,
+ inputAudioStream
+ );
+
+ let recvTrack = localTransceivers[i].receiver.track;
+ let outputAudioStream = this.getStreamForRecvTrack(recvTrack);
+ ok(
+ outputAudioStream,
+ "Local wrapper should have a stream for track id " + recvTrack.id
+ );
+ let outputAnalyser = new AudioStreamAnalyser(
+ audiocontext,
+ outputAudioStream
+ );
+
+ let error = null;
+ cancel.then(e => (error = e));
+
+ let indexOfMax = data =>
+ data.reduce((max, val, i) => (val >= data[max] ? i : max), 0);
+
+ await outputAnalyser.waitForAnalysisSuccess(() => {
+ if (error) {
+ throw error;
+ }
+
+ let inputData = inputAnalyser.getByteFrequencyData();
+ let outputData = outputAnalyser.getByteFrequencyData();
+
+ let inputMax = indexOfMax(inputData);
+ let outputMax = indexOfMax(outputData);
+ info(
+ `Comparing maxima; input[${inputMax}] = ${inputData[inputMax]},` +
+ ` output[${outputMax}] = ${outputData[outputMax]}`
+ );
+ if (!inputData[inputMax] || !outputData[outputMax]) {
+ return false;
+ }
+
+ // When the input and output maxima are within reasonable distance (2% of
+ // total length, which means ~10 for length 512) from each other, we can
+ // be sure that the input tone has made it through the peer connection.
+ info(`input data length: ${inputData.length}`);
+ return Math.abs(inputMax - outputMax) < inputData.length * 0.02;
+ });
+ }
+ },
+
+ /**
+ * Check that stats are present by checking for known stats.
+ */
+ async getStats(selector) {
+ const stats = await this._pc.getStats(selector);
+ const dict = {};
+ for (const [k, v] of stats.entries()) {
+ dict[k] = v;
+ }
+ info(`${this}: Got stats: ${JSON.stringify(dict)}`);
+ return stats;
+ },
+
+ /**
+ * Checks that we are getting the media streams we expect.
+ *
+ * @param {object} stats
+ * The stats to check from this PeerConnectionWrapper
+ */
+ checkStats(stats) {
+ const isRemote = ({ type }) =>
+ ["remote-outbound-rtp", "remote-inbound-rtp"].includes(type);
+ var counters = {};
+ for (let [key, res] of stats) {
+ info("Checking stats for " + key + " : " + res);
+ // validate stats
+ ok(res.id == key, "Coherent stats id");
+ const now = performance.timeOrigin + performance.now();
+ const minimum = performance.timeOrigin;
+ const type = isRemote(res) ? "rtcp" : "rtp";
+ ok(
+ res.timestamp >= minimum,
+ `Valid ${type} timestamp ${res.timestamp} >= ${minimum} (
+ ${res.timestamp - minimum} ms)`
+ );
+ ok(
+ res.timestamp <= now,
+ `Valid ${type} timestamp ${res.timestamp} <= ${now} (
+ ${res.timestamp - now} ms)`
+ );
+ if (isRemote(res)) {
+ continue;
+ }
+ counters[res.type] = (counters[res.type] || 0) + 1;
+
+ switch (res.type) {
+ case "inbound-rtp":
+ case "outbound-rtp":
+ {
+ // Inbound tracks won't have an ssrc if RTP is not flowing.
+ // (eg; negotiated inactive)
+ ok(
+ res.ssrc || res.type == "inbound-rtp",
+ "Outbound RTP stats has an ssrc."
+ );
+
+ if (res.ssrc) {
+ // ssrc is a 32 bit number returned as an unsigned long
+ ok(!/[^0-9]/.test(`${res.ssrc}`), "SSRC is numeric");
+ ok(parseInt(res.ssrc) < Math.pow(2, 32), "SSRC is within limits");
+ }
+
+ if (res.type == "outbound-rtp") {
+ ok(res.packetsSent !== undefined, "Rtp packetsSent");
+ // We assume minimum payload to be 1 byte (guess from RFC 3550)
+ ok(res.bytesSent >= res.packetsSent, "Rtp bytesSent");
+ } else {
+ ok(res.packetsReceived !== undefined, "Rtp packetsReceived");
+ ok(res.bytesReceived >= res.packetsReceived, "Rtp bytesReceived");
+ }
+ if (res.remoteId) {
+ var rem = stats.get(res.remoteId);
+ ok(isRemote(rem), "Remote is rtcp");
+ ok(rem.localId == res.id, "Remote backlink match");
+ if (res.type == "outbound-rtp") {
+ ok(rem.type == "remote-inbound-rtp", "Rtcp is inbound");
+ if (rem.packetsLost) {
+ ok(
+ rem.packetsLost >= 0,
+ "Rtcp packetsLost " + rem.packetsLost + " >= 0"
+ );
+ ok(
+ rem.packetsLost < 1000,
+ "Rtcp packetsLost " + rem.packetsLost + " < 1000"
+ );
+ }
+ if (!this.disableRtpCountChecking) {
+ // no guarantee which one is newer!
+ // Note: this must change when we add a timestamp field to remote RTCP reports
+ // and make rem.timestamp be the reception time
+ if (res.timestamp < rem.timestamp) {
+ info(
+ "REVERSED timestamps: rec:" +
+ rem.packetsReceived +
+ " time:" +
+ rem.timestamp +
+ " sent:" +
+ res.packetsSent +
+ " time:" +
+ res.timestamp
+ );
+ }
+ }
+ if (rem.jitter) {
+ ok(rem.jitter >= 0, "Rtcp jitter " + rem.jitter + " >= 0");
+ ok(rem.jitter < 5, "Rtcp jitter " + rem.jitter + " < 5 sec");
+ }
+ if (rem.roundTripTime) {
+ ok(
+ rem.roundTripTime >= 0,
+ "Rtcp rtt " + rem.roundTripTime + " >= 0"
+ );
+ ok(
+ rem.roundTripTime < 60,
+ "Rtcp rtt " + rem.roundTripTime + " < 1 min"
+ );
+ }
+ } else {
+ ok(rem.type == "remote-outbound-rtp", "Rtcp is outbound");
+ ok(rem.packetsSent !== undefined, "Rtcp packetsSent");
+ ok(rem.bytesSent !== undefined, "Rtcp bytesSent");
+ }
+ ok(rem.ssrc == res.ssrc, "Remote ssrc match");
+ } else {
+ info("No rtcp info received yet");
+ }
+ }
+ break;
+ }
+ }
+
+ var nin = this._pc.getTransceivers().filter(t => {
+ return (
+ !t.stopped &&
+ t.currentDirection != "inactive" &&
+ t.currentDirection != "sendonly"
+ );
+ }).length;
+ const nout = Object.keys(this.expectedLocalTrackInfo).length;
+ var ndata = this.dataChannels.length;
+
+ // TODO(Bug 957145): Restore stronger inbound-rtp test once Bug 948249 is fixed
+ //is((counters["inbound-rtp"] || 0), nin, "Have " + nin + " inbound-rtp stat(s)");
+ ok(
+ (counters["inbound-rtp"] || 0) >= nin,
+ "Have at least " + nin + " inbound-rtp stat(s) *"
+ );
+
+ is(
+ counters["outbound-rtp"] || 0,
+ nout,
+ "Have " + nout + " outbound-rtp stat(s)"
+ );
+
+ var numLocalCandidates = counters["local-candidate"] || 0;
+ var numRemoteCandidates = counters["remote-candidate"] || 0;
+ // If there are no tracks, there will be no stats either.
+ if (nin + nout + ndata > 0) {
+ ok(numLocalCandidates, "Have local-candidate stat(s)");
+ ok(numRemoteCandidates, "Have remote-candidate stat(s)");
+ } else {
+ is(numLocalCandidates, 0, "Have no local-candidate stats");
+ is(numRemoteCandidates, 0, "Have no remote-candidate stats");
+ }
+ },
+
+ /**
+ * Compares the Ice server configured for this PeerConnectionWrapper
+ * with the ICE candidates received in the RTCP stats.
+ *
+ * @param {object} stats
+ * The stats to be verified for relayed vs. direct connection.
+ */
+ checkStatsIceConnectionType(stats, expectedLocalCandidateType) {
+ let lId;
+ let rId;
+ for (let stat of stats.values()) {
+ if (stat.type == "candidate-pair" && stat.selected) {
+ lId = stat.localCandidateId;
+ rId = stat.remoteCandidateId;
+ break;
+ }
+ }
+ isnot(
+ lId,
+ undefined,
+ "Got local candidate ID " + lId + " for selected pair"
+ );
+ isnot(
+ rId,
+ undefined,
+ "Got remote candidate ID " + rId + " for selected pair"
+ );
+ let lCand = stats.get(lId);
+ let rCand = stats.get(rId);
+ if (!lCand || !rCand) {
+ ok(
+ false,
+ "failed to find candidatepair IDs or stats for local: " +
+ lId +
+ " remote: " +
+ rId
+ );
+ return;
+ }
+
+ info(
+ "checkStatsIceConnectionType verifying: local=" +
+ JSON.stringify(lCand) +
+ " remote=" +
+ JSON.stringify(rCand)
+ );
+ expectedLocalCandidateType = expectedLocalCandidateType || "host";
+ var candidateType = lCand.candidateType;
+ if (lCand.relayProtocol === "tcp" && candidateType === "relay") {
+ candidateType = "relay-tcp";
+ }
+
+ if (lCand.relayProtocol === "tls" && candidateType === "relay") {
+ candidateType = "relay-tls";
+ }
+
+ if (expectedLocalCandidateType === "srflx" && candidateType === "prflx") {
+ // Be forgiving of prflx when expecting srflx, since that can happen due
+ // to timing.
+ candidateType = "srflx";
+ }
+
+ is(
+ candidateType,
+ expectedLocalCandidateType,
+ "Local candidate type is what we expected for selected pair"
+ );
+ },
+
+ /**
+ * Compares amount of established ICE connection according to ICE candidate
+ * pairs in the stats reporting with the expected amount of connection based
+ * on the constraints.
+ *
+ * @param {object} stats
+ * The stats to check for ICE candidate pairs
+ * @param {object} testOptions
+ * The test options object from the PeerConnectionTest
+ */
+ checkStatsIceConnections(stats, testOptions) {
+ var numIceConnections = 0;
+ stats.forEach(stat => {
+ if (stat.type === "candidate-pair" && stat.selected) {
+ numIceConnections += 1;
+ }
+ });
+ info("ICE connections according to stats: " + numIceConnections);
+ isnot(
+ numIceConnections,
+ 0,
+ "Number of ICE connections according to stats is not zero"
+ );
+ if (testOptions.bundle) {
+ if (testOptions.rtcpmux) {
+ is(numIceConnections, 1, "stats reports exactly 1 ICE connection");
+ } else {
+ is(
+ numIceConnections,
+ 2,
+ "stats report exactly 2 ICE connections for media and RTCP"
+ );
+ }
+ } else {
+ var numAudioTransceivers = this._pc
+ .getTransceivers()
+ .filter(transceiver => {
+ return (
+ !transceiver.stopped && transceiver.receiver.track.kind == "audio"
+ );
+ }).length;
+
+ var numVideoTransceivers = this._pc
+ .getTransceivers()
+ .filter(transceiver => {
+ return (
+ !transceiver.stopped && transceiver.receiver.track.kind == "video"
+ );
+ }).length;
+
+ var numExpectedTransports = numAudioTransceivers + numVideoTransceivers;
+ if (!testOptions.rtcpmux) {
+ numExpectedTransports *= 2;
+ }
+
+ if (this.dataChannels.length) {
+ ++numExpectedTransports;
+ }
+
+ info(
+ "expected audio + video + data transports: " + numExpectedTransports
+ );
+ is(
+ numIceConnections,
+ numExpectedTransports,
+ "stats ICE connections matches expected A/V transports"
+ );
+ }
+ },
+
+ expectNegotiationNeeded() {
+ if (!this.observedNegotiationNeeded) {
+ this.observedNegotiationNeeded = new Promise(resolve => {
+ this.onnegotiationneeded = resolve;
+ });
+ }
+ },
+
+ /**
+ * Property-matching function for finding a certain stat in passed-in stats
+ *
+ * @param {object} stats
+ * The stats to check from this PeerConnectionWrapper
+ * @param {object} props
+ * The properties to look for
+ * @returns {boolean} Whether an entry containing all match-props was found.
+ */
+ hasStat(stats, props) {
+ for (let res of stats.values()) {
+ var match = true;
+ for (let prop in props) {
+ if (res[prop] !== props[prop]) {
+ match = false;
+ break;
+ }
+ }
+ if (match) {
+ return true;
+ }
+ }
+ return false;
+ },
+
+ /**
+ * Closes the connection
+ */
+ close() {
+ this._pc.close();
+ this.localMediaElements.forEach(e => e.pause());
+ info(this + ": Closed connection.");
+ },
+
+ /**
+ * Returns the string representation of the class
+ *
+ * @returns {String} The string representation
+ */
+ toString() {
+ return "PeerConnectionWrapper (" + this.label + ")";
+ },
+};
+
+// haxx to prevent SimpleTest from failing at window.onload
+function addLoadEvent() {}
+
+function loadScript(...scripts) {
+ return Promise.all(
+ scripts.map(script => {
+ var el = document.createElement("script");
+ if (typeof scriptRelativePath === "string" && script.charAt(0) !== "/") {
+ script = scriptRelativePath + script;
+ }
+ el.src = script;
+ document.head.appendChild(el);
+ return new Promise(r => {
+ el.onload = r;
+ el.onerror = r;
+ });
+ })
+ );
+}
+
+// Ensure SimpleTest.js is loaded before other scripts.
+/* import-globals-from /testing/mochitest/tests/SimpleTest/SimpleTest.js */
+/* import-globals-from head.js */
+/* import-globals-from templates.js */
+/* import-globals-from turnConfig.js */
+/* import-globals-from dataChannel.js */
+/* import-globals-from network.js */
+/* import-globals-from sdpUtils.js */
+
+var scriptsReady = loadScript("/tests/SimpleTest/SimpleTest.js").then(() => {
+ return loadScript(
+ "head.js",
+ "templates.js",
+ "turnConfig.js",
+ "dataChannel.js",
+ "network.js",
+ "sdpUtils.js"
+ );
+});
+
+function createHTML(options) {
+ return scriptsReady.then(() => realCreateHTML(options));
+}
+
+var iceServerWebsocket;
+var iceServersArray = [];
+
+var addTurnsSelfsignedCerts = () => {
+ var gUrl = SimpleTest.getTestFileURL("addTurnsSelfsignedCert.js");
+ var gScript = SpecialPowers.loadChromeScript(gUrl);
+ var certs = [];
+ // If the ICE server is running TURNS, and includes a "cert" attribute in
+ // its JSON, we set up an override that will forgive things like
+ // self-signed for it.
+ iceServersArray.forEach(iceServer => {
+ if (iceServer.hasOwnProperty("cert")) {
+ iceServer.urls.forEach(url => {
+ if (url.startsWith("turns:")) {
+ // Assumes no port or params!
+ certs.push({ cert: iceServer.cert, hostname: url.substr(6) });
+ }
+ });
+ }
+ });
+
+ return new Promise((resolve, reject) => {
+ gScript.addMessageListener("certs-added", () => {
+ resolve();
+ });
+
+ gScript.sendAsyncMessage("add-turns-certs", certs);
+ });
+};
+
+var setupIceServerConfig = useIceServer => {
+ // We disable ICE support for HTTP proxy when using a TURN server, because
+ // mochitest uses a fake HTTP proxy to serve content, which will eat our STUN
+ // packets for TURN TCP.
+ var enableHttpProxy = enable =>
+ SpecialPowers.pushPrefEnv({
+ set: [["media.peerconnection.disable_http_proxy", !enable]],
+ });
+
+ var spawnIceServer = () =>
+ new Promise((resolve, reject) => {
+ iceServerWebsocket = new WebSocket("ws://localhost:8191/");
+ iceServerWebsocket.onopen = event => {
+ info("websocket/process bridge open, starting ICE Server...");
+ iceServerWebsocket.send("iceserver");
+ };
+
+ iceServerWebsocket.onmessage = event => {
+ // The first message will contain the iceServers configuration, subsequent
+ // messages are just logging.
+ info("ICE Server: " + event.data);
+ resolve(event.data);
+ };
+
+ iceServerWebsocket.onerror = () => {
+ reject("ICE Server error: Is the ICE server websocket up?");
+ };
+
+ iceServerWebsocket.onclose = () => {
+ info("ICE Server websocket closed");
+ reject("ICE Server gone before getting configuration");
+ };
+ });
+
+ if (!useIceServer) {
+ info("Skipping ICE Server for this test");
+ return enableHttpProxy(true);
+ }
+
+ return enableHttpProxy(false)
+ .then(spawnIceServer)
+ .then(iceServersStr => {
+ iceServersArray = JSON.parse(iceServersStr);
+ })
+ .then(addTurnsSelfsignedCerts);
+};
+
+async function runNetworkTest(testFunction, fixtureOptions = {}) {
+ let { AppConstants } = SpecialPowers.ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+ );
+
+ await scriptsReady;
+ await runTestWhenReady(async options => {
+ await startNetworkAndTest();
+ await setupIceServerConfig(fixtureOptions.useIceServer);
+ await testFunction(options);
+ await networkTestFinished();
+ });
+}
diff --git a/dom/media/webrtc/tests/mochitests/peerconnection_audio_forced_sample_rate.js b/dom/media/webrtc/tests/mochitests/peerconnection_audio_forced_sample_rate.js
new file mode 100644
index 0000000000..d0c647be0d
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/peerconnection_audio_forced_sample_rate.js
@@ -0,0 +1,32 @@
+// This function takes a sample-rate, and tests that audio flows correctly when
+// the sampling-rate at which the MTG runs is not one of the sampling-rates that
+// the MediaPipeline can work with.
+// It is in a separate file because we have an MTG per document, and we want to
+// test multiple sample-rates, so we include it in multiple HTML mochitest
+// files.
+async function test_peerconnection_audio_forced_sample_rate(forcedSampleRate) {
+ await scriptsReady;
+ await pushPrefs(["media.cubeb.force_sample_rate", forcedSampleRate]);
+ await runNetworkTest(function (options) {
+ const test = new PeerConnectionTest(options);
+ const ac = new AudioContext();
+ test.setMediaConstraints([{ audio: true }], []);
+ test.chain.replace("PC_LOCAL_GUM", [
+ function PC_LOCAL_WEBAUDIO_SOURCE(test) {
+ const oscillator = ac.createOscillator();
+ oscillator.type = "sine";
+ oscillator.frequency.value = 700;
+ oscillator.start();
+ const dest = ac.createMediaStreamDestination();
+ oscillator.connect(dest);
+ test.pcLocal.attachLocalStream(dest.stream);
+ },
+ ]);
+ test.chain.append([
+ function CHECK_REMOTE_AUDIO_FLOW(test) {
+ return test.pcRemote.checkReceivingToneFrom(ac, test.pcLocal);
+ },
+ ]);
+ return test.run();
+ });
+}
diff --git a/dom/media/webrtc/tests/mochitests/sdpUtils.js b/dom/media/webrtc/tests/mochitests/sdpUtils.js
new file mode 100644
index 0000000000..51cae10dba
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/sdpUtils.js
@@ -0,0 +1,398 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var sdputils = {
+ // Finds the codec id / payload type given a codec format
+ // (e.g., "VP8", "VP9/90000"). `offset` tells us which one to use in case of
+ // multiple matches.
+ findCodecId(sdp, format, offset = 0) {
+ let regex = new RegExp("rtpmap:([0-9]+) " + format, "gi");
+ let match;
+ for (let i = 0; i <= offset; ++i) {
+ match = regex.exec(sdp);
+ if (!match) {
+ throw new Error(
+ "Couldn't find offset " +
+ i +
+ " of codec " +
+ format +
+ " while looking for offset " +
+ offset +
+ " in sdp:\n" +
+ sdp
+ );
+ }
+ }
+ // match[0] is the full matched string
+ // match[1] is the first parenthesis group
+ return match[1];
+ },
+
+ // Returns a list of all payload types, excluding rtx, in an sdp.
+ getPayloadTypes(sdp) {
+ const regex = /^a=rtpmap:([0-9]+) (?:(?!rtx).)*$/gim;
+ const pts = [];
+ for (const [line, pt] of sdp.matchAll(regex)) {
+ pts.push(pt);
+ }
+ return pts;
+ },
+
+ // Finds all the extmap ids in the given sdp. Note that this does NOT
+ // consider m-sections, so a more generic version would need to
+ // look at each m-section separately.
+ findExtmapIds(sdp) {
+ var sdpExtmapIds = [];
+ extmapRegEx = /^a=extmap:([0-9+])/gm;
+ // must call exec on the regex to get each match in the string
+ while ((searchResults = extmapRegEx.exec(sdp)) !== null) {
+ // returned array has the matched text as the first item,
+ // and then one item for each capturing parenthesis that
+ // matched containing the text that was captured.
+ sdpExtmapIds.push(searchResults[1]);
+ }
+ return sdpExtmapIds;
+ },
+
+ findExtmapIdsUrnsDirections(sdp) {
+ var sdpExtmap = [];
+ extmapRegEx = /^a=extmap:([0-9+])([A-Za-z/]*) ([A-Za-z0-9_:\-\/\.]+)/gm;
+ // must call exec on the regex to get each match in the string
+ while ((searchResults = extmapRegEx.exec(sdp)) !== null) {
+ // returned array has the matched text as the first item,
+ // and then one item for each capturing parenthesis that
+ // matched containing the text that was captured.
+ var idUrn = [];
+ idUrn.push(searchResults[1]);
+ idUrn.push(searchResults[3]);
+ idUrn.push(searchResults[2].slice(1));
+ sdpExtmap.push(idUrn);
+ }
+ return sdpExtmap;
+ },
+
+ verify_unique_extmap_ids(sdp) {
+ const sdpExtmapIds = sdputils.findExtmapIdsUrnsDirections(sdp);
+
+ return sdpExtmapIds.reduce(function (result, item, index) {
+ const [id, urn, dir] = item;
+ ok(
+ !(id in result) || (result[id][0] === urn && result[id][1] === dir),
+ "ID " + id + " is unique ID for " + urn + " and direction " + dir
+ );
+ result[id] = [urn, dir];
+ return result;
+ }, {});
+ },
+
+ getMSections(sdp) {
+ return sdp
+ .split(new RegExp("^m=", "gm"))
+ .slice(1)
+ .map(s => "m=" + s);
+ },
+
+ getAudioMSections(sdp) {
+ return this.getMSections(sdp).filter(section =>
+ section.startsWith("m=audio")
+ );
+ },
+
+ getVideoMSections(sdp) {
+ return this.getMSections(sdp).filter(section =>
+ section.startsWith("m=video")
+ );
+ },
+
+ checkSdpAfterEndOfTrickle(description, testOptions, label) {
+ info("EOC-SDP: " + JSON.stringify(description));
+
+ const checkForTransportAttributes = msection => {
+ info("Checking msection: " + msection);
+ ok(
+ msection.includes("a=end-of-candidates"),
+ label + ": SDP contains end-of-candidates"
+ );
+
+ if (!msection.startsWith("m=application")) {
+ if (testOptions.rtcpmux) {
+ ok(
+ msection.includes("a=rtcp-mux"),
+ label + ": SDP contains rtcp-mux"
+ );
+ } else {
+ ok(msection.includes("a=rtcp:"), label + ": SDP contains rtcp port");
+ }
+ }
+ };
+
+ const hasOwnTransport = msection => {
+ const port0Check = new RegExp(/^m=\S+ 0 /).exec(msection);
+ if (port0Check) {
+ return false;
+ }
+ const midMatch = new RegExp(/\r\na=mid:(\S+)/).exec(msection);
+ if (!midMatch) {
+ return true;
+ }
+ const mid = midMatch[1];
+ const bundleGroupMatch = new RegExp(
+ "\\r\\na=group:BUNDLE \\S.* " + mid + "\\s+"
+ ).exec(description.sdp);
+ return bundleGroupMatch == null;
+ };
+
+ const msectionsWithOwnTransports = this.getMSections(
+ description.sdp
+ ).filter(hasOwnTransport);
+
+ ok(
+ msectionsWithOwnTransports.length,
+ "SDP should contain at least one msection with a transport"
+ );
+ msectionsWithOwnTransports.forEach(checkForTransportAttributes);
+
+ if (testOptions.ssrc) {
+ ok(description.sdp.includes("a=ssrc"), label + ": SDP contains a=ssrc");
+ } else {
+ ok(
+ !description.sdp.includes("a=ssrc"),
+ label + ": SDP does not contain a=ssrc"
+ );
+ }
+ },
+
+ // Note, we don't bother removing the fmtp lines, which makes a good test
+ // for some SDP parsing issues.
+ removeCodec(sdp, codec) {
+ var updated_sdp = sdp.replace(
+ new RegExp("a=rtpmap:" + codec + ".*\\/90000\\r\\n", ""),
+ ""
+ );
+ updated_sdp = updated_sdp.replace(
+ new RegExp("(RTP\\/SAVPF.*)( " + codec + ")(.*\\r\\n)", ""),
+ "$1$3"
+ );
+ updated_sdp = updated_sdp.replace(
+ new RegExp("a=rtcp-fb:" + codec + " nack\\r\\n", ""),
+ ""
+ );
+ updated_sdp = updated_sdp.replace(
+ new RegExp("a=rtcp-fb:" + codec + " nack pli\\r\\n", ""),
+ ""
+ );
+ updated_sdp = updated_sdp.replace(
+ new RegExp("a=rtcp-fb:" + codec + " ccm fir\\r\\n", ""),
+ ""
+ );
+ return updated_sdp;
+ },
+
+ removeAllButPayloadType(sdp, pt) {
+ return sdp.replace(
+ new RegExp("m=(\\w+ \\w+) UDP/TLS/RTP/SAVPF .*" + pt + ".*\\r\\n", "gi"),
+ "m=$1 UDP/TLS/RTP/SAVPF " + pt + "\r\n"
+ );
+ },
+
+ removeRtpMapForPayloadType(sdp, pt) {
+ return sdp.replace(new RegExp("a=rtpmap:" + pt + ".*\\r\\n", "gi"), "");
+ },
+
+ removeRtcpMux(sdp) {
+ return sdp.replace(/a=rtcp-mux\r\n/g, "");
+ },
+
+ removeSSRCs(sdp) {
+ return sdp.replace(/a=ssrc.*\r\n/g, "");
+ },
+
+ removeBundle(sdp) {
+ return sdp.replace(/a=group:BUNDLE .*\r\n/g, "");
+ },
+
+ reduceAudioMLineToPcmuPcma(sdp) {
+ return sdp.replace(
+ /m=audio .*\r\n/g,
+ "m=audio 9 UDP/TLS/RTP/SAVPF 0 8\r\n"
+ );
+ },
+
+ setAllMsectionsInactive(sdp) {
+ return sdp
+ .replace(/\r\na=sendrecv/g, "\r\na=inactive")
+ .replace(/\r\na=sendonly/g, "\r\na=inactive")
+ .replace(/\r\na=recvonly/g, "\r\na=inactive");
+ },
+
+ removeAllRtpMaps(sdp) {
+ return sdp.replace(/a=rtpmap:.*\r\n/g, "");
+ },
+
+ reduceAudioMLineToDynamicPtAndOpus(sdp) {
+ return sdp.replace(
+ /m=audio .*\r\n/g,
+ "m=audio 9 UDP/TLS/RTP/SAVPF 101 109\r\n"
+ );
+ },
+
+ addTiasBps(sdp, bps) {
+ return sdp.replace(/c=IN (.*)\r\n/g, "c=IN $1\r\nb=TIAS:" + bps + "\r\n");
+ },
+
+ removeSimulcastProperties(sdp) {
+ return sdp
+ .replace(/a=simulcast:.*\r\n/g, "")
+ .replace(/a=rid:.*\r\n/g, "")
+ .replace(
+ /a=extmap:[^\s]* urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id.*\r\n/g,
+ ""
+ )
+ .replace(
+ /a=extmap:[^\s]* urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id.*\r\n/g,
+ ""
+ );
+ },
+
+ transferSimulcastProperties(offer_sdp, answer_sdp) {
+ if (!offer_sdp.includes("a=simulcast:")) {
+ return answer_sdp;
+ }
+ ok(
+ offer_sdp.includes("a=simulcast:send "),
+ "Offer contains simulcast attribute"
+ );
+ var o_simul = offer_sdp.match(/simulcast:send (.*)([\n$])*/i);
+ var new_answer_sdp = answer_sdp + "a=simulcast:recv " + o_simul[1] + "\r\n";
+ ok(offer_sdp.includes("a=rid:"), "Offer contains RID attribute");
+ var o_rids = offer_sdp.match(/a=rid:(.*)/gi);
+ o_rids.forEach(o_rid => {
+ new_answer_sdp = new_answer_sdp + o_rid.replace(/send/, "recv") + "\r\n";
+ });
+ var extmap_id = offer_sdp.match(
+ "a=extmap:([0-9+])/sendonly urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id"
+ );
+ ok(extmap_id != null, "Offer contains RID RTP header extension");
+ new_answer_sdp =
+ new_answer_sdp +
+ "a=extmap:" +
+ extmap_id[1] +
+ "/recvonly urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id\r\n";
+ var extmap_id = offer_sdp.match(
+ "a=extmap:([0-9+])/sendonly urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id"
+ );
+ if (extmap_id != null) {
+ new_answer_sdp =
+ new_answer_sdp +
+ "a=extmap:" +
+ extmap_id[1] +
+ "/recvonly urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id\r\n";
+ }
+
+ return new_answer_sdp;
+ },
+
+ verifySdp(
+ desc,
+ expectedType,
+ offerConstraintsList,
+ offerOptions,
+ testOptions
+ ) {
+ info("Examining this SessionDescription: " + JSON.stringify(desc));
+ info("offerConstraintsList: " + JSON.stringify(offerConstraintsList));
+ info("offerOptions: " + JSON.stringify(offerOptions));
+ ok(desc, "SessionDescription is not null");
+ is(desc.type, expectedType, "SessionDescription type is " + expectedType);
+ ok(desc.sdp.length > 10, "SessionDescription body length is plausible");
+ ok(desc.sdp.includes("a=ice-ufrag"), "ICE username is present in SDP");
+ ok(desc.sdp.includes("a=ice-pwd"), "ICE password is present in SDP");
+ ok(desc.sdp.includes("a=fingerprint"), "ICE fingerprint is present in SDP");
+ //TODO: update this for loopback support bug 1027350
+ ok(
+ !desc.sdp.includes(LOOPBACK_ADDR),
+ "loopback interface is absent from SDP"
+ );
+ var requiresTrickleIce = !desc.sdp.includes("a=candidate");
+ if (requiresTrickleIce) {
+ info("No ICE candidate in SDP -> requiring trickle ICE");
+ } else {
+ info("at least one ICE candidate is present in SDP");
+ }
+
+ //TODO: how can we check for absence/presence of m=application?
+
+ var audioTracks =
+ sdputils.countTracksInConstraint("audio", offerConstraintsList) ||
+ (offerOptions && offerOptions.offerToReceiveAudio ? 1 : 0);
+
+ info("expected audio tracks: " + audioTracks);
+ if (audioTracks == 0) {
+ ok(!desc.sdp.includes("m=audio"), "audio m-line is absent from SDP");
+ } else {
+ ok(desc.sdp.includes("m=audio"), "audio m-line is present in SDP");
+ is(
+ testOptions.opus,
+ desc.sdp.includes("a=rtpmap:109 opus/48000/2"),
+ "OPUS codec is present in SDP"
+ );
+ //TODO: ideally the rtcp-mux should be for the m=audio, and not just
+ // anywhere in the SDP (JS SDP parser bug 1045429)
+ is(
+ testOptions.rtcpmux,
+ desc.sdp.includes("a=rtcp-mux"),
+ "RTCP Mux is offered in SDP"
+ );
+ }
+
+ var videoTracks =
+ sdputils.countTracksInConstraint("video", offerConstraintsList) ||
+ (offerOptions && offerOptions.offerToReceiveVideo ? 1 : 0);
+
+ info("expected video tracks: " + videoTracks);
+ if (videoTracks == 0) {
+ ok(!desc.sdp.includes("m=video"), "video m-line is absent from SDP");
+ } else {
+ ok(desc.sdp.includes("m=video"), "video m-line is present in SDP");
+ if (testOptions.h264) {
+ ok(
+ desc.sdp.includes("a=rtpmap:126 H264/90000") ||
+ desc.sdp.includes("a=rtpmap:97 H264/90000"),
+ "H.264 codec is present in SDP"
+ );
+ } else {
+ ok(
+ desc.sdp.includes("a=rtpmap:120 VP8/90000") ||
+ desc.sdp.includes("a=rtpmap:121 VP9/90000"),
+ "VP8 or VP9 codec is present in SDP"
+ );
+ }
+ is(
+ testOptions.rtcpmux,
+ desc.sdp.includes("a=rtcp-mux"),
+ "RTCP Mux is offered in SDP"
+ );
+ is(
+ testOptions.ssrc,
+ desc.sdp.includes("a=ssrc"),
+ "a=ssrc signaled in SDP"
+ );
+ }
+
+ return requiresTrickleIce;
+ },
+
+ /**
+ * Counts the amount of audio tracks in a given media constraint.
+ *
+ * @param constraints
+ * The contraint to be examined.
+ */
+ countTracksInConstraint(type, constraints) {
+ if (!Array.isArray(constraints)) {
+ return 0;
+ }
+ return constraints.reduce((sum, c) => sum + (c[type] ? 1 : 0), 0);
+ },
+};
diff --git a/dom/media/webrtc/tests/mochitests/simulcast.js b/dom/media/webrtc/tests/mochitests/simulcast.js
new file mode 100644
index 0000000000..0af36478c4
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/simulcast.js
@@ -0,0 +1,232 @@
+"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.
+ */
+
+// Borrowed from wpt, with some dependencies removed.
+
+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 =
+ description.sdp.match(/a=sendrecv|a=sendonly|a=recvonly|a=inactive/) ||
+ "a=sendrecv";
+ 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 =
+ description.sdp.match(/a=sendrecv|a=sendonly|a=recvonly|a=inactive/) ||
+ "a=sendrecv";
+ 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);
+}
+
+// This would be useful for cases other than simulcast, but we do not use it
+// anywhere else right now, nor do we have a place for wpt-friendly helpers at
+// the moment.
+function createPlaybackElement(track) {
+ const elem = document.createElement(track.kind);
+ elem.autoplay = true;
+ elem.srcObject = new MediaStream([track]);
+ elem.id = track.id;
+ return elem;
+}
+
+async function getPlaybackWithLoadedMetadata(track) {
+ const elem = createPlaybackElement(track);
+ return new Promise(resolve => {
+ elem.addEventListener("loadedmetadata", () => {
+ resolve(elem);
+ });
+ });
+}
diff --git a/dom/media/webrtc/tests/mochitests/stats.js b/dom/media/webrtc/tests/mochitests/stats.js
new file mode 100644
index 0000000000..475f8eeca9
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/stats.js
@@ -0,0 +1,1596 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const statsExpectedByType = {
+ "inbound-rtp": {
+ expected: [
+ "trackIdentifier",
+ "id",
+ "timestamp",
+ "type",
+ "ssrc",
+ "mediaType",
+ "kind",
+ "codecId",
+ "packetsReceived",
+ "packetsLost",
+ "packetsDiscarded",
+ "bytesReceived",
+ "jitter",
+ "lastPacketReceivedTimestamp",
+ "headerBytesReceived",
+ // Always missing from libwebrtc stats
+ // "estimatedPlayoutTimestamp",
+ "jitterBufferDelay",
+ "jitterBufferEmittedCount",
+ ],
+ optional: ["remoteId", "nackCount", "qpSum"],
+ localVideoOnly: [
+ "firCount",
+ "pliCount",
+ "framesDecoded",
+ "framesDropped",
+ "discardedPackets",
+ "framesPerSecond",
+ "frameWidth",
+ "frameHeight",
+ "framesReceived",
+ "totalDecodeTime",
+ "totalInterFrameDelay",
+ "totalProcessingDelay",
+ "totalSquaredInterFrameDelay",
+ ],
+ localAudioOnly: [
+ "totalSamplesReceived",
+ // libwebrtc doesn't seem to do FEC for video
+ "fecPacketsReceived",
+ "fecPacketsDiscarded",
+ "concealedSamples",
+ "silentConcealedSamples",
+ "concealmentEvents",
+ "insertedSamplesForDeceleration",
+ "removedSamplesForAcceleration",
+ "audioLevel",
+ "totalAudioEnergy",
+ "totalSamplesDuration",
+ ],
+ unimplemented: [
+ "mediaTrackId",
+ "transportId",
+ "associateStatsId",
+ "sliCount",
+ "packetsRepaired",
+ "fractionLost",
+ "burstPacketsLost",
+ "burstLossCount",
+ "burstDiscardCount",
+ "gapDiscardRate",
+ "gapLossRate",
+ ],
+ deprecated: ["mozRtt", "isRemote"],
+ },
+ "outbound-rtp": {
+ expected: [
+ "id",
+ "timestamp",
+ "type",
+ "ssrc",
+ "mediaType",
+ "kind",
+ "codecId",
+ "packetsSent",
+ "bytesSent",
+ "remoteId",
+ "headerBytesSent",
+ "retransmittedPacketsSent",
+ "retransmittedBytesSent",
+ ],
+ optional: ["nackCount", "qpSum"],
+ localAudioOnly: [],
+ localVideoOnly: [
+ "framesEncoded",
+ "firCount",
+ "pliCount",
+ "frameWidth",
+ "frameHeight",
+ "framesSent",
+ "hugeFramesSent",
+ "totalEncodeTime",
+ "totalEncodedBytesTarget",
+ ],
+ unimplemented: ["mediaTrackId", "transportId", "sliCount", "targetBitrate"],
+ deprecated: ["isRemote"],
+ },
+ "remote-inbound-rtp": {
+ expected: [
+ "id",
+ "timestamp",
+ "type",
+ "ssrc",
+ "mediaType",
+ "kind",
+ "codecId",
+ "packetsLost",
+ "jitter",
+ "localId",
+ "totalRoundTripTime",
+ "fractionLost",
+ "roundTripTimeMeasurements",
+ ],
+ optional: ["roundTripTime", "nackCount", "packetsReceived"],
+ unimplemented: [
+ "mediaTrackId",
+ "transportId",
+ "packetsDiscarded",
+ "associateStatsId",
+ "sliCount",
+ "packetsRepaired",
+ "burstPacketsLost",
+ "burstLossCount",
+ "burstDiscardCount",
+ "gapDiscardRate",
+ "gapLossRate",
+ ],
+ deprecated: ["mozRtt", "isRemote"],
+ },
+ "remote-outbound-rtp": {
+ expected: [
+ "id",
+ "timestamp",
+ "type",
+ "ssrc",
+ "mediaType",
+ "kind",
+ "codecId",
+ "packetsSent",
+ "bytesSent",
+ "localId",
+ "remoteTimestamp",
+ ],
+ optional: ["nackCount"],
+ unimplemented: ["mediaTrackId", "transportId", "sliCount", "targetBitrate"],
+ deprecated: ["isRemote"],
+ },
+ "media-source": {
+ expected: ["id", "timestamp", "type", "trackIdentifier", "kind"],
+ unimplemented: [
+ "audioLevel",
+ "totalAudioEnergy",
+ "totalSamplesDuration",
+ "echoReturnLoss",
+ "echoReturnLossEnhancement",
+ "droppedSamplesDuration",
+ "droppedSamplesEvents",
+ "totalCaptureDelay",
+ "totalSamplesCaptured",
+ "width",
+ "height",
+ "frames",
+ "framesPerSecond",
+ ],
+ optional: [],
+ deprecated: [],
+ },
+ csrc: { skip: true },
+ codec: {
+ expected: [
+ "timestamp",
+ "type",
+ "id",
+ "payloadType",
+ "transportId",
+ "mimeType",
+ "clockRate",
+ "sdpFmtpLine",
+ ],
+ optional: ["codecType", "channels"],
+ unimplemented: [],
+ deprecated: [],
+ },
+ "peer-connection": { skip: true },
+ "data-channel": { skip: true },
+ track: { skip: true },
+ transport: { skip: true },
+ "candidate-pair": {
+ expected: [
+ "id",
+ "timestamp",
+ "type",
+ "transportId",
+ "localCandidateId",
+ "remoteCandidateId",
+ "state",
+ "priority",
+ "nominated",
+ "writable",
+ "readable",
+ "bytesSent",
+ "bytesReceived",
+ "lastPacketSentTimestamp",
+ "lastPacketReceivedTimestamp",
+ ],
+ optional: ["selected"],
+ unimplemented: [
+ "totalRoundTripTime",
+ "currentRoundTripTime",
+ "availableOutgoingBitrate",
+ "availableIncomingBitrate",
+ "requestsReceived",
+ "requestsSent",
+ "responsesReceived",
+ "responsesSent",
+ "retransmissionsReceived",
+ "retransmissionsSent",
+ "consentRequestsSent",
+ ],
+ deprecated: [],
+ },
+ "local-candidate": {
+ expected: [
+ "id",
+ "timestamp",
+ "type",
+ "address",
+ "protocol",
+ "port",
+ "candidateType",
+ "priority",
+ ],
+ optional: ["relayProtocol", "proxied"],
+ unimplemented: ["networkType", "url", "transportId"],
+ deprecated: [
+ "candidateId",
+ "portNumber",
+ "ipAddress",
+ "componentId",
+ "mozLocalTransport",
+ "transport",
+ ],
+ },
+ "remote-candidate": {
+ expected: [
+ "id",
+ "timestamp",
+ "type",
+ "address",
+ "protocol",
+ "port",
+ "candidateType",
+ "priority",
+ ],
+ optional: ["relayProtocol", "proxied"],
+ unimplemented: ["networkType", "url", "transportId"],
+ deprecated: [
+ "candidateId",
+ "portNumber",
+ "ipAddress",
+ "componentId",
+ "mozLocalTransport",
+ "transport",
+ ],
+ },
+ certificate: { skip: true },
+};
+
+["in", "out"].forEach(pre => {
+ let s = statsExpectedByType[pre + "bound-rtp"];
+ s.optional = [...s.optional, ...s.localVideoOnly, ...s.localAudioOnly];
+});
+
+//
+// Checks that the fields in a report conform to the expectations in
+// statExpectedByType
+//
+function checkExpectedFields(report) {
+ report.forEach(stat => {
+ let expectations = statsExpectedByType[stat.type];
+ ok(expectations, "Stats type " + stat.type + " was expected");
+ // If the type is not expected or if it is flagged for skipping continue to
+ // the next
+ if (!expectations || expectations.skip) {
+ return;
+ }
+ // Check that all required fields exist
+ expectations.expected.forEach(field => {
+ ok(
+ field in stat,
+ "Expected stat field " + stat.type + "." + field + " exists"
+ );
+ });
+ // Check that each field is either expected or optional
+ let allowed = [...expectations.expected, ...expectations.optional];
+ Object.keys(stat).forEach(field => {
+ ok(
+ allowed.includes(field),
+ "Stat field " +
+ stat.type +
+ "." +
+ field +
+ ` is allowed. ${JSON.stringify(stat)}`
+ );
+ });
+
+ //
+ // Ensure that unimplemented fields are not implemented
+ // note: if a field is implemented it should be moved to expected or
+ // optional.
+ //
+ expectations.unimplemented.forEach(field => {
+ ok(
+ !Object.keys(stat).includes(field),
+ "Unimplemented field " + stat.type + "." + field + " does not exist."
+ );
+ });
+
+ //
+ // Ensure that all deprecated fields are not present
+ //
+ expectations.deprecated.forEach(field => {
+ ok(
+ !Object.keys(stat).includes(field),
+ "Deprecated field " + stat.type + "." + field + " does not exist."
+ );
+ });
+ });
+}
+
+function pedanticChecks(report) {
+ // Check that report is only-maplike
+ [...report.keys()].forEach(key =>
+ is(
+ report[key],
+ undefined,
+ `Report is not dictionary like, it lacks a property for key ${key}`
+ )
+ );
+ report.forEach((statObj, mapKey) => {
+ info(`"${mapKey} = ${JSON.stringify(statObj, null, 2)}`);
+ });
+ // eslint-disable-next-line complexity
+ report.forEach((statObj, mapKey) => {
+ let tested = {};
+ // Record what fields get tested.
+ // To access a field foo without marking it as tested use stat.inner.foo
+ let stat = new Proxy(statObj, {
+ get(stat, key) {
+ if (key == "inner") {
+ return stat;
+ }
+ tested[key] = true;
+ return stat[key];
+ },
+ });
+
+ let expectations = statsExpectedByType[stat.type];
+
+ if (expectations.skip) {
+ return;
+ }
+
+ // All stats share the following attributes inherited from RTCStats
+ is(stat.id, mapKey, stat.type + ".id is the same as the report key.");
+
+ // timestamp
+ ok(stat.timestamp >= 0, stat.type + ".timestamp is not less than 0");
+ // If the timebase for the timestamp is not properly set the timestamp
+ // will appear relative to the year 1970; Bug 1495446
+ const date = new Date(stat.timestamp);
+ ok(
+ date.getFullYear() > 1970,
+ `${stat.type}.timestamp is relative to current time, date=${date}`
+ );
+ //
+ // RTCStreamStats attributes with common behavior
+ //
+ // inbound-rtp, outbound-rtp, remote-inbound-rtp, remote-outbound-rtp
+ // inherit from RTCStreamStats
+ if (
+ [
+ "inbound-rtp",
+ "outbound-rtp",
+ "remote-inbound-rtp",
+ "remote-outbound-rtp",
+ ].includes(stat.type)
+ ) {
+ const isRemote = stat.type.startsWith("remote-");
+ //
+ // Common RTCStreamStats fields
+ //
+
+ // SSRC
+ ok(stat.ssrc, stat.type + ".ssrc has a value");
+
+ // kind
+ ok(
+ ["audio", "video"].includes(stat.kind),
+ stat.type + ".kind is 'audio' or 'video'"
+ );
+
+ // mediaType, renamed to kind but remains for backward compability.
+ ok(
+ ["audio", "video"].includes(stat.mediaType),
+ stat.type + ".mediaType is 'audio' or 'video'"
+ );
+
+ ok(stat.kind == stat.mediaType, "kind equals legacy mediaType");
+
+ // codecId
+ ok(stat.codecId, `${stat.type}.codecId has a value`);
+ ok(report.has(stat.codecId), `codecId ${stat.codecId} exists in report`);
+ is(
+ report.get(stat.codecId).type,
+ "codec",
+ `codecId ${stat.codecId} in report is codec type`
+ );
+ is(
+ report.get(stat.codecId).mimeType.slice(0, 5),
+ stat.kind,
+ `codecId ${stat.codecId} in report is for a mimeType of the same ` +
+ `media type as the referencing rtp stream stat`
+ );
+
+ if (isRemote) {
+ // local id
+ if (stat.localId) {
+ ok(
+ report.has(stat.localId),
+ `localId ${stat.localId} exists in report.`
+ );
+ is(
+ report.get(stat.localId).ssrc,
+ stat.ssrc,
+ "remote ssrc and local ssrc match."
+ );
+ is(
+ report.get(stat.localId).remoteId,
+ stat.id,
+ "local object has remote object as it's own remote object."
+ );
+ }
+ } else {
+ // remote id
+ if (stat.remoteId) {
+ ok(
+ report.has(stat.remoteId),
+ `remoteId ${stat.remoteId} exists in report.`
+ );
+ is(
+ report.get(stat.remoteId).ssrc,
+ stat.ssrc,
+ "remote ssrc and local ssrc match."
+ );
+ is(
+ report.get(stat.remoteId).localId,
+ stat.id,
+ "remote object has local object as it's own local object."
+ );
+ }
+ }
+
+ // nackCount
+ if (stat.nackCount) {
+ ok(
+ stat.nackCount >= 0,
+ `${stat.type}.nackCount is sane (${stat.kind}).`
+ );
+ }
+
+ if (!isRemote && stat.inner.kind == "video") {
+ // firCount
+ ok(
+ stat.firCount >= 0 && stat.firCount < 100,
+ `${stat.type}.firCount is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.firCount}`
+ );
+
+ // pliCount
+ ok(
+ stat.pliCount >= 0 && stat.pliCount < 200,
+ `${stat.type}.pliCount is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.pliCount}`
+ );
+
+ // qpSum
+ if (stat.qpSum !== undefined) {
+ ok(
+ stat.qpSum > 0,
+ `${stat.type}.qpSum is at least 0 ` +
+ `${stat.kind} test. value=${stat.qpSum}`
+ );
+ }
+ } else {
+ is(
+ stat.qpSum,
+ undefined,
+ `${stat.type}.qpSum does not exist when stat.kind != video`
+ );
+ }
+ }
+
+ if (stat.type == "inbound-rtp") {
+ //
+ // Required fields
+ //
+
+ // trackIdentifier
+ is(typeof stat.trackIdentifier, "string");
+ isnot(stat.trackIdentifier, "");
+
+ // packetsReceived
+ ok(
+ stat.packetsReceived >= 0 && stat.packetsReceived < 10 ** 5,
+ `${stat.type}.packetsReceived is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.packetsReceived}`
+ );
+
+ // packetsDiscarded
+ ok(
+ stat.packetsDiscarded >= 0 && stat.packetsDiscarded < 100,
+ `${stat.type}.packetsDiscarded is sane number for a short test. ` +
+ `value=${stat.packetsDiscarded}`
+ );
+ // bytesReceived
+ ok(
+ stat.bytesReceived >= 0 && stat.bytesReceived < 10 ** 9, // Not a magic number, just a guess
+ `${stat.type}.bytesReceived is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.bytesReceived}`
+ );
+
+ // packetsLost
+ ok(
+ stat.packetsLost < 100,
+ `${stat.type}.packetsLost is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.packetsLost}`
+ );
+
+ // This should be much lower for audio, TODO: Bug 1330575
+ let expectedJitter = stat.kind == "video" ? 0.5 : 1;
+ // jitter
+ ok(
+ stat.jitter < expectedJitter,
+ `${stat.type}.jitter is sane number for a ${stat.kind} ` +
+ `local only test. value=${stat.jitter}`
+ );
+
+ // lastPacketReceivedTimestamp
+ ok(
+ stat.lastPacketReceivedTimestamp !== undefined,
+ `${stat.type}.lastPacketReceivedTimestamp has a value`
+ );
+
+ // headerBytesReceived
+ ok(
+ stat.headerBytesReceived >= 0 && stat.headerBytesReceived < 50000,
+ `${stat.type}.headerBytesReceived is sane for a short test. ` +
+ `value=${stat.headerBytesReceived}`
+ );
+
+ // Always missing from libwebrtc stats
+ // estimatedPlayoutTimestamp
+ // ok(
+ // stat.estimatedPlayoutTimestamp !== undefined,
+ // `${stat.type}.estimatedPlayoutTimestamp has a value`
+ // );
+
+ // jitterBufferEmittedCount
+ let expectedJitterBufferEmmitedCount = stat.kind == "video" ? 7 : 1000;
+ ok(
+ stat.jitterBufferEmittedCount > expectedJitterBufferEmmitedCount,
+ `${stat.type}.jitterBufferEmittedCount is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.jitterBufferEmittedCount}`
+ );
+
+ // jitterBufferDelay
+ let avgJitterBufferDelay =
+ stat.jitterBufferDelay / stat.jitterBufferEmittedCount;
+ ok(
+ avgJitterBufferDelay > 0.01 && avgJitterBufferDelay < 10,
+ `${stat.type}.jitterBufferDelay is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.jitterBufferDelay}/${stat.jitterBufferEmittedCount}=${avgJitterBufferDelay}`
+ );
+
+ //
+ // Optional fields
+ //
+
+ //
+ // Local audio only stats
+ //
+ if (stat.inner.kind != "audio") {
+ expectations.localAudioOnly.forEach(field => {
+ ok(
+ stat[field] === undefined,
+ `${stat.type} does not have field ${field}` +
+ ` when kind is not 'audio'`
+ );
+ });
+ } else {
+ expectations.localAudioOnly.forEach(field => {
+ ok(
+ stat.inner[field] !== undefined,
+ stat.type + " has field " + field + " when kind is video"
+ );
+ });
+ // totalSamplesReceived
+ ok(
+ stat.totalSamplesReceived > 1000,
+ `${stat.type}.totalSamplesReceived is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.totalSamplesReceived}`
+ );
+
+ // fecPacketsReceived
+ ok(
+ stat.fecPacketsReceived >= 0 && stat.fecPacketsReceived < 10 ** 5,
+ `${stat.type}.fecPacketsReceived is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.fecPacketsReceived}`
+ );
+
+ // fecPacketsDiscarded
+ ok(
+ stat.fecPacketsDiscarded >= 0 && stat.fecPacketsDiscarded < 100,
+ `${stat.type}.fecPacketsDiscarded is sane number for a short test. ` +
+ `value=${stat.fecPacketsDiscarded}`
+ );
+ // concealedSamples
+ ok(
+ stat.concealedSamples >= 0 &&
+ stat.concealedSamples <= stat.totalSamplesReceived,
+ `${stat.type}.concealedSamples is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.concealedSamples}`
+ );
+
+ // silentConcealedSamples
+ ok(
+ stat.silentConcealedSamples >= 0 &&
+ stat.silentConcealedSamples <= stat.concealedSamples,
+ `${stat.type}.silentConcealedSamples is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.silentConcealedSamples}`
+ );
+
+ // concealmentEvents
+ ok(
+ stat.concealmentEvents >= 0 &&
+ stat.concealmentEvents <= stat.packetsReceived,
+ `${stat.type}.concealmentEvents is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.concealmentEvents}`
+ );
+
+ // insertedSamplesForDeceleration
+ ok(
+ stat.insertedSamplesForDeceleration >= 0 &&
+ stat.insertedSamplesForDeceleration <= stat.totalSamplesReceived,
+ `${stat.type}.insertedSamplesForDeceleration is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.insertedSamplesForDeceleration}`
+ );
+
+ // removedSamplesForAcceleration
+ ok(
+ stat.removedSamplesForAcceleration >= 0 &&
+ stat.removedSamplesForAcceleration <= stat.totalSamplesReceived,
+ `${stat.type}.removedSamplesForAcceleration is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.removedSamplesForAcceleration}`
+ );
+
+ // audioLevel
+ ok(
+ stat.audioLevel >= 0 && stat.audioLevel <= 128,
+ `${stat.type}.bytesReceived is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.audioLevel}`
+ );
+
+ // totalAudioEnergy
+ ok(
+ stat.totalAudioEnergy >= 0 && stat.totalAudioEnergy <= 128,
+ `${stat.type}.totalAudioEnergy is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.totalAudioEnergy}`
+ );
+
+ // totalSamplesDuration
+ ok(
+ stat.totalSamplesDuration >= 0 && stat.totalSamplesDuration <= 300,
+ `${stat.type}.totalSamplesDuration is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.totalSamplesDuration}`
+ );
+ }
+
+ //
+ // Local video only stats
+ //
+ if (stat.inner.kind != "video") {
+ expectations.localVideoOnly.forEach(field => {
+ ok(
+ stat[field] === undefined,
+ `${stat.type} does not have field ${field}` +
+ ` when kind is not 'video'`
+ );
+ });
+ } else {
+ expectations.localVideoOnly.forEach(field => {
+ ok(
+ stat.inner[field] !== undefined,
+ stat.type + " has field " + field + " when kind is video"
+ );
+ });
+ // discardedPackets
+ ok(
+ stat.discardedPackets < 100,
+ `${stat.type}.discardedPackets is a sane number for a short test. ` +
+ `value=${stat.discardedPackets}`
+ );
+ // framesPerSecond
+ ok(
+ stat.framesPerSecond > 0 && stat.framesPerSecond < 70,
+ `${stat.type}.framesPerSecond is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.framesPerSecond}`
+ );
+
+ // framesDecoded
+ ok(
+ stat.framesDecoded > 0 && stat.framesDecoded < 1000000,
+ `${stat.type}.framesDecoded is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.framesDecoded}`
+ );
+
+ // framesDropped
+ ok(
+ stat.framesDropped >= 0 && stat.framesDropped < 100,
+ `${stat.type}.framesDropped is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.framesDropped}`
+ );
+
+ // frameWidth
+ ok(
+ stat.frameWidth > 0 && stat.frameWidth < 100000,
+ `${stat.type}.frameWidth is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.framesSent}`
+ );
+
+ // frameHeight
+ ok(
+ stat.frameHeight > 0 && stat.frameHeight < 100000,
+ `${stat.type}.frameHeight is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.frameHeight}`
+ );
+
+ // totalDecodeTime
+ ok(
+ stat.totalDecodeTime >= 0 && stat.totalDecodeTime < 300,
+ `${stat.type}.totalDecodeTime is sane for a short test. ` +
+ `value=${stat.totalDecodeTime}`
+ );
+
+ // totalProcessingDelay
+ ok(
+ stat.totalProcessingDelay < 100,
+ `${stat.type}.totalProcessingDelay is sane number for a short test ` +
+ `local only test. value=${stat.totalProcessingDelay}`
+ );
+
+ // totalInterFrameDelay
+ ok(
+ stat.totalInterFrameDelay >= 0 && stat.totalInterFrameDelay < 100,
+ `${stat.type}.totalInterFrameDelay is sane for a short test. ` +
+ `value=${stat.totalInterFrameDelay}`
+ );
+
+ // totalSquaredInterFrameDelay
+ ok(
+ stat.totalSquaredInterFrameDelay >= 0 &&
+ stat.totalSquaredInterFrameDelay < 100,
+ `${stat.type}.totalSquaredInterFrameDelay is sane for a short test. ` +
+ `value=${stat.totalSquaredInterFrameDelay}`
+ );
+
+ // framesReceived
+ ok(
+ stat.framesReceived >= 0 && stat.framesReceived < 100000,
+ `${stat.type}.framesReceived is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.framesReceived}`
+ );
+ }
+ } else if (stat.type == "remote-inbound-rtp") {
+ // roundTripTime
+ ok(
+ stat.roundTripTime >= 0,
+ `${stat.type}.roundTripTime is sane with` +
+ `value of: ${stat.roundTripTime} (${stat.kind})`
+ );
+ //
+ // Required fields
+ //
+
+ // packetsLost
+ ok(
+ stat.packetsLost < 100,
+ `${stat.type}.packetsLost is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.packetsLost}`
+ );
+
+ // jitter
+ ok(
+ stat.jitter >= 0,
+ `${stat.type}.jitter is sane number (${stat.kind}). ` +
+ `value=${stat.jitter}`
+ );
+
+ //
+ // Optional fields
+ //
+
+ // packetsReceived
+ if (stat.packetsReceived) {
+ ok(
+ stat.packetsReceived >= 0 && stat.packetsReceived < 10 ** 5,
+ `${stat.type}.packetsReceived is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.packetsReceived}`
+ );
+ }
+
+ // totalRoundTripTime
+ ok(
+ stat.totalRoundTripTime < 50000,
+ `${stat.type}.totalRoundTripTime is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.totalRoundTripTime}`
+ );
+
+ // fractionLost
+ ok(
+ stat.fractionLost < 0.2,
+ `${stat.type}.fractionLost is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.fractionLost}`
+ );
+
+ // roundTripTimeMeasurements
+ ok(
+ stat.roundTripTimeMeasurements >= 1 &&
+ stat.roundTripTimeMeasurements < 500,
+ `${stat.type}.roundTripTimeMeasurements is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.roundTripTimeMeasurements}`
+ );
+ } else if (stat.type == "outbound-rtp") {
+ //
+ // Required fields
+ //
+
+ // packetsSent
+ ok(
+ stat.packetsSent > 0 && stat.packetsSent < 10000,
+ `${stat.type}.packetsSent is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.packetsSent}`
+ );
+
+ // bytesSent
+ const audio1Min = 16000 * 60; // 128kbps
+ const video1Min = 250000 * 60; // 2Mbps
+ ok(
+ stat.bytesSent > 0 &&
+ stat.bytesSent < (stat.kind == "video" ? video1Min : audio1Min),
+ `${stat.type}.bytesSent is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.bytesSent}`
+ );
+
+ // headerBytesSent
+ ok(
+ stat.headerBytesSent > 0 &&
+ stat.headerBytesSent < (stat.kind == "video" ? video1Min : audio1Min),
+ `${stat.type}.headerBytesSent is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.headerBytesSent}`
+ );
+
+ // retransmittedPacketsSent
+ ok(
+ stat.retransmittedPacketsSent >= 0 &&
+ stat.retransmittedPacketsSent <
+ (stat.kind == "video" ? video1Min : audio1Min),
+ `${stat.type}.retransmittedPacketsSent is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.retransmittedPacketsSent}`
+ );
+
+ // retransmittedBytesSent
+ ok(
+ stat.retransmittedBytesSent >= 0 &&
+ stat.retransmittedBytesSent <
+ (stat.kind == "video" ? video1Min : audio1Min),
+ `${stat.type}.retransmittedBytesSent is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.retransmittedBytesSent}`
+ );
+
+ //
+ // Optional fields
+ //
+
+ // qpSum
+ // This is supported for all of our vpx codecs (on the encode side, see
+ // bug 1519590)
+ const mimeType = report.get(stat.codecId).mimeType;
+ if (mimeType.includes("VP")) {
+ ok(
+ stat.qpSum >= 0,
+ `${stat.type}.qpSum is a sane number (${stat.kind}) ` +
+ `for ${report.get(stat.codecId).mimeType}. value=${stat.qpSum}`
+ );
+ } else if (mimeType.includes("H264")) {
+ // OpenH264 encoder records QP so we check for either condition.
+ if (!stat.qpSum && !("qpSum" in stat)) {
+ ok(
+ !stat.qpSum && !("qpSum" in stat),
+ `${stat.type}.qpSum absent for ${report.get(stat.codecId).mimeType}`
+ );
+ } else {
+ ok(
+ stat.qpSum >= 0,
+ `${stat.type}.qpSum is a sane number (${stat.kind}) ` +
+ `for ${report.get(stat.codecId).mimeType}. value=${stat.qpSum}`
+ );
+ }
+ } else {
+ ok(
+ !stat.qpSum && !("qpSum" in stat),
+ `${stat.type}.qpSum absent for ${report.get(stat.codecId).mimeType}`
+ );
+ }
+
+ //
+ // Local video only stats
+ //
+ if (stat.inner.kind != "video") {
+ expectations.localVideoOnly.forEach(field => {
+ ok(
+ stat[field] === undefined,
+ `${stat.type} does not have field ` +
+ `${field} when kind is not 'video'`
+ );
+ });
+ } else {
+ expectations.localVideoOnly.forEach(field => {
+ ok(
+ stat.inner[field] !== undefined,
+ `${stat.type} has field ` +
+ `${field} when kind is video and isRemote is false`
+ );
+ });
+
+ // framesEncoded
+ ok(
+ stat.framesEncoded >= 0 && stat.framesEncoded < 100000,
+ `${stat.type}.framesEncoded is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.framesEncoded}`
+ );
+
+ // frameWidth
+ ok(
+ stat.frameWidth >= 0 && stat.frameWidth < 100000,
+ `${stat.type}.frameWidth is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.frameWidth}`
+ );
+
+ // frameHeight
+ ok(
+ stat.frameHeight >= 0 && stat.frameHeight < 100000,
+ `${stat.type}.frameHeight is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.frameHeight}`
+ );
+
+ // framesSent
+ ok(
+ stat.framesSent >= 0 && stat.framesSent < 100000,
+ `${stat.type}.framesSent is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.framesSent}`
+ );
+
+ // hugeFramesSent
+ ok(
+ stat.hugeFramesSent >= 0 && stat.hugeFramesSent < 100000,
+ `${stat.type}.hugeFramesSent is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.hugeFramesSent}`
+ );
+
+ // totalEncodeTime
+ ok(
+ stat.totalEncodeTime >= 0,
+ `${stat.type}.totalEncodeTime is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.totalEncodeTime}`
+ );
+
+ // totalEncodedBytesTarget
+ ok(
+ stat.totalEncodedBytesTarget > 1000,
+ `${stat.type}.totalEncodedBytesTarget is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.totalEncodedBytesTarget}`
+ );
+ }
+ } else if (stat.type == "remote-outbound-rtp") {
+ //
+ // Required fields
+ //
+
+ // packetsSent
+ ok(
+ stat.packetsSent > 0 && stat.packetsSent < 10000,
+ `${stat.type}.packetsSent is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.packetsSent}`
+ );
+
+ // bytesSent
+ const audio1Min = 16000 * 60; // 128kbps
+ const video1Min = 250000 * 60; // 2Mbps
+ ok(
+ stat.bytesSent > 0 &&
+ stat.bytesSent < (stat.kind == "video" ? video1Min : audio1Min),
+ `${stat.type}.bytesSent is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.bytesSent}`
+ );
+
+ ok(
+ stat.remoteTimestamp !== undefined,
+ `${stat.type}.remoteTimestamp ` + `is not undefined (${stat.kind})`
+ );
+ const ageSeconds = (stat.timestamp - stat.remoteTimestamp) / 1000;
+ // remoteTimestamp is exact (so it can be mapped to a packet), whereas
+ // timestamp has reduced precision. It is possible that
+ // remoteTimestamp occurs a millisecond into the future from
+ // timestamp. We also subtract half a millisecond when reducing
+ // precision on libwebrtc timestamps, to counteract the potential
+ // rounding up that libwebrtc may do since it tends to round its
+ // internal timestamps to whole milliseconds. In the worst case
+ // remoteTimestamp may therefore occur 2 milliseconds ahead of
+ // timestamp.
+ ok(
+ ageSeconds >= -0.002 && ageSeconds < 30,
+ `${stat.type}.remoteTimestamp is on the same timeline as ` +
+ `${stat.type}.timestamp, and no older than 30 seconds. ` +
+ `difference=${ageSeconds}s`
+ );
+ } else if (stat.type == "media-source") {
+ // trackIdentifier
+ is(typeof stat.trackIdentifier, "string");
+ isnot(stat.trackIdentifier, "");
+
+ // kind
+ is(typeof stat.kind, "string");
+ ok(stat.kind == "audio" || stat.kind == "video");
+ } else if (stat.type == "codec") {
+ //
+ // Required fields
+ //
+
+ // mimeType & payloadType
+ switch (stat.mimeType) {
+ case "audio/opus":
+ is(stat.payloadType, 109, "codec.payloadType for opus");
+ break;
+ case "video/VP8":
+ is(stat.payloadType, 120, "codec.payloadType for VP8");
+ break;
+ case "video/VP9":
+ is(stat.payloadType, 121, "codec.payloadType for VP9");
+ break;
+ case "video/H264":
+ ok(
+ stat.payloadType == 97 || stat.payloadType == 126,
+ `codec.payloadType for H264 was ${stat.payloadType}, exp. 97 or 126`
+ );
+ break;
+ default:
+ ok(
+ false,
+ `Unexpected codec.mimeType ${stat.mimeType} for payloadType ` +
+ `${stat.payloadType}`
+ );
+ break;
+ }
+
+ // transportId
+ // (no transport stats yet)
+ ok(stat.transportId, "codec.transportId is set");
+
+ // clockRate
+ if (stat.mimeType.startsWith("audio")) {
+ is(stat.clockRate, 48000, "codec.clockRate for audio/opus");
+ } else if (stat.mimeType.startsWith("video")) {
+ is(stat.clockRate, 90000, "codec.clockRate for video");
+ }
+
+ // sdpFmtpLine
+ // (not technically mandated by spec, but expected here)
+ ok(stat.sdpFmtpLine, "codec.sdpFmtpLine is set");
+ const opusParams = [
+ "maxplaybackrate",
+ "maxaveragebitrate",
+ "usedtx",
+ "stereo",
+ "useinbandfec",
+ "cbr",
+ "ptime",
+ "minptime",
+ "maxptime",
+ ];
+ const vpxParams = ["max-fs", "max-fr"];
+ const h264Params = [
+ "packetization-mode",
+ "level-asymmetry-allowed",
+ "profile-level-id",
+ "max-fs",
+ "max-cpb",
+ "max-dpb",
+ "max-br",
+ "max-mbps",
+ ];
+ for (const param of stat.sdpFmtpLine.split(";")) {
+ const [key, value] = param.split("=");
+ if (stat.payloadType == 109) {
+ ok(
+ opusParams.includes(key),
+ `codec.sdpFmtpLine param ${key}=${value} for opus`
+ );
+ } else if (stat.payloadType == 120 || stat.payloadType == 121) {
+ ok(
+ vpxParams.includes(key),
+ `codec.sdpFmtpLine param ${key}=${value} for VPx`
+ );
+ } else if (stat.payloadType == 97 || stat.payloadType == 126) {
+ ok(
+ h264Params.includes(key),
+ `codec.sdpFmtpLine param ${key}=${value} for H264`
+ );
+ if (key == "packetization-mode") {
+ if (stat.payloadType == 97) {
+ is(value, "0", "codec.sdpFmtpLine: H264 (97) packetization-mode");
+ } else if (stat.payloadType == 126) {
+ is(
+ value,
+ "1",
+ "codec.sdpFmtpLine: H264 (126) packetization-mode"
+ );
+ }
+ }
+ if (key == "profile-level-id") {
+ is(value, "42e01f", "codec.sdpFmtpLine: H264 profile-level-id");
+ }
+ }
+ }
+
+ //
+ // Optional fields
+ //
+
+ // codecType
+ ok(
+ !Object.keys(stat).includes("codecType") ||
+ stat.codecType == "encode" ||
+ stat.codecType == "decode",
+ "codec.codecType (${codec.codecType}) is an expected value or absent"
+ );
+ let numRecvStreams = 0;
+ let numSendStreams = 0;
+ const counts = {
+ "inbound-rtp": 0,
+ "outbound-rtp": 0,
+ "remote-inbound-rtp": 0,
+ "remote-outbound-rtp": 0,
+ };
+ const [kind] = stat.mimeType.split("/");
+ report.forEach(other => {
+ if (other.type == "inbound-rtp" && other.kind == kind) {
+ numRecvStreams += 1;
+ } else if (other.type == "outbound-rtp" && other.kind == kind) {
+ numSendStreams += 1;
+ }
+ if (other.codecId == stat.id) {
+ counts[other.type] += 1;
+ }
+ });
+ const expectedCounts = {
+ encode: {
+ "inbound-rtp": 0,
+ "outbound-rtp": numSendStreams,
+ "remote-inbound-rtp": numSendStreams,
+ "remote-outbound-rtp": 0,
+ },
+ decode: {
+ "inbound-rtp": numRecvStreams,
+ "outbound-rtp": 0,
+ "remote-inbound-rtp": 0,
+ "remote-outbound-rtp": numRecvStreams,
+ },
+ absent: {
+ "inbound-rtp": numRecvStreams,
+ "outbound-rtp": numSendStreams,
+ "remote-inbound-rtp": numSendStreams,
+ "remote-outbound-rtp": numRecvStreams,
+ },
+ };
+ // Note that the logic above assumes at most one sender and at most one
+ // receiver was used to generate this stats report. If more senders or
+ // receivers are present, they'd be referring to not only this codec stat,
+ // skewing `numSendStreams` and `numRecvStreams` above.
+ // This could be fixed when we support `senderId` and `receiverId` in
+ // RTCOutboundRtpStreamStats and RTCInboundRtpStreamStats respectively.
+ for (const [key, value] of Object.entries(counts)) {
+ is(
+ value,
+ expectedCounts[stat.codecType || "absent"][key],
+ `codec.codecType ${stat.codecType || "absent"} ref from ${key} stat`
+ );
+ }
+
+ // channels
+ if (stat.mimeType.startsWith("audio")) {
+ ok(stat.channels, "codec.channels should exist for audio");
+ if (stat.channels) {
+ if (stat.sdpFmtpLine.includes("stereo=1")) {
+ is(stat.channels, 2, "codec.channels for stereo audio");
+ } else {
+ is(stat.channels, 1, "codec.channels for mono audio");
+ }
+ }
+ } else {
+ ok(!stat.channels, "codec.channels should not exist for video");
+ }
+ } else if (stat.type == "candidate-pair") {
+ info("candidate-pair is: " + JSON.stringify(stat));
+ //
+ // Required fields
+ //
+
+ // transportId
+ ok(
+ stat.transportId,
+ `${stat.type}.transportId has a value. value=` +
+ `${stat.transportId} (${stat.kind})`
+ );
+
+ // localCandidateId
+ ok(
+ stat.localCandidateId,
+ `${stat.type}.localCandidateId has a value. value=` +
+ `${stat.localCandidateId} (${stat.kind})`
+ );
+
+ // remoteCandidateId
+ ok(
+ stat.remoteCandidateId,
+ `${stat.type}.remoteCandidateId has a value. value=` +
+ `${stat.remoteCandidateId} (${stat.kind})`
+ );
+
+ // priority
+ ok(
+ stat.priority,
+ `${stat.type}.priority has a value. value=` +
+ `${stat.priority} (${stat.kind})`
+ );
+
+ // readable
+ ok(
+ stat.readable,
+ `${stat.type}.readable is true. value=${stat.readable} ` +
+ `(${stat.kind})`
+ );
+
+ // writable
+ ok(
+ stat.writable,
+ `${stat.type}.writable is true. value=${stat.writable} ` +
+ `(${stat.kind})`
+ );
+
+ // state
+ if (
+ stat.state == "succeeded" &&
+ stat.selected !== undefined &&
+ stat.selected
+ ) {
+ info("candidate-pair state is succeeded and selected is true");
+ // nominated
+ ok(
+ stat.nominated,
+ `${stat.type}.nominated is true. value=${stat.nominated} ` +
+ `(${stat.kind})`
+ );
+
+ // bytesSent
+ ok(
+ stat.bytesSent > 1000,
+ `${stat.type}.bytesSent is a sane number (>1,000) for a short ` +
+ `${stat.kind} test. value=${stat.bytesSent}`
+ );
+
+ // bytesReceived
+ ok(
+ stat.bytesReceived > 500,
+ `${stat.type}.bytesReceived is a sane number (>500) for a short ` +
+ `${stat.kind} test. value=${stat.bytesReceived}`
+ );
+
+ // lastPacketSentTimestamp
+ ok(
+ stat.lastPacketSentTimestamp,
+ `${stat.type}.lastPacketSentTimestamp has a value. value=` +
+ `${stat.lastPacketSentTimestamp} (${stat.kind})`
+ );
+
+ // lastPacketReceivedTimestamp
+ ok(
+ stat.lastPacketReceivedTimestamp,
+ `${stat.type}.lastPacketReceivedTimestamp has a value. value=` +
+ `${stat.lastPacketReceivedTimestamp} (${stat.kind})`
+ );
+ } else {
+ info("candidate-pair is _not_ both state == succeeded and selected");
+ // nominated
+ ok(
+ stat.nominated !== undefined,
+ `${stat.type}.nominated exists. value=${stat.nominated} ` +
+ `(${stat.kind})`
+ );
+ ok(
+ stat.bytesSent !== undefined,
+ `${stat.type}.bytesSent exists. value=${stat.bytesSent} ` +
+ `(${stat.kind})`
+ );
+ ok(
+ stat.bytesReceived !== undefined,
+ `${stat.type}.bytesReceived exists. value=${stat.bytesReceived} ` +
+ `(${stat.kind})`
+ );
+ ok(
+ stat.lastPacketSentTimestamp !== undefined,
+ `${stat.type}.lastPacketSentTimestamp exists. value=` +
+ `${stat.lastPacketSentTimestamp} (${stat.kind})`
+ );
+ ok(
+ stat.lastPacketReceivedTimestamp !== undefined,
+ `${stat.type}.lastPacketReceivedTimestamp exists. value=` +
+ `${stat.lastPacketReceivedTimestamp} (${stat.kind})`
+ );
+ }
+
+ //
+ // Optional fields
+ //
+ // selected
+ ok(
+ stat.selected === undefined ||
+ (stat.state == "succeeded" && stat.selected) ||
+ !stat.selected,
+ `${stat.type}.selected is undefined, true when state is succeeded, ` +
+ `or false. value=${stat.selected} (${stat.kind})`
+ );
+ } else if (
+ stat.type == "local-candidate" ||
+ stat.type == "remote-candidate"
+ ) {
+ info(`candidate is ${JSON.stringify(stat)}`);
+
+ // address
+ ok(
+ stat.address,
+ `${stat.type} has address. value=${stat.address} ` + `(${stat.kind})`
+ );
+
+ // protocol
+ ok(
+ stat.protocol,
+ `${stat.type} has protocol. value=${stat.protocol} ` + `(${stat.kind})`
+ );
+
+ // port
+ ok(
+ stat.port >= 0,
+ `${stat.type} has port >= 0. value=${stat.port} ` + `(${stat.kind})`
+ );
+ ok(
+ stat.port <= 65535,
+ `${stat.type} has port <= 65535. value=${stat.port} ` + `(${stat.kind})`
+ );
+
+ // candidateType
+ ok(
+ stat.candidateType,
+ `${stat.type} has candidateType. value=${stat.candidateType} ` +
+ `(${stat.kind})`
+ );
+
+ // priority
+ ok(
+ stat.priority > 0 && stat.priority < 2 ** 32 - 1,
+ `${stat.type} has priority between 1 and 2^32 - 1 inc. ` +
+ `value=${stat.priority} (${stat.kind})`
+ );
+
+ // relayProtocol
+ if (stat.type == "local-candidate" && stat.candidateType == "relay") {
+ ok(
+ stat.relayProtocol,
+ `relay ${stat.type} has relayProtocol. value=${stat.relayProtocol} ` +
+ `(${stat.kind})`
+ );
+ } else {
+ is(
+ stat.relayProtocol,
+ undefined,
+ `relayProtocol is undefined for candidates that are not relay and ` +
+ `local. value=${stat.relayProtocol} (${stat.kind})`
+ );
+ }
+
+ // proxied
+ if (stat.proxied) {
+ ok(
+ stat.proxied == "proxied" || stat.proxied == "non-proxied",
+ `${stat.type} has proxied. value=${stat.proxied} (${stat.kind})`
+ );
+ }
+ }
+
+ //
+ // Ensure everything was tested
+ //
+ [...expectations.expected, ...expectations.optional].forEach(field => {
+ ok(
+ Object.keys(tested).includes(field),
+ `${stat.type}.${field} was tested.`
+ );
+ });
+ });
+}
+
+function dumpStats(stats) {
+ const dict = {};
+ for (const [k, v] of stats.entries()) {
+ dict[k] = v;
+ }
+ info(`Got stats: ${JSON.stringify(dict)}`);
+}
+
+async function waitForSyncedRtcp(pc) {
+ // Ensures that RTCP is present
+ let ensureSyncedRtcp = async () => {
+ let report = await pc.getStats();
+ for (const v of report.values()) {
+ if (v.type.endsWith("bound-rtp") && !(v.remoteId || v.localId)) {
+ info(`${v.id} is missing remoteId or localId: ${JSON.stringify(v)}`);
+ return null;
+ }
+ if (v.type == "remote-inbound-rtp" && v.roundTripTime === undefined) {
+ info(`${v.id} is missing roundTripTime: ${JSON.stringify(v)}`);
+ return null;
+ }
+ }
+ return report;
+ };
+ // Returns true if there is proof in aStats of rtcp flow for all remote stats
+ // objects, compared to baseStats.
+ const hasAllRtcpUpdated = (baseStats, stats) => {
+ let hasRtcpStats = false;
+ for (const v of stats.values()) {
+ if (v.type == "remote-outbound-rtp") {
+ hasRtcpStats = true;
+ if (!v.remoteTimestamp) {
+ // `remoteTimestamp` is 0 or not present.
+ return false;
+ }
+ if (v.remoteTimestamp <= baseStats.get(v.id)?.remoteTimestamp) {
+ // `remoteTimestamp` has not advanced further than the base stats,
+ // i.e., no new sender report has been received.
+ return false;
+ }
+ } else if (v.type == "remote-inbound-rtp") {
+ hasRtcpStats = true;
+ // The ideal thing here would be to check `reportsReceived`, but it's
+ // not yet implemented.
+ if (!v.packetsReceived) {
+ // `packetsReceived` is 0 or not present.
+ return false;
+ }
+ if (v.packetsReceived <= baseStats.get(v.id)?.packetsReceived) {
+ // `packetsReceived` has not advanced further than the base stats,
+ // i.e., no new receiver report has been received.
+ return false;
+ }
+ }
+ }
+ return hasRtcpStats;
+ };
+ let attempts = 0;
+ const baseStats = await pc.getStats();
+ // Time-units are MS
+ const waitPeriod = 100;
+ const maxTime = 20000;
+ for (let totalTime = maxTime; totalTime > 0; totalTime -= waitPeriod) {
+ try {
+ let syncedStats = await ensureSyncedRtcp();
+ if (syncedStats && hasAllRtcpUpdated(baseStats, syncedStats)) {
+ dumpStats(syncedStats);
+ return syncedStats;
+ }
+ } catch (e) {
+ info(e);
+ info(e.stack);
+ throw e;
+ }
+ attempts += 1;
+ info(`waitForSyncedRtcp: no sync on attempt ${attempts}, retrying.`);
+ await wait(waitPeriod);
+ }
+ throw Error(
+ "Waiting for synced RTCP timed out after at least " + maxTime + "ms"
+ );
+}
+
+function checkSenderStats(senderStats, streamCount) {
+ const outboundRtpReports = [];
+ const remoteInboundRtpReports = [];
+ for (const v of senderStats.values()) {
+ if (v.type == "outbound-rtp") {
+ outboundRtpReports.push(v);
+ } else if (v.type == "remote-inbound-rtp") {
+ remoteInboundRtpReports.push(v);
+ }
+ }
+ is(
+ outboundRtpReports.length,
+ streamCount,
+ `Sender with ${streamCount} simulcast streams has ${streamCount} outbound-rtp reports`
+ );
+ is(
+ remoteInboundRtpReports.length,
+ streamCount,
+ `Sender with ${streamCount} simulcast streams has ${streamCount} remote-inbound-rtp reports`
+ );
+ for (const outboundRtpReport of outboundRtpReports) {
+ is(
+ outboundRtpReports.filter(r => r.ssrc == outboundRtpReport.ssrc).length,
+ 1,
+ "Simulcast send track SSRCs are distinct"
+ );
+ const remoteReports = remoteInboundRtpReports.filter(
+ r => r.id == outboundRtpReport.remoteId
+ );
+ is(
+ remoteReports.length,
+ 1,
+ "Simulcast send tracks have exactly one remote counterpart"
+ );
+ const remoteInboundRtpReport = remoteReports[0];
+ is(
+ outboundRtpReport.ssrc,
+ remoteInboundRtpReport.ssrc,
+ "SSRC matches for outbound-rtp and remote-inbound-rtp"
+ );
+ }
+}
+
+function PC_LOCAL_TEST_LOCAL_STATS(test) {
+ return waitForSyncedRtcp(test.pcLocal._pc).then(stats => {
+ checkExpectedFields(stats);
+ pedanticChecks(stats);
+ return Promise.all([
+ test.pcLocal._pc.getSenders().map(async s => {
+ checkSenderStats(
+ await s.getStats(),
+ Math.max(1, s.getParameters()?.encodings?.length ?? 0)
+ );
+ }),
+ ]);
+ });
+}
+
+function PC_REMOTE_TEST_REMOTE_STATS(test) {
+ return waitForSyncedRtcp(test.pcRemote._pc).then(stats => {
+ checkExpectedFields(stats);
+ pedanticChecks(stats);
+ return Promise.all([
+ test.pcRemote._pc.getSenders().map(async s => {
+ checkSenderStats(
+ await s.getStats(),
+ s.track ? Math.max(1, s.getParameters()?.encodings?.length ?? 0) : 0
+ );
+ }),
+ ]);
+ });
+}
diff --git a/dom/media/webrtc/tests/mochitests/templates.js b/dom/media/webrtc/tests/mochitests/templates.js
new file mode 100644
index 0000000000..6b7750fd2c
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/templates.js
@@ -0,0 +1,615 @@
+/**
+ * Default list of commands to execute for a PeerConnection test.
+ */
+
+const STABLE = "stable";
+const HAVE_LOCAL_OFFER = "have-local-offer";
+const HAVE_REMOTE_OFFER = "have-remote-offer";
+const CLOSED = "closed";
+
+const ICE_NEW = "new";
+const GATH_NEW = "new";
+const GATH_GATH = "gathering";
+const GATH_COMPLETE = "complete";
+
+function deltaSeconds(date1, date2) {
+ return (date2.getTime() - date1.getTime()) / 1000;
+}
+
+function dumpSdp(test) {
+ if (typeof test._local_offer !== "undefined") {
+ dump("ERROR: SDP offer: " + test._local_offer.sdp.replace(/[\r]/g, ""));
+ }
+ if (typeof test._remote_answer !== "undefined") {
+ dump("ERROR: SDP answer: " + test._remote_answer.sdp.replace(/[\r]/g, ""));
+ }
+
+ if (
+ test.pcLocal &&
+ typeof test.pcLocal._local_ice_candidates !== "undefined"
+ ) {
+ dump(
+ "pcLocal._local_ice_candidates: " +
+ JSON.stringify(test.pcLocal._local_ice_candidates) +
+ "\n"
+ );
+ dump(
+ "pcLocal._remote_ice_candidates: " +
+ JSON.stringify(test.pcLocal._remote_ice_candidates) +
+ "\n"
+ );
+ dump(
+ "pcLocal._ice_candidates_to_add: " +
+ JSON.stringify(test.pcLocal._ice_candidates_to_add) +
+ "\n"
+ );
+ }
+ if (
+ test.pcRemote &&
+ typeof test.pcRemote._local_ice_candidates !== "undefined"
+ ) {
+ dump(
+ "pcRemote._local_ice_candidates: " +
+ JSON.stringify(test.pcRemote._local_ice_candidates) +
+ "\n"
+ );
+ dump(
+ "pcRemote._remote_ice_candidates: " +
+ JSON.stringify(test.pcRemote._remote_ice_candidates) +
+ "\n"
+ );
+ dump(
+ "pcRemote._ice_candidates_to_add: " +
+ JSON.stringify(test.pcRemote._ice_candidates_to_add) +
+ "\n"
+ );
+ }
+
+ if (test.pcLocal && typeof test.pcLocal.iceConnectionLog !== "undefined") {
+ dump(
+ "pcLocal ICE connection state log: " +
+ test.pcLocal.iceConnectionLog +
+ "\n"
+ );
+ }
+ if (test.pcRemote && typeof test.pcRemote.iceConnectionLog !== "undefined") {
+ dump(
+ "pcRemote ICE connection state log: " +
+ test.pcRemote.iceConnectionLog +
+ "\n"
+ );
+ }
+
+ if (
+ test.pcLocal &&
+ test.pcRemote &&
+ typeof test.pcLocal.setRemoteDescDate !== "undefined" &&
+ typeof test.pcRemote.setLocalDescDate !== "undefined"
+ ) {
+ var delta = deltaSeconds(
+ test.pcLocal.setRemoteDescDate,
+ test.pcRemote.setLocalDescDate
+ );
+ dump(
+ "Delay between pcLocal.setRemote <-> pcRemote.setLocal: " + delta + "\n"
+ );
+ }
+ if (
+ test.pcLocal &&
+ test.pcRemote &&
+ typeof test.pcLocal.setRemoteDescDate !== "undefined" &&
+ typeof test.pcLocal.setRemoteDescStableEventDate !== "undefined"
+ ) {
+ var delta = deltaSeconds(
+ test.pcLocal.setRemoteDescDate,
+ test.pcLocal.setRemoteDescStableEventDate
+ );
+ dump(
+ "Delay between pcLocal.setRemote <-> pcLocal.signalingStateStable: " +
+ delta +
+ "\n"
+ );
+ }
+ if (
+ test.pcLocal &&
+ test.pcRemote &&
+ typeof test.pcRemote.setLocalDescDate !== "undefined" &&
+ typeof test.pcRemote.setLocalDescStableEventDate !== "undefined"
+ ) {
+ var delta = deltaSeconds(
+ test.pcRemote.setLocalDescDate,
+ test.pcRemote.setLocalDescStableEventDate
+ );
+ dump(
+ "Delay between pcRemote.setLocal <-> pcRemote.signalingStateStable: " +
+ delta +
+ "\n"
+ );
+ }
+}
+
+// We need to verify that at least one candidate has been (or will be) gathered.
+function waitForAnIceCandidate(pc) {
+ return new Promise(resolve => {
+ if (!pc.localRequiresTrickleIce || pc._local_ice_candidates.length) {
+ resolve();
+ } else {
+ // In some circumstances, especially when both PCs are on the same
+ // browser, even though we are connected, the connection can be
+ // established without receiving a single candidate from one or other
+ // peer. So we wait for at least one...
+ pc._pc.addEventListener("icecandidate", resolve);
+ }
+ }).then(() => {
+ ok(
+ pc._local_ice_candidates.length,
+ pc + " received local trickle ICE candidates"
+ );
+ isnot(
+ pc._pc.iceGatheringState,
+ GATH_NEW,
+ pc + " ICE gathering state is not 'new'"
+ );
+ });
+}
+
+async function checkTrackStats(pc, track, outbound) {
+ const audio = track.kind == "audio";
+ const msg =
+ `${pc} stats ${outbound ? "outbound " : "inbound "}` +
+ `${audio ? "audio" : "video"} rtp track id ${track.id}`;
+ const stats = await pc.getStats(track);
+ ok(
+ pc.hasStat(stats, {
+ type: outbound ? "outbound-rtp" : "inbound-rtp",
+ kind: audio ? "audio" : "video",
+ }),
+ `${msg} - found expected stats`
+ );
+ ok(
+ !pc.hasStat(stats, {
+ type: outbound ? "inbound-rtp" : "outbound-rtp",
+ }),
+ `${msg} - did not find extra stats with wrong direction`
+ );
+ ok(
+ !pc.hasStat(stats, {
+ kind: audio ? "video" : "audio",
+ }),
+ `${msg} - did not find extra stats with wrong media type`
+ );
+}
+
+function checkAllTrackStats(pc) {
+ return Promise.all([
+ ...pc
+ .getExpectedActiveReceivers()
+ .map(({ track }) => checkTrackStats(pc, track, false)),
+ ...pc
+ .getExpectedSenders()
+ .map(({ track }) => checkTrackStats(pc, track, true)),
+ ]);
+}
+
+// Commands run once at the beginning of each test, even when performing a
+// renegotiation test.
+var commandsPeerConnectionInitial = [
+ function PC_LOCAL_SETUP_ICE_LOGGER(test) {
+ test.pcLocal.logIceConnectionState();
+ },
+
+ function PC_REMOTE_SETUP_ICE_LOGGER(test) {
+ test.pcRemote.logIceConnectionState();
+ },
+
+ function PC_LOCAL_SETUP_SIGNALING_LOGGER(test) {
+ test.pcLocal.logSignalingState();
+ },
+
+ function PC_REMOTE_SETUP_SIGNALING_LOGGER(test) {
+ test.pcRemote.logSignalingState();
+ },
+
+ function PC_LOCAL_SETUP_TRACK_HANDLER(test) {
+ test.pcLocal.setupTrackEventHandler();
+ },
+
+ function PC_REMOTE_SETUP_TRACK_HANDLER(test) {
+ test.pcRemote.setupTrackEventHandler();
+ },
+
+ function PC_LOCAL_CHECK_INITIAL_SIGNALINGSTATE(test) {
+ is(
+ test.pcLocal.signalingState,
+ STABLE,
+ "Initial local signalingState is 'stable'"
+ );
+ },
+
+ function PC_REMOTE_CHECK_INITIAL_SIGNALINGSTATE(test) {
+ is(
+ test.pcRemote.signalingState,
+ STABLE,
+ "Initial remote signalingState is 'stable'"
+ );
+ },
+
+ function PC_LOCAL_CHECK_INITIAL_ICE_STATE(test) {
+ is(
+ test.pcLocal.iceConnectionState,
+ ICE_NEW,
+ "Initial local ICE connection state is 'new'"
+ );
+ },
+
+ function PC_REMOTE_CHECK_INITIAL_ICE_STATE(test) {
+ is(
+ test.pcRemote.iceConnectionState,
+ ICE_NEW,
+ "Initial remote ICE connection state is 'new'"
+ );
+ },
+
+ function PC_LOCAL_CHECK_INITIAL_CAN_TRICKLE_SYNC(test) {
+ is(
+ test.pcLocal._pc.canTrickleIceCandidates,
+ null,
+ "Local trickle status should start out unknown"
+ );
+ },
+
+ function PC_REMOTE_CHECK_INITIAL_CAN_TRICKLE_SYNC(test) {
+ is(
+ test.pcRemote._pc.canTrickleIceCandidates,
+ null,
+ "Remote trickle status should start out unknown"
+ );
+ },
+];
+
+var commandsGetUserMedia = [
+ function PC_LOCAL_GUM(test) {
+ return test.pcLocal.getAllUserMediaAndAddStreams(test.pcLocal.constraints);
+ },
+
+ function PC_REMOTE_GUM(test) {
+ return test.pcRemote.getAllUserMediaAndAddStreams(
+ test.pcRemote.constraints
+ );
+ },
+];
+
+var commandsPeerConnectionOfferAnswer = [
+ function PC_LOCAL_SETUP_ICE_HANDLER(test) {
+ test.pcLocal.setupIceCandidateHandler(test);
+ },
+
+ function PC_REMOTE_SETUP_ICE_HANDLER(test) {
+ test.pcRemote.setupIceCandidateHandler(test);
+ },
+
+ function PC_LOCAL_CREATE_OFFER(test) {
+ return test.createOffer(test.pcLocal).then(offer => {
+ is(
+ test.pcLocal.signalingState,
+ STABLE,
+ "Local create offer does not change signaling state"
+ );
+ });
+ },
+
+ function PC_LOCAL_SET_LOCAL_DESCRIPTION(test) {
+ return test
+ .setLocalDescription(test.pcLocal, test.originalOffer, HAVE_LOCAL_OFFER)
+ .then(() => {
+ is(
+ test.pcLocal.signalingState,
+ HAVE_LOCAL_OFFER,
+ "signalingState after local setLocalDescription is 'have-local-offer'"
+ );
+ });
+ },
+
+ function PC_REMOTE_GET_OFFER(test) {
+ test._local_offer = test.originalOffer;
+ test._offer_constraints = test.pcLocal.constraints;
+ test._offer_options = test.pcLocal.offerOptions;
+ return Promise.resolve();
+ },
+
+ function PC_REMOTE_SET_REMOTE_DESCRIPTION(test) {
+ return test
+ .setRemoteDescription(test.pcRemote, test._local_offer, HAVE_REMOTE_OFFER)
+ .then(() => {
+ is(
+ test.pcRemote.signalingState,
+ HAVE_REMOTE_OFFER,
+ "signalingState after remote setRemoteDescription is 'have-remote-offer'"
+ );
+ });
+ },
+
+ function PC_REMOTE_CHECK_CAN_TRICKLE_SYNC(test) {
+ is(
+ test.pcRemote._pc.canTrickleIceCandidates,
+ true,
+ "Remote thinks that local can trickle"
+ );
+ },
+
+ function PC_LOCAL_SANE_LOCAL_SDP(test) {
+ test.pcLocal.localRequiresTrickleIce = sdputils.verifySdp(
+ test._local_offer,
+ "offer",
+ test._offer_constraints,
+ test._offer_options,
+ test.testOptions
+ );
+ },
+
+ function PC_REMOTE_SANE_REMOTE_SDP(test) {
+ test.pcRemote.remoteRequiresTrickleIce = sdputils.verifySdp(
+ test._local_offer,
+ "offer",
+ test._offer_constraints,
+ test._offer_options,
+ test.testOptions
+ );
+ },
+
+ function PC_REMOTE_CREATE_ANSWER(test) {
+ return test.createAnswer(test.pcRemote).then(answer => {
+ is(
+ test.pcRemote.signalingState,
+ HAVE_REMOTE_OFFER,
+ "Remote createAnswer does not change signaling state"
+ );
+ });
+ },
+
+ function PC_REMOTE_SET_LOCAL_DESCRIPTION(test) {
+ return test
+ .setLocalDescription(test.pcRemote, test.originalAnswer, STABLE)
+ .then(() => {
+ is(
+ test.pcRemote.signalingState,
+ STABLE,
+ "signalingState after remote setLocalDescription is 'stable'"
+ );
+ });
+ },
+
+ function PC_LOCAL_GET_ANSWER(test) {
+ test._remote_answer = test.originalAnswer;
+ test._answer_constraints = test.pcRemote.constraints;
+ return Promise.resolve();
+ },
+
+ function PC_LOCAL_SET_REMOTE_DESCRIPTION(test) {
+ return test
+ .setRemoteDescription(test.pcLocal, test._remote_answer, STABLE)
+ .then(() => {
+ is(
+ test.pcLocal.signalingState,
+ STABLE,
+ "signalingState after local setRemoteDescription is 'stable'"
+ );
+ });
+ },
+
+ function PC_REMOTE_SANE_LOCAL_SDP(test) {
+ test.pcRemote.localRequiresTrickleIce = sdputils.verifySdp(
+ test._remote_answer,
+ "answer",
+ test._offer_constraints,
+ test._offer_options,
+ test.testOptions
+ );
+ },
+ function PC_LOCAL_SANE_REMOTE_SDP(test) {
+ test.pcLocal.remoteRequiresTrickleIce = sdputils.verifySdp(
+ test._remote_answer,
+ "answer",
+ test._offer_constraints,
+ test._offer_options,
+ test.testOptions
+ );
+ },
+
+ function PC_LOCAL_CHECK_CAN_TRICKLE_SYNC(test) {
+ is(
+ test.pcLocal._pc.canTrickleIceCandidates,
+ true,
+ "Local thinks that remote can trickle"
+ );
+ },
+
+ function PC_LOCAL_WAIT_FOR_ICE_CONNECTED(test) {
+ return test.pcLocal.waitForIceConnected().then(() => {
+ info(
+ test.pcLocal +
+ ": ICE connection state log: " +
+ test.pcLocal.iceConnectionLog
+ );
+ });
+ },
+
+ function PC_REMOTE_WAIT_FOR_ICE_CONNECTED(test) {
+ return test.pcRemote.waitForIceConnected().then(() => {
+ info(
+ test.pcRemote +
+ ": ICE connection state log: " +
+ test.pcRemote.iceConnectionLog
+ );
+ });
+ },
+
+ function PC_LOCAL_VERIFY_ICE_GATHERING(test) {
+ return waitForAnIceCandidate(test.pcLocal);
+ },
+
+ function PC_REMOTE_VERIFY_ICE_GATHERING(test) {
+ return waitForAnIceCandidate(test.pcRemote);
+ },
+
+ function PC_LOCAL_WAIT_FOR_MEDIA_FLOW(test) {
+ return test.pcLocal.waitForMediaFlow();
+ },
+
+ function PC_REMOTE_WAIT_FOR_MEDIA_FLOW(test) {
+ return test.pcRemote.waitForMediaFlow();
+ },
+
+ function PC_LOCAL_CHECK_STATS(test) {
+ return test.pcLocal.getStats().then(stats => {
+ test.pcLocal.checkStats(stats);
+ });
+ },
+
+ function PC_REMOTE_CHECK_STATS(test) {
+ return test.pcRemote.getStats().then(stats => {
+ test.pcRemote.checkStats(stats);
+ });
+ },
+
+ function PC_LOCAL_CHECK_ICE_CONNECTION_TYPE(test) {
+ return test.pcLocal.getStats().then(stats => {
+ test.pcLocal.checkStatsIceConnectionType(
+ stats,
+ test.testOptions.expectedLocalCandidateType
+ );
+ });
+ },
+
+ function PC_REMOTE_CHECK_ICE_CONNECTION_TYPE(test) {
+ return test.pcRemote.getStats().then(stats => {
+ test.pcRemote.checkStatsIceConnectionType(
+ stats,
+ test.testOptions.expectedRemoteCandidateType
+ );
+ });
+ },
+
+ function PC_LOCAL_CHECK_ICE_CONNECTIONS(test) {
+ return test.pcLocal.getStats().then(stats => {
+ test.pcLocal.checkStatsIceConnections(stats, test.testOptions);
+ });
+ },
+
+ function PC_REMOTE_CHECK_ICE_CONNECTIONS(test) {
+ return test.pcRemote.getStats().then(stats => {
+ test.pcRemote.checkStatsIceConnections(stats, test.testOptions);
+ });
+ },
+
+ function PC_LOCAL_CHECK_MSID(test) {
+ return test.pcLocal.checkLocalMsids();
+ },
+ function PC_REMOTE_CHECK_MSID(test) {
+ return test.pcRemote.checkLocalMsids();
+ },
+
+ function PC_LOCAL_CHECK_TRACK_STATS(test) {
+ return checkAllTrackStats(test.pcLocal);
+ },
+ function PC_REMOTE_CHECK_TRACK_STATS(test) {
+ return checkAllTrackStats(test.pcRemote);
+ },
+ function PC_LOCAL_VERIFY_SDP_AFTER_END_OF_TRICKLE(test) {
+ if (test.pcLocal.endOfTrickleSdp) {
+ /* In case the endOfTrickleSdp promise is resolved already it will win the
+ * race because it gets evaluated first. But if endOfTrickleSdp is still
+ * pending the rejection will win the race. */
+ return Promise.race([
+ test.pcLocal.endOfTrickleSdp,
+ Promise.reject("No SDP"),
+ ]).then(
+ sdp =>
+ sdputils.checkSdpAfterEndOfTrickle(
+ sdp,
+ test.testOptions,
+ test.pcLocal.label
+ ),
+ () =>
+ info(
+ "pcLocal: Gathering is not complete yet, skipping post-gathering SDP check"
+ )
+ );
+ }
+ },
+ function PC_REMOTE_VERIFY_SDP_AFTER_END_OF_TRICKLE(test) {
+ if (test.pcRemote.endOfTrickleSdp) {
+ /* In case the endOfTrickleSdp promise is resolved already it will win the
+ * race because it gets evaluated first. But if endOfTrickleSdp is still
+ * pending the rejection will win the race. */
+ return Promise.race([
+ test.pcRemote.endOfTrickleSdp,
+ Promise.reject("No SDP"),
+ ]).then(
+ sdp =>
+ sdputils.checkSdpAfterEndOfTrickle(
+ sdp,
+ test.testOptions,
+ test.pcRemote.label
+ ),
+ () =>
+ info(
+ "pcRemote: Gathering is not complete yet, skipping post-gathering SDP check"
+ )
+ );
+ }
+ },
+];
+
+function PC_LOCAL_REMOVE_ALL_BUT_H264_FROM_OFFER(test) {
+ isnot(
+ test.originalOffer.sdp.search("H264/90000"),
+ -1,
+ "H.264 should be present in the SDP offer"
+ );
+ test.originalOffer.sdp = sdputils.removeCodec(
+ sdputils.removeCodec(
+ sdputils.removeCodec(test.originalOffer.sdp, 120),
+ 121,
+ 97
+ )
+ );
+ info("Updated H264 only offer: " + JSON.stringify(test.originalOffer));
+}
+
+function PC_LOCAL_REMOVE_BUNDLE_FROM_OFFER(test) {
+ test.originalOffer.sdp = sdputils.removeBundle(test.originalOffer.sdp);
+ info("Updated no bundle offer: " + JSON.stringify(test.originalOffer));
+}
+
+function PC_LOCAL_REMOVE_RTCPMUX_FROM_OFFER(test) {
+ test.originalOffer.sdp = sdputils.removeRtcpMux(test.originalOffer.sdp);
+ info("Updated no RTCP-Mux offer: " + JSON.stringify(test.originalOffer));
+}
+
+function PC_LOCAL_REMOVE_SSRC_FROM_OFFER(test) {
+ test.originalOffer.sdp = sdputils.removeSSRCs(test.originalOffer.sdp);
+ info("Updated no SSRCs offer: " + JSON.stringify(test.originalOffer));
+}
+
+function PC_REMOTE_REMOVE_SSRC_FROM_ANSWER(test) {
+ test.originalAnswer.sdp = sdputils.removeSSRCs(test.originalAnswer.sdp);
+ info("Updated no SSRCs answer: " + JSON.stringify(test.originalAnswer));
+}
+
+var addRenegotiation = (chain, commands, checks) => {
+ chain.append(commands);
+ chain.append(commandsPeerConnectionOfferAnswer);
+ if (checks) {
+ chain.append(checks);
+ }
+};
+
+var addRenegotiationAnswerer = (chain, commands, checks) => {
+ chain.append(function SWAP_PC_LOCAL_PC_REMOTE(test) {
+ var temp = test.pcLocal;
+ test.pcLocal = test.pcRemote;
+ test.pcRemote = temp;
+ });
+ addRenegotiation(chain, commands, checks);
+};
diff --git a/dom/media/webrtc/tests/mochitests/test_1488832.html b/dom/media/webrtc/tests/mochitests/test_1488832.html
new file mode 100644
index 0000000000..8798994b24
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_1488832.html
@@ -0,0 +1,37 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<iframe id="testframe"></iframe>
+<script>
+"use strict";
+
+createHTML({
+ title: "gUM shutdown race",
+ bug: "1488832"
+});
+
+runTest(async () => {
+ testframe.srcdoc = `
+ <html>
+ <head>
+ <script>
+ function start() {
+ for (let i = 0; i < 16; i++) {
+ window.navigator.mediaDevices.getUserMedia({video: true})
+ setTimeout('location.reload()', 100)
+ }
+ }
+ document.addEventListener('DOMContentLoaded', start)
+ </` + `script>
+ </head>
+ </html>`;
+
+ await wait(10000);
+ testframe.srcdoc = "";
+});
+</script>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_1717318.html b/dom/media/webrtc/tests/mochitests/test_1717318.html
new file mode 100644
index 0000000000..425bd29e7e
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_1717318.html
@@ -0,0 +1,26 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>PC construct with no global object (bug 1717318)</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script class="testbody" type="text/javascript">
+"use strict";
+
+// nsIArray is not special here, it could be pretty much any interface.
+// We do this outside the try block just in case someday the interface is
+// removed.
+const dummyInterface = SpecialPowers.Components.interfaces.nsIArray;
+ok(dummyInterface, "nsIArray should exist");
+try {
+ // Just don't crash.
+ SpecialPowers.Components.classes["@mozilla.org/peerconnection;1"]
+ .createInstance(dummyInterface);
+} catch (e) {}
+
+</script>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_a_noOp.html b/dom/media/webrtc/tests/mochitests/test_a_noOp.html
new file mode 100644
index 0000000000..971f5d7666
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_a_noOp.html
@@ -0,0 +1,32 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1264772
+-->
+<head>
+ <title>Test for Bug 1264772</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1264772">Mozilla Bug 1264772</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 1264772 **/
+// The WebRTC tests seem to have more problems with intermittents (at
+// least on Android) if they run first in a test run. This is a dummy test
+// to ensure that the browser is ready prior to running any actual WebRTC
+// tests.
+//
+// Note: mochitests are run in alphabetical order, so it is not sufficient
+// for this test to appear first in the manifest.
+ok(true, "test passed");
+
+</script>
+</pre>
+</body>
diff --git a/dom/media/webrtc/tests/mochitests/test_dataChannel_basicAudio.html b/dom/media/webrtc/tests/mochitests/test_dataChannel_basicAudio.html
new file mode 100644
index 0000000000..06ca9562ad
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_dataChannel_basicAudio.html
@@ -0,0 +1,25 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "796895",
+ title: "Basic data channel audio connection"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ test = new PeerConnectionTest(options);
+ addInitialDataChannel(test.chain);
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ return test.run();
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_dataChannel_basicAudioVideo.html b/dom/media/webrtc/tests/mochitests/test_dataChannel_basicAudioVideo.html
new file mode 100644
index 0000000000..ea534ca2e7
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_dataChannel_basicAudioVideo.html
@@ -0,0 +1,26 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "796891",
+ title: "Basic data channel audio/video connection"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ test = new PeerConnectionTest(options);
+ addInitialDataChannel(test.chain);
+ test.setMediaConstraints([{audio: true}, {video: true}],
+ [{audio: true}, {video: true}]);
+ return test.run();
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_dataChannel_basicAudioVideoCombined.html b/dom/media/webrtc/tests/mochitests/test_dataChannel_basicAudioVideoCombined.html
new file mode 100644
index 0000000000..d5409986ec
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_dataChannel_basicAudioVideoCombined.html
@@ -0,0 +1,26 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "796891",
+ title: "Basic data channel audio/video connection"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ test = new PeerConnectionTest(options);
+ addInitialDataChannel(test.chain);
+ test.setMediaConstraints([{audio: true, video: true}],
+ [{audio: true, video: true}]);
+ return test.run();
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_dataChannel_basicAudioVideoNoBundle.html b/dom/media/webrtc/tests/mochitests/test_dataChannel_basicAudioVideoNoBundle.html
new file mode 100644
index 0000000000..7dc22d86ad
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_dataChannel_basicAudioVideoNoBundle.html
@@ -0,0 +1,27 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1016476",
+ title: "Basic data channel audio/video connection without bundle"
+ });
+
+var test;
+runNetworkTest(function (options) {
+ options = options || { };
+ options.bundle = false;
+ test = new PeerConnectionTest(options);
+ addInitialDataChannel(test.chain);
+ test.setMediaConstraints([{audio: true}, {video: true}],
+ [{audio: true}, {video: true}]);
+ return test.run();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_dataChannel_basicDataOnly.html b/dom/media/webrtc/tests/mochitests/test_dataChannel_basicDataOnly.html
new file mode 100644
index 0000000000..98e72f7a21
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_dataChannel_basicDataOnly.html
@@ -0,0 +1,24 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "796894",
+ title: "Basic datachannel only connection"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ test = new PeerConnectionTest(options);
+ addInitialDataChannel(test.chain);
+ return test.run();
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_dataChannel_basicVideo.html b/dom/media/webrtc/tests/mochitests/test_dataChannel_basicVideo.html
new file mode 100644
index 0000000000..90f2d7caff
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_dataChannel_basicVideo.html
@@ -0,0 +1,25 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "796889",
+ title: "Basic data channel video connection"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ test = new PeerConnectionTest(options);
+ addInitialDataChannel(test.chain);
+ test.setMediaConstraints([{video: true}], [{video: true}]);
+ return test.run();
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_dataChannel_bug1013809.html b/dom/media/webrtc/tests/mochitests/test_dataChannel_bug1013809.html
new file mode 100644
index 0000000000..e36caebab4
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_dataChannel_bug1013809.html
@@ -0,0 +1,27 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "796895",
+ title: "Basic data channel audio connection"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ test = new PeerConnectionTest(options);
+ addInitialDataChannel(test.chain);
+ var sld = test.chain.remove("PC_REMOTE_SET_LOCAL_DESCRIPTION");
+ test.chain.insertAfter("PC_LOCAL_SET_REMOTE_DESCRIPTION", sld);
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ return test.run();
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_dataChannel_dataOnlyBufferedAmountLow.html b/dom/media/webrtc/tests/mochitests/test_dataChannel_dataOnlyBufferedAmountLow.html
new file mode 100644
index 0000000000..26767e0865
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_dataChannel_dataOnlyBufferedAmountLow.html
@@ -0,0 +1,25 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1051685",
+ title: "Verify bufferedAmountLowThreshold works"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ test = new PeerConnectionTest(options);
+ addInitialDataChannel(test.chain);
+ test.chain.insertAfter('PC_REMOTE_CHECK_ICE_CONNECTIONS', commandsCheckLargeXfer);
+ return test.run();
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_dataChannel_dtlsVersions.html b/dom/media/webrtc/tests/mochitests/test_dataChannel_dtlsVersions.html
new file mode 100644
index 0000000000..6f0cbc5d3d
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_dataChannel_dtlsVersions.html
@@ -0,0 +1,38 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1284103",
+ title: "Test basic data channel audio connection for supported DTLS versions"
+ });
+
+ async function testDtlsVersion(options, version) {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.peerconnection.dtls.version.min", version],
+ ["media.peerconnection.dtls.version.max", version]
+ ]
+ });
+
+ const test = new PeerConnectionTest(options);
+ addInitialDataChannel(test.chain);
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+
+ await test.run();
+ }
+
+ runNetworkTest(async (options) => {
+ // 770 = DTLS 1.0, 771 = DTLS 1.2, 772 = DTLS 1.3
+ for (var version = 770; version <= 772; version++) {
+ await testDtlsVersion(options, version);
+ }
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_dataChannel_hostnameObfuscation.html b/dom/media/webrtc/tests/mochitests/test_dataChannel_hostnameObfuscation.html
new file mode 100644
index 0000000000..d0790fb9c9
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_dataChannel_hostnameObfuscation.html
@@ -0,0 +1,59 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1592620",
+ title: "Blocklist to disable hostname obfuscation"
+ });
+
+ async function testBlocklist(options, blocklistEntry, shouldBeObfuscated) {
+ let test = new PeerConnectionTest(options);
+ addInitialDataChannel(test.chain);
+
+ if (blocklistEntry !== null) {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.peerconnection.ice.obfuscate_host_addresses.blocklist",
+ blocklistEntry]
+ ]
+ });
+ }
+
+ test.chain.insertAfter('PC_LOCAL_WAIT_FOR_ICE_CONNECTED', [
+ async function CHECK_LOCAL_CANDIDATES() {
+ const stats = await test.pcLocal.getStats();
+ stats.forEach(s => {
+ if (s.type === 'local-candidate') {
+ if (shouldBeObfuscated) {
+ ok(s.address.includes(".local"), "address should be obfuscated");
+ } else {
+ ok(!s.address.includes(".local"), "address should not be obfuscated");
+ }
+ }
+ });
+ }]);
+
+ await test.run();
+ }
+
+ runNetworkTest(async (options) => {
+ await SpecialPowers.pushPrefEnv({
+ set: [["media.peerconnection.ice.obfuscate_host_addresses", true]]
+ });
+ await testBlocklist(options, null, true);
+ await testBlocklist(options, "", true);
+ await testBlocklist(options, "example.com", true);
+ await testBlocklist(options, "mochi.test", false);
+ await testBlocklist(options, "example.com,mochi.test", false);
+ await testBlocklist(options, "*.test", false);
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_dataChannel_noOffer.html b/dom/media/webrtc/tests/mochitests/test_dataChannel_noOffer.html
new file mode 100644
index 0000000000..a6e9fa5214
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_dataChannel_noOffer.html
@@ -0,0 +1,33 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "856319",
+ title: "Don't offer m=application unless createDataChannel is called first"
+ });
+
+ runNetworkTest(async function () {
+ const pc = new RTCPeerConnection();
+
+ // necessary to circumvent bug 864109
+ const options = { offerToReceiveAudio: true };
+
+ const errorCallback = generateErrorCallback();
+ try {
+ const offer = await pc.createOffer(options);
+ ok(!offer.sdp.includes("m=application"),
+ "m=application is not contained in the SDP");
+ } catch(e) {
+ errorCallback(e);
+ }
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_dataChannel_stats.html b/dom/media/webrtc/tests/mochitests/test_dataChannel_stats.html
new file mode 100644
index 0000000000..4498e2d23a
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_dataChannel_stats.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1218356",
+ title: "DataChannel stats"
+ });
+
+ runNetworkTest(function (options) {
+ const test = new PeerConnectionTest(options);
+ test.chain.remove('PC_LOCAL_CHECK_STATS');
+ test.chain.remove('PC_REMOTE_CHECK_STATS');
+ addInitialDataChannel(test.chain);
+ test.chain.removeAfter("PC_REMOTE_CHECK_ICE_CONNECTIONS");
+ test.chain.insertAfter("PC_REMOTE_CHECK_ICE_CONNECTIONS",
+ async function TEST_DATA_CHANNEL_STATS(test) {
+ const channel = test.pcLocal.dataChannels[0];
+ test.pcRemote.dataChannels[0].onbufferedamountlow = () => {};
+ test.pcRemote.dataChannels[0].send(`Sending Message`);
+ channel.onbufferedamountlow = () => {};
+ const event = await new Promise( r => channel.onmessage = r);
+ info(`Received message: "${event.data}"`);
+ const report = await test.pcLocal.getStats();
+ info(`Received Stats ${JSON.stringify([...report.values()], null, 2)}\n`);
+ const stats = [...report.values()].find(block => block.type == "data-channel");
+ info(`DataChannel stats ${JSON.stringify(stats, null, 2)}`);
+ is(stats.label, channel.label, 'DataChannel stats has correct label');
+ is(stats.protocol, channel.protocol,
+ 'DataChannel stats has correct protocol');
+ is(stats.dataChannelIdentifier, channel.id,
+ 'DataChannel stats has correct dataChannelIdentifier');
+ is(stats.state, channel.readyState, 'DataChannel has correct state');
+ is(stats.bytesReceived, 15, 'DataChannel has correct bytesReceived');
+ is(stats.bytesSent, 0, 'DataChannel has correct bytesSent');
+ is(stats.messagesReceived, 1,
+ 'DataChannel has correct messagesReceived');
+ is(stats.messagesSent, 0, 'DataChannel has correct messagesSent');
+ });
+ return test.run();
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_defaultAudioConstraints.html b/dom/media/webrtc/tests/mochitests/test_defaultAudioConstraints.html
new file mode 100644
index 0000000000..8e0db48fff
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_defaultAudioConstraints.html
@@ -0,0 +1,80 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+"use strict";
+
+createHTML({
+ title: "Test that the audio constraints that observe at the audio constraints we expect.",
+ bug: "1509842"
+});
+
+runTest(async () => {
+ // We need a real device to get a MediaEngine supporting constraints
+ let audioDevice = SpecialPowers.getCharPref("media.audio_loopback_dev", "");
+ if (!audioDevice) {
+ todo(false, "No device set by framework. Try --use-test-media-devices");
+ return;
+ }
+
+ // Get a gUM track with the default settings, check that they are what we
+ // expect.
+ let stream = await navigator.mediaDevices.getUserMedia({ audio: true });
+ let track = stream.getAudioTracks()[0];
+ let defaultSettings = track.getSettings();
+
+ is(defaultSettings.echoCancellation, true,
+ "Echo cancellation should be ON by default.");
+ is(defaultSettings.noiseSuppression, true,
+ "Noise suppression should be ON by default.");
+ is(defaultSettings.autoGainControl, true,
+ "Automatic gain control should be ON by default.");
+
+ track.stop();
+
+ // This is UA-dependant, and belongs in a Mochitest, not in a WPT.
+ // When a gUM track has been requested with `echoCancellation` OFF, check that
+ // `noiseSuppression` and `autoGainControl` are off as well.
+ stream =
+ await navigator.mediaDevices.getUserMedia({audio:{echoCancellation: false}});
+ track = stream.getAudioTracks()[0];
+ defaultSettings = track.getSettings();
+
+ is(defaultSettings.echoCancellation, false,
+ "Echo cancellation should be OFF when requested.");
+ is(defaultSettings.noiseSuppression, false,
+ `Noise suppression should be OFF when echoCancellation is the only
+ constraint and is OFF.`);
+ is(defaultSettings.autoGainControl, false,
+ `Automatic gain control should be OFF when echoCancellation is the only
+ constraint and is OFF.`);
+
+ track.stop();
+
+ // When a gUM track has been requested with `echoCancellation` OFF, check that
+ // `noiseSuppression` and `autoGainControl` are not OFF as well if another
+ // constraint has been specified.
+ stream =
+ await navigator.mediaDevices.getUserMedia({audio:{echoCancellation: false,
+ autoGainControl: true}});
+ track = stream.getAudioTracks()[0];
+ defaultSettings = track.getSettings();
+
+ is(defaultSettings.echoCancellation, false,
+ "Echo cancellation should be OFF when requested.");
+ is(defaultSettings.noiseSuppression, false,
+ `Noise suppression should be OFF when echoCancellation is OFF and another
+ constraint has been specified.`);
+ is(defaultSettings.autoGainControl, true,
+ "Auto gain control should be ON when requested.");
+
+ track.stop();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_enumerateDevices.html b/dom/media/webrtc/tests/mochitests/test_enumerateDevices.html
new file mode 100644
index 0000000000..48bec0006a
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_enumerateDevices.html
@@ -0,0 +1,141 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({ title: "Run enumerateDevices code", bug: "1046245" });
+/**
+ Tests covering enumerateDevices API and deviceId constraint. Exercise code.
+*/
+
+async function mustSucceedWithStream(msg, f) {
+ try {
+ const stream = await f();
+ for (const track of stream.getTracks()) {
+ track.stop();
+ }
+ ok(true, msg + " must succeed");
+ } catch (e) {
+ is(e.name, null, msg + " must succeed: " + e.message);
+ }
+}
+
+async function mustFailWith(msg, reason, constraint, f) {
+ try {
+ await f();
+ ok(false, msg + " must fail");
+ } catch(e) {
+ is(e.name, reason, msg + " must fail: " + e.message);
+ if (constraint) {
+ is(e.constraint, constraint, msg + " must fail w/correct constraint.");
+ }
+ }
+}
+
+const gUM = c => navigator.mediaDevices.getUserMedia(c);
+
+const kinds = ["videoinput", "audioinput", "audiooutput"];
+
+function validateDevice({kind, label, deviceId, groupId}) {
+ ok(kinds.includes(kind), "Known device kind");
+ is(deviceId.length, 44, "deviceId length id as expected for Firefox");
+ ok(label.length !== undefined, "Device label: " + label);
+ isnot(groupId, "", "groupId must be present.");
+}
+
+runTest(async () => {
+ await pushPrefs(["media.navigator.streams.fake", true]);
+
+ // Validate enumerated devices after gUM.
+ for (const track of (await gUM({video: true, audio: true})).getTracks()) {
+ track.stop();
+ }
+
+ let devices = await navigator.mediaDevices.enumerateDevices();
+ ok(devices.length, "At least one device found");
+ const jsoned = JSON.parse(JSON.stringify(devices));
+ is(jsoned[0].kind, devices[0].kind, "kind survived serializer");
+ is(jsoned[0].deviceId, devices[0].deviceId, "deviceId survived serializer");
+ for (const device of devices) {
+ validateDevice(device);
+ if (device.kind == "audiooutput") continue;
+ // Test deviceId constraint
+ let deviceId = device.deviceId;
+ let constraints = (device.kind == "videoinput") ? { video: { deviceId } }
+ : { audio: { deviceId } };
+ for (const track of (await gUM(constraints)).getTracks()) {
+ is(typeof(track.label), "string", "Track label is a string");
+ is(track.label, device.label, "Track label is the device label");
+ track.stop();
+ }
+ }
+
+ const unknownId = "unknown9qHr8B0JIbcHlbl9xR+jMbZZ8WyoPfpCXPfc=";
+
+ // Check deviceId failure paths for video.
+
+ await mustSucceedWithStream("unknown plain deviceId on video",
+ () => gUM({ video: { deviceId: unknownId } }));
+ await mustSucceedWithStream("unknown plain deviceId on audio",
+ () => gUM({ audio: { deviceId: unknownId } }));
+ await mustFailWith("unknown exact deviceId on video",
+ "OverconstrainedError", "deviceId",
+ () => gUM({ video: { deviceId: { exact: unknownId } } }));
+ await mustFailWith("unknown exact deviceId on audio",
+ "OverconstrainedError", "deviceId",
+ () => gUM({ audio: { deviceId: { exact: unknownId } } }));
+
+ // Check that deviceIds are stable for same origin and differ across origins.
+
+ const path = "/tests/dom/media/webrtc/tests/mochitests/test_enumerateDevices_iframe.html";
+ const origins = ["https://example.com", "https://test1.example.com"];
+ info(window.location);
+
+ const haveDevicesMap = new Promise(resolve => {
+ const map = new Map();
+ window.addEventListener("message", ({origin, data}) => {
+ ok(origins.includes(origin), "Got message from expected origin");
+ map.set(origin, JSON.parse(data));
+ if (map.size < origins.length) return;
+ resolve(map);
+ });
+ });
+
+ await Promise.all(origins.map(origin => {
+ const iframe = document.createElement("iframe");
+ iframe.src = origin + path;
+ iframe.allow = "camera;microphone;speaker-selection";
+ info(iframe.src);
+ document.documentElement.appendChild(iframe);
+ return new Promise(resolve => iframe.onload = resolve);
+ }));
+ let devicesMap = await haveDevicesMap;
+ let [sameOriginDevices, differentOriginDevices] = origins.map(o => devicesMap.get(o));
+
+ is(sameOriginDevices.length, devices.length, "same origin same devices");
+ is(differentOriginDevices.length, devices.length, "cross origin same devices");
+ [...sameOriginDevices, ...differentOriginDevices].forEach(d => validateDevice(d));
+
+ for (const device of sameOriginDevices) {
+ ok(devices.find(d => d.deviceId == device.deviceId),
+ "Same origin deviceId for " + device.label + " must match");
+ }
+ for (const device of differentOriginDevices) {
+ ok(!devices.find(d => d.deviceId == device.deviceId),
+ "Different origin deviceId for " + device.label + " must be different");
+ }
+
+ // Check the special case of no devices found.
+ await pushPrefs(["media.navigator.streams.fake", false],
+ ["media.audio_loopback_dev", "none"],
+ ["media.video_loopback_dev", "none"]);
+ devices = await navigator.mediaDevices.enumerateDevices();
+ is(devices.length, 0, "No devices");
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_enumerateDevices_getUserMediaFake.html b/dom/media/webrtc/tests/mochitests/test_enumerateDevices_getUserMediaFake.html
new file mode 100644
index 0000000000..7952bcba1b
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_enumerateDevices_getUserMediaFake.html
@@ -0,0 +1,63 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <script src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+ <script>
+"use strict";
+
+createHTML({
+ title: "Test labeled devices or speakers aren't exposed in enumerateDevices() after fake getUserMedia()",
+ bug: "1743524"
+});
+
+runTest(async () => {
+ await pushPrefs(
+ ["media.setsinkid.enabled", true],
+ // This test uses real devices because fake devices are not grouped with
+ // audiooutput devices.
+ ["media.navigator.streams.fake", false]);
+ const devices = navigator.mediaDevices;
+ {
+ // `fake:true` means that getUserMedia() resolves without any permission
+ // check, and so this should not be sufficient to expose real device info.
+ const stream = await devices.getUserMedia({ audio: true, fake: true });
+ stream.getTracks()[0].stop();
+ const list = await devices.enumerateDevices();
+ const labeledDevices = list.filter(({label}) => label != "");
+ is(labeledDevices.length, 0, "must be zero labeled devices after fake gUM");
+ const outputDevices = list.filter(({kind}) => kind == "audiooutput");
+ is(outputDevices.length, 0, "must be zero output devices after fake gUM");
+ }
+ {
+ // Check without `fake:true` to verify assumptions about existing devices.
+ let stream;
+ try {
+ stream = await devices.getUserMedia({ audio: true });
+ stream.getTracks()[0].stop();
+ } catch (e) {
+ if (e.name == "NotFoundError" &&
+ navigator.userAgent.includes("Mac OS X")) {
+ todo(false, "Expecting no real audioinput device on Mac test machines");
+ return;
+ }
+ throw e;
+ }
+ {
+ const list = await devices.enumerateDevices();
+ const audioDevices = list.filter(({kind}) => kind.includes("audio"));
+ ok(audioDevices.length, "have audio devices after real gUM");
+ const unlabeledAudioDevices = audioDevices.filter(({label}) => !label);
+ is(unlabeledAudioDevices.length, 0,
+ "must be zero unlabeled audio devices after real gUM");
+
+ const outputDevices = list.filter(({kind}) => kind == "audiooutput");
+ isnot(outputDevices.length, 0, "have output devices after real gUM");
+ }
+ }
+});
+ </script>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_enumerateDevices_iframe.html b/dom/media/webrtc/tests/mochitests/test_enumerateDevices_iframe.html
new file mode 100644
index 0000000000..beea3a4f97
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_enumerateDevices_iframe.html
@@ -0,0 +1,28 @@
+<!DOCTYPE HTML>
+<html>
+<body>
+<pre id="test">
+<script type="application/javascript">
+/**
+ Runs inside iframe in test_enumerateDevices.html.
+*/
+
+const pushPrefs = (...p) => SpecialPowers.pushPrefEnv({set: p});
+const gUM = c => navigator.mediaDevices.getUserMedia(c);
+
+(async () => {
+ await pushPrefs(["media.navigator.streams.fake", true]);
+
+ // Validate enumerated devices after gUM.
+ for (const track of (await gUM({video: true, audio: true})).getTracks()) {
+ track.stop();
+ }
+
+ const devices = await navigator.mediaDevices.enumerateDevices();
+ parent.postMessage(JSON.stringify(devices), "https://example.com:443");
+
+})().catch(e => setTimeout(() => { throw e; }));
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_enumerateDevices_iframe_pre_gum.html b/dom/media/webrtc/tests/mochitests/test_enumerateDevices_iframe_pre_gum.html
new file mode 100644
index 0000000000..f2dc2d1f65
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_enumerateDevices_iframe_pre_gum.html
@@ -0,0 +1,22 @@
+<!DOCTYPE HTML>
+<html>
+<body>
+<pre id="test">
+<script type="application/javascript">
+/**
+ Runs inside iframe in test_enumerateDevices_legacy.html.
+*/
+
+const pushPrefs = (...p) => SpecialPowers.pushPrefEnv({set: p});
+
+(async () => {
+ await pushPrefs(["media.navigator.streams.fake", true]);
+
+ const devices = await navigator.mediaDevices.enumerateDevices();
+ parent.postMessage(JSON.stringify(devices), "https://example.com:443");
+
+})().catch(e => setTimeout(() => { throw e; }));
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_enumerateDevices_legacy.html b/dom/media/webrtc/tests/mochitests/test_enumerateDevices_legacy.html
new file mode 100644
index 0000000000..c599f2b599
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_enumerateDevices_legacy.html
@@ -0,0 +1,147 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({ title: "Run enumerateDevices code", bug: "1046245" });
+/**
+ This is a modified copy of test_enumerateDevices.html testing the
+ enumerateDevices() legacy version and deviceId constraint.
+*/
+
+async function mustSucceedWithStream(msg, f) {
+ try {
+ const stream = await f();
+ for (const track of stream.getTracks()) {
+ track.stop();
+ }
+ ok(true, msg + " must succeed");
+ } catch (e) {
+ is(e.name, null, msg + " must succeed: " + e.message);
+ }
+}
+
+async function mustFailWith(msg, reason, constraint, f) {
+ try {
+ await f();
+ ok(false, msg + " must fail");
+ } catch(e) {
+ is(e.name, reason, msg + " must fail: " + e.message);
+ if (constraint) {
+ is(e.constraint, constraint, msg + " must fail w/correct constraint.");
+ }
+ }
+}
+
+const gUM = c => navigator.mediaDevices.getUserMedia(c);
+
+const kinds = ["videoinput", "audioinput", "audiooutput"];
+
+function validateDevice({kind, label, deviceId, groupId}) {
+ ok(kinds.includes(kind), "Known device kind");
+ is(deviceId.length, 44, "deviceId length id as expected for Firefox");
+ ok(label.length !== undefined, "Device label: " + label);
+ isnot(groupId, "", "groupId must be present.");
+}
+
+runTest(async () => {
+ await pushPrefs(["media.navigator.streams.fake", true],
+ ["media.devices.enumerate.legacy.enabled", true]);
+
+ // Validate enumerated devices before gUM (legacy).
+
+ let devices = await navigator.mediaDevices.enumerateDevices();
+ ok(devices.length, "At least one device found");
+ const jsoned = JSON.parse(JSON.stringify(devices));
+ is(jsoned[0].kind, devices[0].kind, "kind survived serializer");
+ is(jsoned[0].deviceId, devices[0].deviceId, "deviceId survived serializer");
+ for (const device of devices) {
+ validateDevice(device);
+ if (device.kind == "audiooutput") continue;
+ is(device.label, "", "Device label is empty");
+ // Test deviceId constraint
+ let deviceId = device.deviceId;
+ let constraints = (device.kind == "videoinput") ? { video: { deviceId } }
+ : { audio: { deviceId } };
+ let namedDevices;
+ for (const track of (await gUM(constraints)).getTracks()) {
+ is(typeof(track.label), "string", "Track label is a string");
+ isnot(track.label.length, 0, "Track label is not empty");
+ if (!namedDevices) {
+ namedDevices = await navigator.mediaDevices.enumerateDevices();
+ }
+ const namedDevice = namedDevices.find(d => d.deviceId == device.deviceId);
+ is(track.label, namedDevice.label, "Track label is the device label");
+ track.stop();
+ }
+ }
+
+ const unknownId = "unknown9qHr8B0JIbcHlbl9xR+jMbZZ8WyoPfpCXPfc=";
+
+ // Check deviceId failure paths for video.
+
+ await mustSucceedWithStream("unknown plain deviceId on video",
+ () => gUM({ video: { deviceId: unknownId } }));
+ await mustSucceedWithStream("unknown plain deviceId on audio",
+ () => gUM({ audio: { deviceId: unknownId } }));
+ await mustFailWith("unknown exact deviceId on video",
+ "OverconstrainedError", "deviceId",
+ () => gUM({ video: { deviceId: { exact: unknownId } } }));
+ await mustFailWith("unknown exact deviceId on audio",
+ "OverconstrainedError", "deviceId",
+ () => gUM({ audio: { deviceId: { exact: unknownId } } }));
+
+ // Check that deviceIds are stable for same origin and differ across origins.
+
+ const path = "/tests/dom/media/webrtc/tests/mochitests/test_enumerateDevices_iframe_pre_gum.html";
+ const origins = ["https://example.com", "https://test1.example.com"];
+ info(window.location);
+
+ const haveDevicesMap = new Promise(resolve => {
+ const map = new Map();
+ window.addEventListener("message", ({origin, data}) => {
+ ok(origins.includes(origin), "Got message from expected origin");
+ map.set(origin, JSON.parse(data));
+ if (map.size < origins.length) return;
+ resolve(map);
+ });
+ });
+
+ await Promise.all(origins.map(origin => {
+ const iframe = document.createElement("iframe");
+ iframe.src = origin + path;
+ iframe.allow = "camera;microphone;speaker-selection";
+ info(iframe.src);
+ document.documentElement.appendChild(iframe);
+ return new Promise(resolve => iframe.onload = resolve);
+ }));
+ let devicesMap = await haveDevicesMap;
+ let [sameOriginDevices, differentOriginDevices] = origins.map(o => devicesMap.get(o));
+
+ is(sameOriginDevices.length, devices.length, "same origin same devices");
+ is(differentOriginDevices.length, devices.length, "cross origin same devices");
+ [...sameOriginDevices, ...differentOriginDevices].forEach(d => validateDevice(d));
+
+ for (const device of sameOriginDevices) {
+ ok(devices.find(d => d.deviceId == device.deviceId),
+ "Same origin deviceId for " + device.label + " must match");
+ }
+ for (const device of differentOriginDevices) {
+ ok(!devices.find(d => d.deviceId == device.deviceId),
+ "Different origin deviceId for " + device.label + " must be different");
+ }
+
+ // Check the special case of no devices found.
+ await pushPrefs(["media.navigator.streams.fake", false],
+ ["media.audio_loopback_dev", "none"],
+ ["media.video_loopback_dev", "none"]);
+ devices = await navigator.mediaDevices.enumerateDevices();
+ is(devices.length, 0, "No devices");
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_enumerateDevices_navigation.html b/dom/media/webrtc/tests/mochitests/test_enumerateDevices_navigation.html
new file mode 100644
index 0000000000..bf7650223f
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_enumerateDevices_navigation.html
@@ -0,0 +1,54 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<iframe id="iframe" srcdoc="<script>
+ window.enumerateDevices = () =>
+ navigator.mediaDevices.enumerateDevices();
+ </script>"
+ width="100%" height="50%" frameborder="1">
+</iframe>
+<pre id="test">
+<script type="application/javascript">
+createHTML({ title: "Suspend enumerateDevices code ", bug: "1479840" });
+/**
+ This test covers the case that the enumerateDevices method is suspended by
+ navigating away the current window. In order to implement that the enumeration
+ is executed in an iframe which is cleared before the enumeration has been resolved
+*/
+
+runTest(async () => {
+ // Run enumerate devices and mesure the time it will take.
+ const start = new Date().getTime();
+ try {
+ await iframe.contentWindow.enumerateDevices();
+ } catch (e) {
+ info("Failed to enumerate devices, error: " + e);
+ }
+ const elapsed = new Date().getTime() - start;
+
+ // Run again and navigate away. Expected to remain pending.
+ let p = iframe.contentWindow.enumerateDevices()
+ p.then( devices => {
+ ok(false, "Enumerate devices promise resolved unexpectedly, found " + devices.length + " devices.");
+ })
+ .catch ( error => {
+ ok(false, "Enumerate devices promise rejected unexpectedly: " + error);
+ });
+ iframe.srcdoc = "";
+
+ // Wait enough time.
+ try {
+ await timeout(p, 5 * elapsed, "timeout");
+ ok(false, "Enumerate devices promise resolved unexpectedly");
+ } catch (e) {
+ is(e.message, "timeout", "We should time out without enumerateDevices rejecting");
+ }
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_fingerprinting_resistance.html b/dom/media/webrtc/tests/mochitests/test_fingerprinting_resistance.html
new file mode 100644
index 0000000000..7e9cd5a219
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_fingerprinting_resistance.html
@@ -0,0 +1,112 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<script src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<script>
+/* global SimpleTest SpecialPowers */
+
+async function testEnumerateDevices(expectDevices) {
+ let devices = await navigator.mediaDevices.enumerateDevices();
+ if (!expectDevices) {
+ SimpleTest.is(devices.length, 0, "testEnumerateDevices: No devices");
+ return;
+ }
+ let cams = devices.filter((device) => device.kind == "videoinput");
+ let mics = devices.filter((device) => device.kind == "audioinput");
+ SimpleTest.ok((cams.length == 1) && (mics.length == 1),
+ "testEnumerateDevices: a microphone and a camera");
+}
+
+async function testGetUserMedia(expectDevices) {
+ const constraints = [
+ {audio: true},
+ {video: true},
+ {audio: true, video: true},
+ {video: {width: {min: 1e9}}}, // impossible
+ {audio: {channelCount: {exact: 1e3}}}, // impossible
+ ];
+ for (let constraint of constraints) {
+ let message = "getUserMedia(" + JSON.stringify(constraint) + ")";
+ try {
+ let stream = await navigator.mediaDevices.getUserMedia(constraint);
+ SimpleTest.ok(expectDevices, message + " resolved");
+ if (!expectDevices) {
+ continue;
+ }
+
+ // We only do testGetUserMedia(true) when privacy.resistFingerprinting
+ // is true, test if MediaStreamTrack.label is spoofed.
+ for (let track of stream.getTracks()) {
+ switch (track.kind) {
+ case "audio":
+ SimpleTest.is(track.label, "Internal Microphone", "AudioStreamTrack.label");
+ break;
+ case "video":
+ SimpleTest.is(track.label, "Internal Camera", "VideoStreamTrack.label");
+ break;
+ default:
+ SimpleTest.ok(false, "Unknown kind: " + track.kind);
+ break;
+ }
+ track.stop();
+ }
+ } catch (e) {
+ if (!expectDevices) {
+ SimpleTest.is(e.name, "NotAllowedError", message + " throws NotAllowedError");
+ } else {
+ SimpleTest.ok(false, message + " failed: " + e);
+ }
+ }
+ }
+}
+
+async function testDevices() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.resistFingerprinting", true],
+ ["media.navigator.streams.fake", true]
+ ]
+ });
+ await testEnumerateDevices(true); // should list a microphone and a camera
+ await testGetUserMedia(true); // should get audio and video streams
+}
+
+async function testNoDevices() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.resistFingerprinting", false],
+ ["media.navigator.permission.device", false],
+ ["media.navigator.streams.fake", false],
+ ["media.audio_loopback_dev", "foo"],
+ ["media.video_loopback_dev", "bar"]
+ ]
+ });
+ await testEnumerateDevices(false); // should list nothing
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.resistFingerprinting", true]
+ ]
+ });
+ await testEnumerateDevices(true); // should list a microphone and a camera
+ await testGetUserMedia(false); // should reject with NotAllowedError
+}
+
+createHTML({
+ title: "Neutralize the threat of fingerprinting of media devices API when 'privacy.resistFingerprinting' is true",
+ bug: "1372073"
+});
+
+runTest(async () => {
+ // Make sure enumerateDevices and getUserMedia work when
+ // privacy.resistFingerprinting is true.
+ await testDevices();
+
+ // Test that absence of devices can't be detected.
+ await testNoDevices();
+});
+</script>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_forceSampleRate.html b/dom/media/webrtc/tests/mochitests/test_forceSampleRate.html
new file mode 100644
index 0000000000..c5a9820aaa
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_forceSampleRate.html
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test the pref media.cubeb.force_sample_rate</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script class="testbody" type="text/javascript">
+const WEIRD_SAMPLE_RATE = 44101;
+
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPrefEnv({"set": [
+ ["media.cubeb.force_sample_rate", WEIRD_SAMPLE_RATE]
+]}).then(function() {
+ var ac = new AudioContext();
+ is(ac.sampleRate, WEIRD_SAMPLE_RATE, "Forced sample-rate set successfully.");
+ SimpleTest.finish();
+});
+</script>
+</pre>
+</body>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_GC_MediaStream.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_GC_MediaStream.html
new file mode 100644
index 0000000000..5aa0e64947
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_GC_MediaStream.html
@@ -0,0 +1,59 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ "use strict";
+
+ createHTML({
+ title: "MediaStreams can be garbage collected",
+ bug: "1407542"
+ });
+
+ let SpecialStream = SpecialPowers.wrap(MediaStream);
+
+ async function testGC(stream, numCopies, copy) {
+ let startStreams = await SpecialStream.countUnderlyingStreams();
+
+ let copies = new Array(numCopies).fill(0).map(() => copy(stream));
+ ok(await SpecialStream.countUnderlyingStreams() > startStreams,
+ "MediaStreamTrack constructor creates more underlying streams");
+
+ copies = [];
+ await new Promise(r => SpecialPowers.exactGC(r));
+ is(await SpecialStream.countUnderlyingStreams(), startStreams,
+ "MediaStreamTracks should have been collected");
+ }
+
+ runTest(async () => {
+ // We do not need LoopbackTone because it is not used
+ // and creates extra streams that affect the result
+ DISABLE_LOOPBACK_TONE = true;
+
+ let gUMStream = await getUserMedia({video: true});
+ info("Testing GC of track-array constructor with cloned tracks");
+ await testGC(gUMStream, 10, s => new MediaStream(s.getTracks().map(t => t.clone())));
+
+ info("Testing GC of empty constructor plus addTrack with cloned tracks");
+ await testGC(gUMStream, 10, s => {
+ let s2 = new MediaStream();
+ s.getTracks().forEach(t => s2.addTrack(t.clone()));
+ return s2;
+ });
+
+ info("Testing GC of cloned stream");
+ await testGC(gUMStream, 10, s => s.clone());
+
+ info("Testing GC of gUM stream");
+ gUMStream = null;
+ await new Promise(r => SpecialPowers.exactGC(r));
+ is(await SpecialStream.countUnderlyingStreams(), 0,
+ "Original gUM stream should be collectable");
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_active_autoplay.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_active_autoplay.html
new file mode 100644
index 0000000000..c1a39cdd4c
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_active_autoplay.html
@@ -0,0 +1,61 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<video id="testAutoplay" autoplay></video>
+<script type="application/javascript">
+"use strict";
+
+const video = document.getElementById("testAutoplay");
+var stream;
+var otherVideoTrack;
+var otherAudioTrack;
+
+createHTML({
+ title: "MediaStream can be autoplayed in media element after going inactive and then active",
+ bug: "1208316"
+});
+
+runTest(() => getUserMedia({audio: true, video: true}).then(s => {
+ stream = s;
+ otherVideoTrack = stream.getVideoTracks()[0].clone();
+ otherAudioTrack = stream.getAudioTracks()[0].clone();
+
+ video.srcObject = stream;
+ return haveEvent(video, "playing", wait(5000, new Error("Timeout")));
+})
+.then(() => {
+ ok(!video.ended, "Video element should be playing after adding a gUM stream");
+ stream.getTracks().forEach(t => t.stop());
+ return haveEvent(video, "ended", wait(5000, new Error("Timeout")));
+})
+.then(() => {
+ ok(video.ended, "Video element should be ended");
+ stream.addTrack(otherVideoTrack);
+ return haveEvent(video, "playing", wait(5000, new Error("Timeout")));
+})
+.then(() => {
+ ok(!video.ended, "Video element should be playing after adding a video track");
+ stream.getTracks().forEach(t => t.stop());
+ return haveEvent(video, "ended", wait(5000, new Error("Timeout")));
+})
+.then(() => {
+ ok(video.ended, "Video element should be ended");
+ stream.addTrack(otherAudioTrack);
+ return haveEvent(video, "playing", wait(5000, new Error("Timeout")));
+})
+.then(() => {
+ ok(!video.ended, "Video element should be playing after adding a audio track");
+ stream.getTracks().forEach(t => t.stop());
+ return haveEvent(video, "ended", wait(5000, new Error("Timeout")));
+})
+.then(() => {
+ ok(video.ended, "Video element should be ended");
+}));
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_addTrackRemoveTrack.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_addTrackRemoveTrack.html
new file mode 100644
index 0000000000..27dad2519f
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_addTrackRemoveTrack.html
@@ -0,0 +1,169 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ "use strict";
+
+ createHTML({
+ title: "MediaStream's addTrack() and removeTrack() with getUserMedia streams Test",
+ bug: "1103188"
+ });
+
+ runTest(() => Promise.resolve()
+ .then(() => getUserMedia({audio: true})).then(stream =>
+ getUserMedia({video: true}).then(otherStream => {
+ info("Test addTrack()ing a video track to an audio-only gUM stream");
+ var track = stream.getTracks()[0];
+ var otherTrack = otherStream.getTracks()[0];
+
+ stream.addTrack(track);
+ checkMediaStreamContains(stream, [track], "Re-added audio");
+
+ stream.addTrack(otherTrack);
+ checkMediaStreamContains(stream, [track, otherTrack], "Added video");
+
+ var testElem = createMediaElement('video', 'testAddTrackAudioVideo');
+ var playback = new MediaStreamPlayback(testElem, stream);
+ return playback.playMedia(false);
+ }))
+ .then(() => getUserMedia({video: true})).then(stream =>
+ getUserMedia({video: true}).then(otherStream => {
+ info("Test addTrack()ing a video track to a video-only gUM stream");
+ var track = stream.getTracks()[0];
+ var otherTrack = otherStream.getTracks()[0];
+
+ stream.addTrack(track);
+ checkMediaStreamContains(stream, [track], "Re-added video");
+
+ stream.addTrack(otherTrack);
+ checkMediaStreamContains(stream, [track, otherTrack], "Added video");
+
+ var test = createMediaElement('video', 'testAddTrackDoubleVideo');
+ var playback = new MediaStreamPlayback(test, stream);
+ return playback.playMedia(false);
+ }))
+ .then(() => getUserMedia({video: true})).then(stream =>
+ getUserMedia({video: true}).then(otherStream => {
+ info("Test removeTrack() existing and added video tracks from a video-only gUM stream");
+ var track = stream.getTracks()[0];
+ var otherTrack = otherStream.getTracks()[0];
+
+ stream.removeTrack(otherTrack);
+ checkMediaStreamContains(stream, [track], "Removed non-existing video");
+
+ stream.addTrack(otherTrack);
+ checkMediaStreamContains(stream, [track, otherTrack], "Added video");
+
+ stream.removeTrack(otherTrack);
+ checkMediaStreamContains(stream, [track], "Removed added video");
+
+ stream.removeTrack(otherTrack);
+ checkMediaStreamContains(stream, [track], "Re-removed added video");
+
+ stream.removeTrack(track);
+ checkMediaStreamContains(stream, [], "Removed original video");
+
+ var elem = createMediaElement('video', 'testRemoveAllVideo');
+ var loadeddata = false;
+ elem.onloadeddata = () => { loadeddata = true; elem.onloadeddata = null; };
+ elem.srcObject = stream;
+ elem.play();
+ return wait(500).then(() => {
+ ok(!loadeddata, "Stream without tracks shall not raise 'loadeddata' on media element");
+ elem.pause();
+ elem.srcObject = null;
+ })
+ .then(() => {
+ stream.addTrack(track);
+ checkMediaStreamContains(stream, [track], "Re-added added-then-removed track");
+ var playback = new MediaStreamPlayback(elem, stream);
+ return playback.playMedia(false);
+ })
+ .then(() => otherTrack.stop());
+ }))
+ .then(() => getUserMedia({ audio: true })).then(audioStream =>
+ getUserMedia({ video: true }).then(videoStream => {
+ info("Test adding track and removing the original");
+ var audioTrack = audioStream.getTracks()[0];
+ var videoTrack = videoStream.getTracks()[0];
+ videoStream.removeTrack(videoTrack);
+ audioStream.addTrack(videoTrack);
+
+ checkMediaStreamContains(videoStream, [], "1, Removed original track");
+ checkMediaStreamContains(audioStream, [audioTrack, videoTrack],
+ "2, Added external track");
+
+ var elem = createMediaElement('video', 'testAddRemoveOriginalTrackVideo');
+ var playback = new MediaStreamPlayback(elem, audioStream);
+ return playback.playMedia(false);
+ }))
+ .then(() => getUserMedia({ audio: true, video: true })).then(stream => {
+ info("Test removing stopped tracks");
+ stream.getTracks().forEach(t => {
+ t.stop();
+ stream.removeTrack(t);
+ });
+ checkMediaStreamContains(stream, [], "Removed stopped tracks");
+ })
+ .then(() => {
+ var ac = new AudioContext();
+
+ var osc1k = createOscillatorStream(ac, 1000);
+ var audioTrack1k = osc1k.getTracks()[0];
+
+ var osc5k = createOscillatorStream(ac, 5000);
+ var audioTrack5k = osc5k.getTracks()[0];
+
+ var osc10k = createOscillatorStream(ac, 10000);
+ var audioTrack10k = osc10k.getTracks()[0];
+
+ var stream = osc1k;
+ return Promise.resolve().then(() => {
+ info("Analysing audio output with original 1k track");
+ var analyser = new AudioStreamAnalyser(ac, stream);
+ return analyser.waitForAnalysisSuccess(array =>
+ array[analyser.binIndexForFrequency(50)] < 50 &&
+ array[analyser.binIndexForFrequency(1000)] > 200 &&
+ array[analyser.binIndexForFrequency(5000)] < 50 &&
+ array[analyser.binIndexForFrequency(10000)] < 50);
+ }).then(() => {
+ info("Analysing audio output with removed original 1k track and added 5k track");
+ stream.removeTrack(audioTrack1k);
+ stream.addTrack(audioTrack5k);
+ var analyser = new AudioStreamAnalyser(ac, stream);
+ return analyser.waitForAnalysisSuccess(array =>
+ array[analyser.binIndexForFrequency(1000)] < 50 &&
+ array[analyser.binIndexForFrequency(5000)] > 200 &&
+ array[analyser.binIndexForFrequency(10000)] < 50);
+ }).then(() => {
+ info("Analysing audio output with removed 5k track and added 10k track");
+ stream.removeTrack(audioTrack5k);
+ stream.addTrack(audioTrack10k);
+ var analyser = new AudioStreamAnalyser(ac, stream);
+ return analyser.waitForAnalysisSuccess(array =>
+ array[analyser.binIndexForFrequency(1000)] < 50 &&
+ array[analyser.binIndexForFrequency(5000)] < 50 &&
+ array[analyser.binIndexForFrequency(10000)] > 200);
+ }).then(() => {
+ info("Analysing audio output with re-added 1k, 5k and added 10k tracks");
+ stream.addTrack(audioTrack1k);
+ stream.addTrack(audioTrack5k);
+ var analyser = new AudioStreamAnalyser(ac, stream);
+ return analyser.waitForAnalysisSuccess(array =>
+ array[analyser.binIndexForFrequency(50)] < 50 &&
+ array[analyser.binIndexForFrequency(1000)] > 200 &&
+ array[analyser.binIndexForFrequency(2500)] < 50 &&
+ array[analyser.binIndexForFrequency(5000)] > 200 &&
+ array[analyser.binIndexForFrequency(7500)] < 50 &&
+ array[analyser.binIndexForFrequency(10000)] > 200 &&
+ array[analyser.binIndexForFrequency(11000)] < 50);
+ });
+ }));
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_addtrack_removetrack_events.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_addtrack_removetrack_events.html
new file mode 100644
index 0000000000..833653ebb2
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_addtrack_removetrack_events.html
@@ -0,0 +1,110 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+"use strict";
+
+createHTML({
+ title: "MediaStream's 'addtrack' and 'removetrack' events shouldn't fire on manual operations",
+ bug: "1208328"
+});
+
+var spinEventLoop = () => new Promise(r => setTimeout(r, 0));
+
+var stream;
+var clone;
+var newStream;
+var tracks = [];
+
+var addTrack = track => {
+ info("Adding track " + track.id);
+ stream.addTrack(track);
+};
+var removeTrack = track => {
+ info("Removing track " + track.id);
+ stream.removeTrack(track);
+};
+var stopTrack = track => {
+ if (track.readyState == "live") {
+ info("Stopping track " + track.id);
+ }
+ track.stop();
+};
+
+runTest(() => getUserMedia({audio: true, video: true})
+ .then(s => {
+ stream = s;
+ clone = s.clone();
+ stream.addEventListener("addtrack", function onAddtrack(event) {
+ ok(false, "addtrack fired unexpectedly for track " + event.track.id);
+ });
+ stream.addEventListener("removetrack", function onRemovetrack(event) {
+ ok(false, "removetrack fired unexpectedly for track " + event.track.id);
+ });
+
+ return getUserMedia({audio: true, video: true});
+ })
+ .then(s => {
+ newStream = s;
+ info("Stopping an original track");
+ stopTrack(stream.getTracks()[0]);
+
+ return spinEventLoop();
+ })
+ .then(() => {
+ info("Removing original tracks");
+ stream.getTracks().forEach(t => (stream.removeTrack(t), tracks.push(t)));
+
+ return spinEventLoop();
+ })
+ .then(() => {
+ info("Adding other gUM tracks");
+ newStream.getTracks().forEach(t => addTrack(t))
+
+ return spinEventLoop();
+ })
+ .then(() => {
+ info("Adding cloned tracks");
+ let clone = stream.clone();
+ clone.getTracks().forEach(t => addTrack(t));
+
+ return spinEventLoop();
+ })
+ .then(() => {
+ info("Removing a clone");
+ removeTrack(clone.getTracks()[0]);
+
+ return spinEventLoop();
+ })
+ .then(() => {
+ info("Stopping clones");
+ clone.getTracks().forEach(t => stopTrack(t));
+
+ return spinEventLoop();
+ })
+ .then(() => {
+ info("Stopping originals");
+ stream.getTracks().forEach(t => stopTrack(t));
+ tracks.forEach(t => stopTrack(t));
+
+ return spinEventLoop();
+ })
+ .then(() => {
+ info("Removing remaining tracks");
+ stream.getTracks().forEach(t => removeTrack(t));
+
+ return spinEventLoop();
+ })
+ .then(() => {
+ // Test MediaStreamTrackEvent required args here.
+ mustThrowWith("MediaStreamTrackEvent without required args",
+ "TypeError", () => new MediaStreamTrackEvent("addtrack", {}));
+ }));
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_audioCapture.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_audioCapture.html
new file mode 100644
index 0000000000..2cc649a321
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_audioCapture.html
@@ -0,0 +1,104 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test AudioCapture </title>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script>
+
+(async () => {
+ // Get an opus file containing a sine wave at maximum amplitude, of duration
+ // `lengthSeconds`, and of frequency `frequency`.
+ async function getSineWaveFile(frequency, lengthSeconds) {
+ const off = new OfflineAudioContext(1, lengthSeconds * 48000, 48000);
+ const osc = off.createOscillator();
+ const rec = new MediaRecorder(off.destination,
+ {mimeType: "audio/ogg; codecs=opus"});
+ osc.frequency.value = frequency;
+ osc.connect(off.destination);
+ osc.start();
+ rec.start();
+ off.startRendering();
+ const {data} = await new Promise(r => rec.ondataavailable = r);
+ return data;
+ }
+
+ await createHTML({
+ bug: "1156472",
+ title: "Test AudioCapture with regular HTMLMediaElement, AudioContext, " +
+ "and HTMLMediaElement playing a MediaStream",
+ visible: true
+ });
+
+ await runTestWhenReady(async () => {
+ /**
+ * Get two HTMLMediaElements:
+ * - One playing a sine tone from a blob (of an opus file created on the fly)
+ * - One being the output for an AudioContext's OscillatorNode, connected to
+ * a MediaSourceDestinationNode.
+ *
+ * Also, use the AudioContext playing through its AudioDestinationNode another
+ * tone, using another OscillatorNode.
+ *
+ * Capture the output of the document, feed that back into the AudioContext,
+ * with an AnalyserNode, and check the frequency content to make sure we
+ * have recorded the three sources.
+ *
+ * The three sine tones have frequencies far apart from each other, so that we
+ * can check that the spectrum of the capture stream contains three
+ * components with a high magnitude.
+ */
+ const wavtone = createMediaElement("audio", "WaveTone");
+ const acTone = createMediaElement("audio", "audioContextTone");
+ const ac = new AudioContext();
+
+ const oscThroughMediaElement = ac.createOscillator();
+ oscThroughMediaElement.frequency.value = 1000;
+ const oscThroughAudioDestinationNode = ac.createOscillator();
+ oscThroughAudioDestinationNode.frequency.value = 5000;
+ const msDest = ac.createMediaStreamDestination();
+
+ oscThroughMediaElement.connect(msDest);
+ oscThroughAudioDestinationNode.connect(ac.destination);
+
+ acTone.srcObject = msDest.stream;
+
+ const blob = await getSineWaveFile(10000, 10);
+ wavtone.src = URL.createObjectURL(blob);
+ oscThroughMediaElement.start();
+ oscThroughAudioDestinationNode.start();
+ wavtone.loop = true;
+ wavtone.play();
+ acTone.play();
+
+ const constraints = {audio: {mediaSource: "audioCapture"}};
+
+ const stream = await getUserMedia(constraints);
+ try {
+ const analyser = new AudioStreamAnalyser(ac, stream);
+ analyser.enableDebugCanvas();
+ await analyser.waitForAnalysisSuccess(array => {
+ // We want to find three frequency components here, around 1000, 5000
+ // and 10000Hz. Frequency are logarithmic. Also make sure we have low
+ // energy in between, not just a flat white noise.
+ return (array[analyser.binIndexForFrequency(50)] < 50 &&
+ array[analyser.binIndexForFrequency(1000)] > 200 &&
+ array[analyser.binIndexForFrequency(2500)] < 50 &&
+ array[analyser.binIndexForFrequency(5000)] > 200 &&
+ array[analyser.binIndexForFrequency(7500)] < 50 &&
+ array[analyser.binIndexForFrequency(10000)] > 200);
+ });
+ } finally {
+ for (let t of stream.getTracks()) {
+ t.stop();
+ }
+ ac.close();
+ }
+ });
+})();
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_audioConstraints.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_audioConstraints.html
new file mode 100644
index 0000000000..162e83063a
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_audioConstraints.html
@@ -0,0 +1,93 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+"use strict";
+
+createHTML({
+ title: "Test that microphone getSettings report correct settings after applyConstraints",
+ bug: "1447982",
+});
+
+function testTrackAgainstAudioConstraints(track, audioConstraints) {
+ let constraints = track.getConstraints();
+ is(constraints.autoGainControl, audioConstraints.autoGainControl,
+ "Should report correct autoGainControl constraint");
+ is(constraints.echoCancellation, audioConstraints.echoCancellation,
+ "Should report correct echoCancellation constraint");
+ is(constraints.noiseSuppression, audioConstraints.noiseSuppression,
+ "Should report correct noiseSuppression constraint");
+
+ let settings = track.getSettings();
+ is(settings.autoGainControl, audioConstraints.autoGainControl,
+ "Should report correct autoGainControl setting");
+ is(settings.echoCancellation, audioConstraints.echoCancellation,
+ "Should report correct echoCancellation setting");
+ is(settings.noiseSuppression, audioConstraints.noiseSuppression,
+ "Should report correct noiseSuppression setting");
+}
+
+async function testAudioConstraints(track, audioConstraints) {
+ // We applyConstraints() first and do a fresh gUM later, to avoid
+ // testing multiple concurrent captures at different settings.
+
+ info(`Testing applying constraints ${JSON.stringify(audioConstraints)} ` +
+ `to track with settings ${JSON.stringify(track.getSettings())}`);
+ await track.applyConstraints(audioConstraints);
+ testTrackAgainstAudioConstraints(track, audioConstraints);
+
+ info("Testing fresh gUM request with audio constraints " +
+ JSON.stringify(audioConstraints));
+ let stream = await getUserMedia({audio: audioConstraints});
+ testTrackAgainstAudioConstraints(stream.getTracks()[0], audioConstraints);
+ stream.getTracks().forEach(t => t.stop());
+}
+
+runTest(async () => {
+ let audioDevice = SpecialPowers.getCharPref("media.audio_loopback_dev", "");
+ if (!audioDevice) {
+ ok(false, "No device set by framework. Try --use-test-media-devices");
+ return;
+ }
+
+ let supportedConstraints = navigator.mediaDevices.getSupportedConstraints();
+ is(supportedConstraints.autoGainControl, true,
+ "autoGainControl constraint should be supported");
+ is(supportedConstraints.echoCancellation, true,
+ "echoCancellation constraint should be supported");
+ is(supportedConstraints.noiseSuppression, true,
+ "noiseSuppression constraint should be supported");
+
+ let egn = (e, g, n) => ({
+ echoCancellation: e,
+ autoGainControl: g,
+ noiseSuppression: n
+ });
+
+ let stream = await getUserMedia({
+ audio: egn(true, true, true),
+ });
+ let track = stream.getTracks()[0];
+ let audioConstraintsToTest = [
+ egn(false, true, true),
+ egn(true, false, true),
+ egn(true, true, false),
+ egn(false, false, true),
+ egn(false, true, false),
+ egn(true, false, false),
+ egn(false, false, false),
+ egn(true, true, true),
+ ];
+ for (let audioConstraints of audioConstraintsToTest) {
+ await testAudioConstraints(track, audioConstraints);
+ }
+ track.stop();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_audioConstraints_concurrentIframes.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_audioConstraints_concurrentIframes.html
new file mode 100644
index 0000000000..d07dbc41f1
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_audioConstraints_concurrentIframes.html
@@ -0,0 +1,157 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+ title: "getUserMedia in multiple iframes with different constraints",
+ bug: "1404977"
+});
+/**
+ * Verify that we can successfully call getUserMedia for the same device in
+ * multiple iframes concurrently. This is checked by creating a number of
+ * iframes and performing a separate getUserMedia call in each. We verify the
+ * stream returned by that call has the same constraints as requested both
+ * immediately after the call and after all gUM calls have been made. The test
+ * then verifies the streams can be played.
+ */
+runTest(async function() {
+ // Compare constraints and return a string with the differences in
+ // echoCancellation, autoGainControl, and noiseSuppression. The string
+ // will be empty if there are no differences.
+ function getConstraintDifferenceString(constraints, otherConstraints) {
+ let diffString = "";
+ if (constraints.echoCancellation != otherConstraints.echoCancellation) {
+ diffString += "echoCancellation different: " +
+ `${constraints.echoCancellation} != ${otherConstraints.echoCancellation}, `;
+ }
+ if (constraints.autoGainControl != otherConstraints.autoGainControl) {
+ diffString += "autoGainControl different: " +
+ `${constraints.autoGainControl} != ${otherConstraints.autoGainControl}, `;
+ }
+ if (constraints.noiseSuppression != otherConstraints.noiseSuppression) {
+ diffString += "noiseSuppression different: " +
+ `${constraints.noiseSuppression} != ${otherConstraints.noiseSuppression}, `;
+ }
+ // Replace trailing comma and space if any
+ return diffString.replace(/, $/, "");
+ }
+
+ // We need a real device to get a MediaEngine supporting constraints
+ let audioDevice = SpecialPowers.getCharPref("media.audio_loopback_dev", "");
+ if (!audioDevice) {
+ todo(false, "No device set by framework. Try --use-test-media-devices");
+ return;
+ }
+
+ let egn = (e, g, n) => ({
+ echoCancellation: e,
+ autoGainControl: g,
+ noiseSuppression: n
+ });
+
+ let allConstraintCombinations = [
+ egn(false, false, false),
+ egn(true, false, false),
+ egn(false, true, false),
+ egn(false, false, true),
+ egn(true, true, false),
+ egn(true, false, true),
+ egn(false, true, true),
+ egn(true, true, true),
+ ];
+
+ // TODO: We would like to be able to perform an arbitrary number of gUM calls
+ // at once, but issues with pulse and audio IPC mean on some systems we're
+ // limited to as few as 2 concurrent calls. To avoid issues we chunk test runs
+ // to only two calls at a time. The while, splice and GC lines can be removed,
+ // the extra scope removed and allConstraintCombinations can be renamed to
+ // constraintCombinations once this issue is resolved. See bug 1480489.
+ while (allConstraintCombinations.length) {
+ {
+ let constraintCombinations = allConstraintCombinations.splice(0, 2);
+ // Array to store objects that associate information used in our test such as
+ // constraints, iframes, gum streams, and various promises.
+ let testCases = [];
+
+ for (let constraints of constraintCombinations) {
+ let testCase = {requestedConstraints: constraints};
+ // Provide an id for logging, labeling related elements.
+ testCase.id = `testCase.` +
+ `e=${constraints.echoCancellation}.` +
+ `g=${constraints.noiseSuppression}.` +
+ `n=${constraints.noiseSuppression}`;
+ testCases.push(testCase);
+ testCase.iframe = document.createElement("iframe");
+ testCase.iframeLoadedPromise = new Promise((resolve, reject) => {
+ testCase.iframe.onload = () => { resolve(); };
+ });
+ document.body.appendChild(testCase.iframe);
+ }
+ is(testCases.length,
+ constraintCombinations.length,
+ "Should have created a testcase for each constraint");
+
+ // Wait for all iframes to be loaded
+ await Promise.all(testCases.map(tc => tc.iframeLoadedPromise));
+
+ // Start a tone at our top level page so the gUM calls will record something
+ // should we wish to verify their recording in future.
+ let tone = new LoopbackTone(new AudioContext, TEST_AUDIO_FREQ);
+ tone.start();
+
+ // One by one see if we can grab a gUM stream per iframe
+ for (let testCase of testCases) {
+ // Use normal gUM rather than our test helper as the test harness was
+ // not made to be used inside iframes.
+ testCase.gumStream =
+ await testCase.iframe.contentWindow.navigator.mediaDevices.getUserMedia({audio: testCase.requestedConstraints})
+ .catch(e => Promise.reject(`getUserMedia calls should not fail! Failed at ${testCase.id} with: ${e}!`));
+ let differenceString = getConstraintDifferenceString(
+ testCase.requestedConstraints,
+ testCase.gumStream.getAudioTracks()[0].getSettings());
+ ok(!differenceString,
+ `gUM stream for ${testCase.id} should have the same constraints as were ` +
+ `requested from gUM. Differences: ${differenceString}`);
+ }
+
+ // Once all streams are collected, make sure the constraints haven't been
+ // mutated by another gUM call.
+ for (let testCase of testCases) {
+ let differenceString = getConstraintDifferenceString(
+ testCase.requestedConstraints,
+ testCase.gumStream.getAudioTracks()[0].getSettings());
+ ok(!differenceString,
+ `gUM stream for ${testCase.id} should not have had constraints altered after ` +
+ `all gUM calls are done. Differences: ${differenceString}`);
+ }
+
+ // We do not currently have tests to verify the behaviour of the different
+ // constraints. Once we do we should do further verification here. See
+ // bug 1406372, bug 1406376, and bug 1406377.
+
+ for (let testCase of testCases) {
+ let testAudio = createMediaElement("audio", `testAudio.${testCase.id}`);
+ let playback = new MediaStreamPlayback(testAudio, testCase.gumStream);
+ await playback.playMediaWithoutStoppingTracks(false);
+ }
+
+ // Stop the tracks for each stream, we left them running above via
+ // playMediaWithoutStoppingTracks to make sure they can play concurrently.
+ for (let testCase of testCases) {
+ testCase.gumStream.getTracks().map(t => t.stop());
+ document.body.removeChild(testCase.iframe);
+ }
+
+ tone.stop();
+ }
+ await new Promise(r => SpecialPowers.exactGC(r));
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_audioConstraints_concurrentStreams.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_audioConstraints_concurrentStreams.html
new file mode 100644
index 0000000000..f5b5e784ea
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_audioConstraints_concurrentStreams.html
@@ -0,0 +1,123 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+ title: "getUserMedia multiple times, concurrently, and with different constraints",
+ bug: "1404977"
+});
+/**
+ * Verify that we can successfully call getUserMedia multiple times for the
+ * same device, concurrently. This is checked by calling getUserMedia a number
+ * of times with different constraints. We verify that the stream returned by
+ * that call has the same constraints as requested both immediately after the
+ * call and after all gUM calls have been made. The test then verifies the
+ * streams can be played.
+ */
+runTest(async function() {
+ // Compare constraints and return a string with the differences in
+ // echoCancellation, autoGainControl, and noiseSuppression. The string
+ // will be empty if there are no differences.
+ function getConstraintDifferenceString(constraints, otherConstraints) {
+ let diffString = "";
+ if (constraints.echoCancellation != otherConstraints.echoCancellation) {
+ diffString += "echoCancellation different: " +
+ `${constraints.echoCancellation} != ${otherConstraints.echoCancellation}, `;
+ }
+ if (constraints.autoGainControl != otherConstraints.autoGainControl) {
+ diffString += "autoGainControl different: " +
+ `${constraints.autoGainControl} != ${otherConstraints.autoGainControl}, `;
+ }
+ if (constraints.noiseSuppression != otherConstraints.noiseSuppression) {
+ diffString += "noiseSuppression different: " +
+ `${constraints.noiseSuppression} != ${otherConstraints.noiseSuppression}, `;
+ }
+ // Replace trailing comma and space if any
+ return diffString.replace(/, $/, "");
+ }
+
+ // We need a real device to get a MediaEngine supporting constraints
+ let audioDevice = SpecialPowers.getCharPref("media.audio_loopback_dev", "");
+ if (!audioDevice) {
+ todo(false, "No device set by framework. Try --use-test-media-devices");
+ return;
+ }
+
+ let egn = (e, g, n) => ({
+ echoCancellation: e,
+ autoGainControl: g,
+ noiseSuppression: n
+ });
+
+ let constraintCombinations = [
+ egn(false, false, false),
+ egn(true, false, false),
+ egn(false, true, false),
+ egn(false, false, true),
+ egn(true, true, false),
+ egn(true, false, true),
+ egn(false, true, true),
+ egn(true, true, true),
+ ];
+
+ // Array to store objects that associate information used in our test such as
+ // constraints, gum streams, and various promises.
+ let testCases = [];
+
+ for (let constraints of constraintCombinations) {
+ let testCase = {requestedConstraints: constraints};
+ // Provide an id for logging, labeling related elements.
+ testCase.id = `testCase.` +
+ `e=${constraints.echoCancellation}.` +
+ `g=${constraints.noiseSuppression}.` +
+ `n=${constraints.noiseSuppression}`;
+ testCases.push(testCase);
+ testCase.gumStream =
+ await getUserMedia({audio: testCase.requestedConstraints})
+ .catch(e => Promise.reject(`getUserMedia calls should not fail! Failed at ${testCase.id} with: ${e}!`));
+ let differenceString = getConstraintDifferenceString(
+ testCase.requestedConstraints,
+ testCase.gumStream.getAudioTracks()[0].getSettings());
+ ok(!differenceString,
+ `gUM stream for ${testCase.id} should have the same constraints as were ` +
+ `requested from gUM. Differences: ${differenceString}`);
+ }
+ is(testCases.length,
+ constraintCombinations.length,
+ "Should have a stream for each constraint");
+
+ // Once all streams are collected, make sure the constraints haven't been
+ // mutated by another gUM call.
+ for (let testCase of testCases) {
+ let differenceString = getConstraintDifferenceString(
+ testCase.requestedConstraints,
+ testCase.gumStream.getAudioTracks()[0].getSettings());
+ ok(!differenceString,
+ `gUM stream for ${testCase.id} should not have had constraints altered after ` +
+ `all gUM calls are done. Differences: ${differenceString}`);
+ }
+
+ // We do not currently have tests to verify the behaviour of the different
+ // constraints. Once we do we should do further verificaiton here. See
+ // bug 1406372, bug 1406376, and bug 1406377.
+
+ for (let testCase of testCases) {
+ let testAudio = createMediaElement("audio", `testAudio.${testCase.id}`);
+ let playback = new MediaStreamPlayback(testAudio, testCase.gumStream);
+ await playback.playMediaWithoutStoppingTracks(false);
+ }
+
+ // Stop the tracks for each stream, we left them running above via
+ // playMediaWithoutStoppingTracks to make sure they can play concurrently.
+ for (let testCase of testCases) {
+ testCase.gumStream.getTracks().map(t => t.stop());
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_basicAudio.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_basicAudio.html
new file mode 100644
index 0000000000..b4775b4244
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_basicAudio.html
@@ -0,0 +1,27 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({ title: "getUserMedia Basic Audio Test", bug: "781534" });
+ /**
+ * Run a test to verify that we can complete a start and stop media playback
+ * cycle for an audio MediaStream on an audio HTMLMediaElement.
+ */
+ runTest(function () {
+ var testAudio = createMediaElement('audio', 'testAudio');
+ var constraints = {audio: true};
+
+ return getUserMedia(constraints).then(stream => {
+ var playback = new MediaStreamPlayback(testAudio, stream);
+ return playback.playMedia(false);
+ });
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_basicAudio_loopback.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_basicAudio_loopback.html
new file mode 100644
index 0000000000..10bf669c00
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_basicAudio_loopback.html
@@ -0,0 +1,99 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+
+<script>
+ createHTML({
+ title: "getUserMedia Basic Audio Test Loopback",
+ bug: "1406350",
+ visible: true
+ });
+
+ /**
+ * Run a test to verify the use of LoopbackTone as audio input.
+ */
+ runTest(async () => {
+ if (!SpecialPowers.getCharPref("media.audio_loopback_dev", "")) {
+ todo(false, "No loopback device set by framework. Try --use-test-media-devices");
+ return;
+ }
+
+ // At this point DefaultLoopbackTone has been instantiated
+ // automatically on frequency TEST_AUDIO_FREQ (440 Hz). Verify
+ // that a tone is detected on that frequency.
+ info("Capturing at default frequency");
+ const stream = await getUserMedia({audio: true});
+
+ try {
+ const audioContext = new AudioContext();
+ const analyser = new AudioStreamAnalyser(audioContext, stream);
+ analyser.enableDebugCanvas();
+ await analyser.waitForAnalysisSuccess(array => {
+ // High energy on 1000 Hz low energy around that
+ const freg_50Hz = array[analyser.binIndexForFrequency(50)];
+ const freq = array[analyser.binIndexForFrequency(TEST_AUDIO_FREQ)];
+ const freq_2000Hz = array[analyser.binIndexForFrequency(2000)];
+
+ info("Analysing audio frequency - low:target:high = "
+ + freg_50Hz + ':' + freq + ':' + freq_2000Hz);
+ return freg_50Hz < 50 && freq > 200 && freq_2000Hz < 50;
+ });
+
+ // Use the LoopbackTone API to change the frequency of the default tone.
+ // Verify that a tone is detected on the new frequency (800 Hz).
+ info("Change loopback tone frequency");
+ DefaultLoopbackTone.changeFrequency(800);
+ await analyser.waitForAnalysisSuccess(array => {
+ const freg_50Hz = array[analyser.binIndexForFrequency(50)];
+ const freq = array[analyser.binIndexForFrequency(800)];
+ const freq_2000Hz = array[analyser.binIndexForFrequency(2000)];
+
+ info("Analysing audio frequency - low:target:high = "
+ + freg_50Hz + ':' + freq + ':' + freq_2000Hz);
+ return freg_50Hz < 50 && freq > 200 && freq_2000Hz < 50;
+ });
+
+ // Create a second tone at a different frequency.
+ // Verify that both tones are detected.
+ info("Multiple loopback tones");
+ DefaultLoopbackTone.changeFrequency(TEST_AUDIO_FREQ);
+ const second_tone = new LoopbackTone(audioContext, 2000);
+ second_tone.start();
+ await analyser.waitForAnalysisSuccess(array => {
+ const freg_50Hz = array[analyser.binIndexForFrequency(50)];
+ const freq = array[analyser.binIndexForFrequency(TEST_AUDIO_FREQ)];
+ const freq_2000Hz = array[analyser.binIndexForFrequency(2000)];
+ const freq_4000Hz = array[analyser.binIndexForFrequency(4000)];
+
+ info("Analysing audio frequency - low:target1:target2:high = "
+ + freg_50Hz + ':' + freq + ':' + freq_2000Hz + ':' + freq_4000Hz);
+ return freg_50Hz < 50 && freq > 200 && freq_2000Hz > 200 && freq_4000Hz < 50;
+ });
+
+ // Stop all tones and verify that there is no audio on the given frequencies.
+ info("Stop all loopback tones");
+ DefaultLoopbackTone.stop();
+ second_tone.stop()
+ await analyser.waitForAnalysisSuccess(array => {
+ const freg_50Hz = array[analyser.binIndexForFrequency(50)];
+ const freq = array[analyser.binIndexForFrequency(TEST_AUDIO_FREQ)];
+ const freq_2000Hz = array[analyser.binIndexForFrequency(2000)];
+
+ info("Analysing audio frequency - low:target:high = "
+ + freg_50Hz + ':' + freq + ':' + freq_2000Hz);
+ return freg_50Hz < 50 && freq < 50 && freq_2000Hz < 50;
+ });
+ } finally {
+ for (let t of stream.getTracks()) {
+ t.stop();
+ }
+ }
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_basicScreenshare.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_basicScreenshare.html
new file mode 100644
index 0000000000..cc73de77da
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_basicScreenshare.html
@@ -0,0 +1,260 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+ <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ title: "getUserMedia Basic Screenshare Test",
+ bug: "1211656",
+ visible: true,
+ });
+
+ const {AppConstants} =
+ SpecialPowers.ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
+
+ // Since the MacOS backend captures in the wrong rgb color profile we need
+ // large thresholds there, and they vary greatly by color. We define
+ // thresholds per platform and color here to still allow the test to run.
+ // Since the colors used (red, green, blue, white) consist only of "pure"
+ // components (0 or 255 for each component), the high thresholds on Mac will
+ // still be able to catch an error where the image is corrupt, or if frames
+ // don't flow.
+ const thresholds = {
+ // macos: captures in the display rgb color profile, which we treat as
+ // sRGB, which is most likely wrong. These thresholds are needed currently
+ // in CI. See bug 1827606.
+ "macosx": { "red": 120, "green": 135, "blue": 35, "white": 10 },
+ // windows: rounding errors in 1) conversion to I420 (the capture),
+ // 2) downscaling, 3) conversion to RGB (for rendering).
+ "win": { "red": 5, "green": 5, "blue": 10, "white": 5 },
+ // linux: rounding errors in 1) conversion to I420 (the capture),
+ // 2) downscaling, 3) conversion to RGB (for rendering).
+ "linux": { "red": 5, "green": 5, "blue": 10, "white": 5 },
+ // android: we don't have a screen capture backend.
+ "android": { "red": 0, "green": 0, "blue": 0, "white": 0 },
+ // other: here just because it's supported by AppConstants.platform.
+ "other": { "red": 0, "green": 0, "blue": 0, "white": 0 },
+ };
+
+ const verifyScreenshare =
+ async (video, helper, upleft, upright, downleft, downright) => {
+ if (video.readyState < video.HAVE_CURRENT_DATA) {
+ info("Waiting for data");
+ await new Promise(r => video.onloadeddata = r);
+ }
+
+ // We assume video size will not change. Offsets help to account for a
+ // square fullscreen-canvas, while the screen is rectangular.
+ const offsetX = Math.max(0, video.videoWidth - video.videoHeight) / 2;
+ const offsetY = Math.max(0, video.videoHeight - video.videoWidth) / 2;
+
+ const verifyAround = async (internalX, internalY, color) => {
+ // Pick a couple of samples around a coordinate to check for a color.
+ // We check multiple rows and columns, to avoid most artifact issues.
+ let areaSamples = [
+ {dx: 0, dy: 0},
+ {dx: 1, dy: 3},
+ {dx: 8, dy: 5},
+ ];
+ const threshold = thresholds[AppConstants.platform][color.name];
+ for (let {dx, dy} of areaSamples) {
+ const x = offsetX + dx + internalX;
+ const y = offsetY + dy + internalY;
+ info(`Checking pixel (${[x,y]}) of total resolution `
+ + `${video.videoWidth}x${video.videoHeight} against ${color.name}.`);
+ let lastPixel = [-1, -1, -1, -1];
+ await helper.waitForPixel(video, px => {
+ lastPixel = Array.from(px);
+ return helper.isPixel(px, color, threshold);
+ }, {
+ offsetX: x,
+ offsetY: y,
+ cancel: wait(30000).then(_ =>
+ new Error(`Checking ${[x,y]} against ${color.name} timed out. ` +
+ `Got [${lastPixel}]. Threshold ${threshold}.`)),
+ });
+ ok(true, `Pixel (${[x,y]}) passed. Got [${lastPixel}].`);
+ }
+ };
+
+ const screenSizeSq = Math.min(video.videoWidth, video.videoHeight);
+
+ info("Waiting for upper left quadrant to become " + upleft.name);
+ await verifyAround(screenSizeSq / 4, screenSizeSq / 4, upleft);
+
+ info("Waiting for upper right quadrant to become " + upright.name);
+ await verifyAround(screenSizeSq * 3 / 4, screenSizeSq / 4, upright);
+
+ info("Waiting for lower left quadrant to become " + downleft.name);
+ await verifyAround(screenSizeSq / 4, screenSizeSq * 3 / 4, downleft);
+
+ info("Waiting for lower right quadrant to become " + downright.name);
+ await verifyAround(screenSizeSq * 3 / 4, screenSizeSq * 3 / 4, downright);
+ };
+
+ /**
+ * Run a test to verify that we can complete a start and stop media playback
+ * cycle for a screenshare MediaStream on a video HTMLMediaElement.
+ */
+ runTest(async function () {
+ await pushPrefs(
+ ["full-screen-api.enabled", true],
+ ["full-screen-api.allow-trusted-requests-only", false],
+ ["full-screen-api.transition-duration.enter", "0 0"],
+ ["full-screen-api.transition-duration.leave", "0 0"],
+ );
+
+ // Improve real estate for screenshots
+ const test = document.getElementById("test");
+ test.setAttribute("style", "height:0;margin:0;");
+ const display = document.getElementById("display");
+ display.setAttribute("style", "margin:0;");
+ const testVideo = createMediaElement('video', 'testVideo');
+ testVideo.removeAttribute("width");
+ testVideo.removeAttribute("height");
+ testVideo.setAttribute("style", "max-height:240px;");
+
+ const canvas = document.createElement("canvas");
+ canvas.width = canvas.height = 20;
+ document.getElementById("content").appendChild(canvas);
+ const draw = ([upleft, upright, downleft, downright]) => {
+ helper.drawColor(canvas, helper[upleft], {offsetX: 0, offsetY: 0});
+ helper.drawColor(canvas, helper[upright], {offsetX: 10, offsetY: 0});
+ helper.drawColor(canvas, helper[downleft], {offsetX: 0, offsetY: 10});
+ helper.drawColor(canvas, helper[downright], {offsetX: 10, offsetY: 10});
+ };
+ const helper = new CaptureStreamTestHelper2D(1, 1);
+
+ const doVerify = async (stream, [upleft, upright, downleft, downright]) => {
+ // Reset from potential earlier verification runs.
+ testVideo.srcObject = null;
+ const playback = new MediaStreamPlayback(testVideo, stream);
+ playback.startMedia();
+ await playback.verifyPlaying();
+ const settings = stream.getTracks()[0].getSettings();
+ is(settings.width, testVideo.videoWidth,
+ "Width setting should match video width");
+ is(settings.height, testVideo.videoHeight,
+ "Height setting should match video height");
+ await SpecialPowers.wrap(canvas).requestFullscreen();
+ try {
+ await verifyScreenshare(testVideo, helper, helper[upleft], helper[upright],
+ helper[downleft], helper[downright]);
+ } finally {
+ await playback.stopTracksForStreamInMediaPlayback();
+ await SpecialPowers.wrap(document).exitFullscreen();
+ // We wait a bit extra here to make sure we have completely left
+ // fullscreen when the --screenshot-on-fail screenshot is captured.
+ await wait(300);
+ }
+ };
+
+ info("Testing screenshare without constraints");
+ SpecialPowers.wrap(document).notifyUserGestureActivation();
+ let stream = await getUserMedia({video: {mediaSource: "screen"}});
+ let settings = stream.getTracks()[0].getSettings();
+ ok(settings.width <= 8192,
+ `Width setting ${settings.width} should be set after gUM (or 0 per bug 1453247)`);
+ ok(settings.height <= 8192,
+ `Height setting ${settings.height} should be set after gUM (or 0 per bug 1453247)`);
+ let colors = ["red", "blue", "green", "white"];
+ draw(colors);
+ await doVerify(stream, colors);
+ const screenWidth = testVideo.videoWidth;
+ const screenHeight = testVideo.videoHeight;
+
+ info("Testing screenshare with size and framerate constraints");
+ SpecialPowers.wrap(document).notifyUserGestureActivation();
+ for (const track of stream.getTracks()) {
+ track.stop();
+ }
+ stream = await getUserMedia({
+ video: {
+ mediaSource: 'screen',
+ width: {
+ min: '10',
+ max: '100'
+ },
+ height: {
+ min: '10',
+ max: '100'
+ },
+ frameRate: {
+ min: '10',
+ max: '15'
+ },
+ },
+ });
+ settings = stream.getTracks()[0].getSettings();
+ ok(settings.width == 0 || (settings.width >= 10 && settings.width <= 100),
+ `Width setting ${settings.width} should be correct after gUM (or 0 per bug 1453247)`);
+ ok(settings.height == 0 || (settings.height >= 10 && settings.height <= 100),
+ `Height setting ${settings.height} should be correct after gUM (or 0 per bug 1453247)`);
+ colors = ["green", "red", "white", "blue"];
+ draw(colors);
+ const streamClone = stream.clone();
+ await doVerify(streamClone, colors);
+ settings = stream.getTracks()[0].getSettings();
+ ok(settings.width >= 10 && settings.width <= 100,
+ `Width setting ${settings.width} should be within constraints`);
+ ok(settings.height >= 10 && settings.height <= 100,
+ `Height setting ${settings.height} should be within constraints`);
+ is(settings.width, testVideo.videoWidth,
+ "Width setting should match video width");
+ is(settings.height, testVideo.videoHeight,
+ "Height setting should match video height");
+ let expectedHeight = (screenHeight * settings.width) / screenWidth;
+ ok(Math.abs(expectedHeight - settings.height) <= 1,
+ "Aspect ratio after constrained gUM should be close enough");
+
+ info("Testing modifying screenshare with applyConstraints");
+ testVideo.srcObject = stream;
+ testVideo.play();
+ await new Promise(r => testVideo.onloadeddata = r);
+ const resize = haveEvent(
+ testVideo, "resize", wait(5000, new Error("Timeout waiting for resize")));
+ await stream.getVideoTracks()[0].applyConstraints({
+ mediaSource: 'screen',
+ width: 200,
+ height: 200,
+ frameRate: {
+ min: '5',
+ max: '10'
+ }
+ });
+ // getSettings() should report correct size as soon as applyConstraints()
+ // resolves - bug 1453259. Until fixed, check that we at least report
+ // something sane.
+ const newSettings = stream.getTracks()[0].getSettings();
+ ok(newSettings.width > settings.width && newSettings.width < screenWidth,
+ `Width setting ${newSettings.width} should have increased after applyConstraints`);
+ ok(newSettings.height > settings.height && newSettings.height < screenHeight,
+ `Height setting ${newSettings.height} should have increased after applyConstraints`);
+ await resize;
+ settings = stream.getTracks()[0].getSettings();
+ ok(settings.width > 100 && settings.width < screenWidth,
+ `Width setting ${settings.width} should have increased after first frame after applyConstraints`);
+ ok(settings.height > 100 && settings.height < screenHeight,
+ `Height setting ${settings.height} should have increased after first frame after applyConstraints`);
+ is(settings.width, testVideo.videoWidth,
+ "Width setting should match video width");
+ is(settings.height, testVideo.videoHeight,
+ "Height setting should match video height");
+ expectedHeight = (screenHeight * settings.width) / screenWidth;
+ ok(Math.abs(expectedHeight - settings.height) <= 1,
+ "Aspect ratio after applying constraints should be close enough");
+ colors = ["white", "green", "blue", "red"];
+ draw(colors);
+ await doVerify(stream, colors);
+ for (const track of [...stream.getTracks(), ...streamClone.getTracks()]) {
+ track.stop();
+ }
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_basicTabshare.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_basicTabshare.html
new file mode 100644
index 0000000000..635cf387d4
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_basicTabshare.html
@@ -0,0 +1,67 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ title: "getUserMedia Basic Tabshare Test",
+ bug: "1193075"
+ });
+ /**
+ * Run a test to verify that we can complete a start and stop media playback
+ * cycle for a tabshare MediaStream on a video HTMLMediaElement.
+ *
+ * Additionally, exercise applyConstraints code for tabshare viewport offset.
+ */
+ runTest(function () {
+ var testVideo = createMediaElement('video', 'testVideo');
+
+ return Promise.resolve()
+ .then(() => pushPrefs(["media.getusermedia.browser.enabled", true]))
+ .then(() => {
+ SpecialPowers.wrap(document).notifyUserGestureActivation();
+ return getUserMedia({
+ video: { mediaSource: "browser",
+ scrollWithPage: true },
+ });
+ })
+ .then(stream => {
+ var playback = new MediaStreamPlayback(testVideo, stream);
+ return playback.playMedia(false);
+ })
+ .then(() => getUserMedia({
+ video: {
+ mediaSource: "browser",
+ viewportOffsetX: 0,
+ viewportOffsetY: 0,
+ viewportWidth: 100,
+ viewportHeight: 100
+ },
+ }))
+ .then(stream => {
+ var playback = new MediaStreamPlayback(testVideo, stream);
+ playback.startMedia(false);
+ return playback.verifyPlaying()
+ .then(() => Promise.all([
+ () => testVideo.srcObject.getVideoTracks()[0].applyConstraints({
+ mediaSource: "browser",
+ viewportOffsetX: 10,
+ viewportOffsetY: 50,
+ viewportWidth: 90,
+ viewportHeight: 50
+ }),
+ () => listenUntil(testVideo, "resize", () => true)
+ ]))
+ .then(() => playback.verifyPlaying()) // still playing
+ .then(() => playback.stopTracksForStreamInMediaPlayback())
+ .then(() => playback.detachFromMediaElement());
+ });
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_basicVideo.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_basicVideo.html
new file mode 100644
index 0000000000..786d9f2e4b
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_basicVideo.html
@@ -0,0 +1,30 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ title: "getUserMedia Basic Video Test",
+ bug: "781534"
+ });
+ /**
+ * Run a test to verify that we can complete a start and stop media playback
+ * cycle for an video MediaStream on a video HTMLMediaElement.
+ */
+ runTest(function () {
+ var testVideo = createMediaElement('video', 'testVideo');
+ var constraints = {video: true};
+
+ return getUserMedia(constraints).then(stream => {
+ var playback = new MediaStreamPlayback(testVideo, stream);
+ return playback.playMedia(false);
+ });
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_basicVideoAudio.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_basicVideoAudio.html
new file mode 100644
index 0000000000..5218bf7301
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_basicVideoAudio.html
@@ -0,0 +1,30 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ title: "getUserMedia Basic Video & Audio Test",
+ bug: "781534"
+ });
+ /**
+ * Run a test to verify that we can complete a start and stop media playback
+ * cycle for a video and audio MediaStream on a video HTMLMediaElement.
+ */
+ runTest(function () {
+ var testVideoAudio = createMediaElement('video', 'testVideoAudio');
+ var constraints = {video: true, audio: true};
+
+ return getUserMedia(constraints).then(stream => {
+ var playback = new MediaStreamPlayback(testVideoAudio, stream);
+ return playback.playMedia(false);
+ });
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_basicVideo_playAfterLoadedmetadata.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_basicVideo_playAfterLoadedmetadata.html
new file mode 100644
index 0000000000..fbab1b4357
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_basicVideo_playAfterLoadedmetadata.html
@@ -0,0 +1,42 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ title: "getUserMedia Basic Video shall receive 'loadedmetadata' without play()ing",
+ bug: "1149494"
+ });
+ /**
+ * Run a test to verify that we will always get 'loadedmetadata' from a video
+ * HTMLMediaElement playing a gUM MediaStream.
+ */
+ runTest(() => {
+ var testVideo = createMediaElement('video', 'testVideo');
+ var constraints = {video: true};
+
+ return getUserMedia(constraints).then(stream => {
+ var playback = new MediaStreamPlayback(testVideo, stream);
+ var video = playback.mediaElement;
+
+ video.srcObject = stream;
+ return new Promise(resolve => {
+ ok(playback.mediaElement.paused,
+ "Media element should be paused before play()ing");
+ video.addEventListener('loadedmetadata', function() {
+ ok(video.videoWidth > 0, "Expected nonzero video width");
+ ok(video.videoHeight > 0, "Expected nonzero video width");
+ resolve();
+ });
+ })
+ .then(() => stream.getTracks().forEach(t => t.stop()));
+ });
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_basicWindowshare.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_basicWindowshare.html
new file mode 100644
index 0000000000..7b27944bdc
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_basicWindowshare.html
@@ -0,0 +1,39 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ title: "getUserMedia Basic Windowshare Test",
+ bug: "1038926"
+ });
+ /**
+ * Run a test to verify that we can complete a start and stop media playback
+ * cycle for an screenshare MediaStream on a video HTMLMediaElement.
+ */
+ runTest(async function () {
+ const testVideo = createMediaElement('video', 'testVideo');
+ const constraints = {
+ video: { mediaSource: "window" },
+ };
+
+ try {
+ await getUserMedia(constraints);
+ ok(false, "Should require user gesture");
+ } catch (e) {
+ is(e.name, "InvalidStateError");
+ }
+
+ SpecialPowers.wrap(document).notifyUserGestureActivation();
+ const stream = await getUserMedia(constraints);
+ const playback = new MediaStreamPlayback(testVideo, stream);
+ return playback.playMedia(false);
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_bug1223696.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_bug1223696.html
new file mode 100644
index 0000000000..6af7b69d70
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_bug1223696.html
@@ -0,0 +1,54 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+ <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ "use strict";
+
+ createHTML({
+ title: "Testing that removeTrack+addTrack of video tracks still render the correct track in a media element",
+ bug: "1223696",
+ visible: true
+ });
+
+ runTest(async function() {
+ const stream = await getUserMedia({audio:true, video: true});
+ info("Test addTrack()ing a video track to an audio-only gUM stream");
+
+ const video = createMediaElement("video", "test_video_track");
+ video.srcObject = stream;
+ video.play();
+
+ await haveEvent(video, "loadeddata", wait(5000, new Error("Timeout")));
+ info("loadeddata");
+
+ const removedTrack = stream.getVideoTracks()[0];
+ stream.removeTrack(removedTrack);
+
+ const h = new CaptureStreamTestHelper2D();
+ const emitter = new VideoFrameEmitter(h.grey, h.grey);
+ emitter.start();
+
+ stream.addTrack(emitter.stream().getVideoTracks()[0]);
+
+ checkMediaStreamContains(stream, [stream.getAudioTracks()[0],
+ emitter.stream().getVideoTracks()[0]]);
+
+ await h.pixelMustBecome(video, h.grey, {
+ threshold: 5,
+ infoString: "The canvas track should be rendered by the media element",
+ });
+
+ emitter.stop();
+ for (const t of [removedTrack, ...stream.getAudioTracks()]) {
+ t.stop();
+ }
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_callbacks.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_callbacks.html
new file mode 100644
index 0000000000..14c6cc7e7f
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_callbacks.html
@@ -0,0 +1,35 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ title: "navigator.mozGetUserMedia Callback Test",
+ bug: "1119593"
+ });
+ /**
+ * Check that the old fashioned callback-based function works.
+ */
+ runTest(function () {
+ var testAudio = createMediaElement('audio', 'testAudio');
+ var constraints = {audio: true};
+
+ SimpleTest.waitForExplicitFinish();
+ return new Promise(resolve =>
+ navigator.mozGetUserMedia(constraints, stream => {
+ checkMediaStreamTracks(constraints, stream);
+
+ var playback = new MediaStreamPlayback(testAudio, stream);
+ return playback.playMedia(false)
+ .then(() => resolve(), generateErrorCallback());
+ }, generateErrorCallback())
+ );
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_constraints.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_constraints.html
new file mode 100644
index 0000000000..d6439ce9d6
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_constraints.html
@@ -0,0 +1,166 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script src="mediaStreamPlayback.js"></script>
+ <script src="constraints.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({ title: "Test getUserMedia constraints", bug: "882145" });
+/**
+ Tests covering gUM constraints API for audio, video and fake video. Exercise
+ successful parsing code and ensure that unknown required constraints and
+ overconstraining cases produce appropriate errors.
+*/
+var tests = [
+ // Each test here tests a different constraint or codepath.
+ { message: "unknown required constraint on video ignored",
+ constraints: { video: { somethingUnknown: { exact: 0 } } },
+ error: null },
+ { message: "unknown required constraint on audio ignored",
+ constraints: { audio: { somethingUnknown: { exact: 0 } } },
+ error: null },
+ { message: "audio overconstrained by facingMode ignored",
+ constraints: { audio: { facingMode: { exact: 'left' } } },
+ error: null },
+ { message: "full screensharing requires permission",
+ constraints: { video: { mediaSource: 'screen' } },
+ error: "NotAllowedError" },
+ { message: "application screensharing no longer exists",
+ constraints: { video: { mediaSource: 'application' } },
+ error: "OverconstrainedError" },
+ { message: "window screensharing requires permission",
+ constraints: { video: { mediaSource: 'window' } },
+ error: "NotAllowedError" },
+ { message: "browser screensharing requires permission",
+ constraints: { video: { mediaSource: 'browser' } },
+ error: "NotAllowedError" },
+ { message: "unknown mediaSource in video fails",
+ constraints: { video: { mediaSource: 'uncle' } },
+ error: "OverconstrainedError",
+ constraint: "mediaSource" },
+ { message: "unknown mediaSource in audio fails",
+ constraints: { audio: { mediaSource: 'uncle' } },
+ error: "OverconstrainedError",
+ constraint: "mediaSource" },
+ { message: "emtpy constraint fails",
+ constraints: { },
+ error: "TypeError" },
+ { message: "Triggering mock failure in default video device fails",
+ constraints: { video: { deviceId: 'bad device' } },
+ error: "NotReadableError" },
+ { message: "Triggering mock failure in default audio device fails",
+ constraints: { audio: { deviceId: 'bad device' } },
+ error: "NotReadableError" },
+ { message: "Success-path: optional video facingMode + audio ignoring facingMode",
+ constraints: { audio: { mediaSource: 'microphone',
+ facingMode: 'left',
+ foo: 0,
+ advanced: [{ facingMode: 'environment' },
+ { facingMode: 'user' },
+ { bar: 0 }] },
+ video: { mediaSource: 'camera',
+ foo: 0,
+ advanced: [{ facingMode: 'environment' },
+ { facingMode: ['user'] },
+ { facingMode: ['left', 'right', 'user'] },
+ { bar: 0 }] } },
+ error: null },
+ { message: "legacy facingMode ignored",
+ constraints: { video: { mandatory: { facingMode: 'left' } } },
+ error: null },
+];
+
+var mustSupport = [
+ 'width', 'height', 'frameRate', 'facingMode', 'deviceId', 'groupId',
+ 'echoCancellation', 'noiseSuppression', 'autoGainControl', 'channelCount',
+
+ // Yet to add:
+ // 'aspectRatio', 'volume', 'sampleRate', 'sampleSize', 'latency'
+
+ // http://fluffy.github.io/w3c-screen-share/#screen-based-video-constraints
+ // OBE by http://w3c.github.io/mediacapture-screen-share
+ 'mediaSource',
+
+ // Experimental https://bugzilla.mozilla.org/show_bug.cgi?id=1131568#c3
+ 'browserWindow', 'scrollWithPage',
+ 'viewportOffsetX', 'viewportOffsetY', 'viewportWidth', 'viewportHeight',
+];
+
+var mustFailWith = (msg, reason, constraint, f) =>
+ f().then(() => ok(false, msg + " must fail"), e => {
+ is(e.name, reason, msg + " must fail: " + e.message);
+ if (constraint !== undefined) {
+ is(e.constraint, constraint, msg + " must fail w/correct constraint.");
+ }
+ });
+
+/**
+ * Starts the test run by running through each constraint
+ * test by verifying that the right resolution and rejection is fired.
+ */
+
+runTest(() => pushPrefs(
+ // This test expects fake devices, particularly for the 'triggering mock
+ // failure *' steps. So explicitly disable loopback and setup fakes
+ ['media.audio_loopback_dev', ''],
+ ['media.video_loopback_dev', ''],
+ ['media.navigator.streams.fake', true]
+ )
+ .then(() => {
+ // Check supported constraints first.
+ var dict = navigator.mediaDevices.getSupportedConstraints();
+ var supported = Object.keys(dict);
+
+ mustSupport.forEach(key => ok(supported.includes(key) && dict[key],
+ "Supports " + key));
+
+ var unexpected = supported.filter(key => !mustSupport.includes(key));
+ is(unexpected.length, 0,
+ "Unanticipated support (please update test): " + unexpected);
+ })
+ .then(() => pushPrefs(["media.getusermedia.browser.enabled", false],
+ ["media.getusermedia.screensharing.enabled", false]))
+ .then(() => tests.reduce((p, test) => p.then(
+ () => {
+ SpecialPowers.wrap(document).notifyUserGestureActivation();
+ return getUserMedia(test.constraints);
+ })
+ .then(stream => {
+ is(null, test.error, test.message);
+ stream.getTracks().forEach(t => t.stop());
+ }, e => {
+ is(e.name, test.error, test.message + ": " + e.message);
+ if (test.constraint) {
+ is(e.constraint, test.constraint,
+ test.message + " w/correct constraint.");
+ }
+ }), Promise.resolve()))
+ .then(() => getUserMedia({video: true, audio: true}))
+ .then(stream => stream.getVideoTracks()[0].applyConstraints({ width: 320 })
+ .then(() => stream.getAudioTracks()[0].applyConstraints({ }))
+ .then(() => {
+ stream.getTracks().forEach(track => track.stop());
+ ok(true, "applyConstraints code exercised");
+ }))
+ // TODO: Test outcome once fake devices support constraints (Bug 1088621)
+ .then(() => mustFailWith("applyConstraints fails on non-Gum tracks",
+ "OverconstrainedError", "",
+ () => (new AudioContext())
+ .createMediaStreamDestination().stream
+ .getAudioTracks()[0].applyConstraints()))
+ .then(() => mustFailWith(
+ "getUserMedia with unsatisfied required constraint",
+ "OverconstrainedError", "deviceId",
+ () => getUserMedia({ audio: true,
+ video: { deviceId: { exact: "unheardof" } } })))
+ .then(() => mustFailWith(
+ "getUserMedia with unsatisfied required constraint array",
+ "OverconstrainedError", "deviceId",
+ () => getUserMedia({ audio: true,
+ video: { deviceId: { exact: ["a", "b"] } } }))));
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_cubebDisabled.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_cubebDisabled.html
new file mode 100644
index 0000000000..54142aeb77
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_cubebDisabled.html
@@ -0,0 +1,42 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ title: "getUserMedia with Cubeb Disabled Test",
+ bug: "1443525"
+ });
+ /**
+ * Run a test to verify we fail gracefully if we cannot fetch a cubeb context
+ * during a gUM call.
+ */
+ runTest(async function () {
+ info("Get user media with cubeb disabled starting");
+ // Push prefs to ensure no cubeb context and no fake streams.
+ await pushPrefs(["media.cubeb.force_null_context", true],
+ ["media.navigator.permission.device", false],
+ ["media.navigator.streams.fake", false]);
+
+ // Request audio only, to avoid cams
+ let constraints = {audio: true, video: false};
+ let stream;
+ try {
+ stream = await getUserMedia(constraints);
+ } catch (e) {
+ // We've got no audio backend, so we expect gUM to fail.
+ ok(e.name == "NotFoundError", "Expected NotFoundError due to no audio tracks!");
+ return;
+ }
+ // If we're not on android we should not have gotten a stream without a cubeb context!
+ ok(false, "getUserMedia not expected to succeed when cubeb is disabled, but it did!");
+ });
+
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_cubebDisabledFakeStreams.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_cubebDisabledFakeStreams.html
new file mode 100644
index 0000000000..f8150cc4c1
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_cubebDisabledFakeStreams.html
@@ -0,0 +1,43 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ title: "getUserMedia fake stream with Cubeb Disabled Test",
+ bug: "1443525"
+ });
+ /**
+ * Run a test to verify we can still return a fake stream even if we cannot
+ * get a cubeb context. See also Bug 1434477
+ */
+ runTest(async function () {
+ info("Get user media with cubeb disabled and fake tracks starting");
+ // Push prefs to ensure no cubeb context and fake streams
+ await pushPrefs(["media.cubeb.force_null_context", true],
+ ["media.navigator.streams.fake", true],
+ ['media.audio_loopback_dev', '']);
+ let testAudio = createMediaElement('audio', 'testAudio');
+ // Request audio only, to avoid cams
+ let constraints = {audio: true, video: false};
+ let stream;
+ try {
+ stream = await getUserMedia(constraints);
+ } catch (e) {
+ // We've got no audio backend, so we expect gUM to fail
+ ok(false, `Did not expect to fail, but got ${e}`);
+ return;
+ }
+ ok(stream, "getUserMedia should get a stream!");
+ let playback = new MediaStreamPlayback(testAudio, stream);
+ return playback.playMedia(false);
+ });
+
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_getTrackById.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_getTrackById.html
new file mode 100644
index 0000000000..161bf631e3
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_getTrackById.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ title: "Basic getTrackById test of gUM stream",
+ bug: "1208390",
+ });
+
+ runTest(() => {
+ var constraints = {audio: true, video: true};
+ return getUserMedia(constraints).then(stream => {
+ is(stream.getTrackById(""), null,
+ "getTrackById of non-matching string should return null");
+
+ let audioTrack = stream.getAudioTracks()[0];
+ is(stream.getTrackById(audioTrack.id), audioTrack,
+ "getTrackById with matching id should return the track");
+
+ let videoTrack = stream.getVideoTracks()[0];
+ is(stream.getTrackById(videoTrack.id), videoTrack,
+ "getTrackById with matching id should return the track");
+
+ stream.removeTrack(audioTrack);
+ is(stream.getTrackById(audioTrack.id), null,
+ "getTrackById with id of removed track should return null");
+
+ let newStream = new MediaStream();
+ is(newStream.getTrackById(videoTrack.id), null,
+ "getTrackById with id of track in other stream should return null");
+
+ newStream.addTrack(audioTrack);
+ is(newStream.getTrackById(audioTrack.id), audioTrack,
+ "getTrackByid with matching id should return the track");
+
+ newStream.addTrack(videoTrack);
+ is(newStream.getTrackById(videoTrack.id), videoTrack,
+ "getTrackByid with matching id should return the track");
+ [audioTrack, videoTrack].forEach(t => t.stop());
+ });
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_gumWithinGum.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_gumWithinGum.html
new file mode 100644
index 0000000000..86a7aa5606
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_gumWithinGum.html
@@ -0,0 +1,38 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({title: "getUserMedia within getUserMedia", bug: "822109" });
+ /**
+ * Run a test that we can complete a playback cycle for a video,
+ * then upon completion, do a playback cycle with audio, such that
+ * the audio gum call happens within the video gum call.
+ */
+ runTest(function () {
+ return getUserMedia({video: true})
+ .then(videoStream => {
+ var testVideo = createMediaElement('video', 'testVideo');
+ var videoPlayback = new MediaStreamPlayback(testVideo,
+ videoStream);
+
+ return videoPlayback.playMediaWithoutStoppingTracks(false)
+ .then(() => getUserMedia({audio: true}))
+ .then(audioStream => {
+ var testAudio = createMediaElement('audio', 'testAudio');
+ var audioPlayback = new MediaStreamPlayback(testAudio,
+ audioStream);
+
+ return audioPlayback.playMedia(false);
+ })
+ .then(() => videoStream.getTracks().forEach(t => t.stop()));
+ });
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_loadedmetadata.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_loadedmetadata.html
new file mode 100644
index 0000000000..d6efac4650
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_loadedmetadata.html
@@ -0,0 +1,39 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ title: "getUserMedia in media element should have video dimensions on loadedmetadata",
+ bug: "1240478"
+ });
+ /**
+ * Tests that assigning a stream to a media element results in the
+ * "loadedmetadata" event without having to play() the media element.
+ *
+ * Also makes sure that the video size has been set on "loadedmetadata".
+ */
+ runTest(function () {
+ var v = document.createElement("video");
+ document.body.appendChild(v);
+ v.preload = "metadata";
+
+ var constraints = {video: true, audio: true};
+ return getUserMedia(constraints).then(stream => new Promise(resolve => {
+ v.srcObject = stream;
+ v.onloadedmetadata = () => {
+ isnot(v.videoWidth, 0, "videoWidth shall be set on 'loadedmetadata'");
+ isnot(v.videoHeight, 0, "videoHeight shall be set on 'loadedmetadata'");
+ resolve();
+ };
+ })
+ .then(() => stream.getTracks().forEach(t => t.stop())));
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_mediaElementCapture_audio.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_mediaElementCapture_audio.html
new file mode 100644
index 0000000000..3b9e00896c
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_mediaElementCapture_audio.html
@@ -0,0 +1,116 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script>
+
+createHTML({
+ bug: "1259788",
+ title: "Test CaptureStream audio content on HTMLMediaElement playing a gUM MediaStream",
+ visible: true
+});
+
+var audioContext;
+var gUMAudioElement;
+var analyser;
+runTest(() => getUserMedia({audio: { echoCancellation: false }})
+ .then(stream => {
+ gUMAudioElement = createMediaElement("audio", "gUMAudio");
+ gUMAudioElement.srcObject = stream;
+
+ audioContext = new AudioContext();
+ info("Capturing");
+
+ analyser = new AudioStreamAnalyser(audioContext,
+ gUMAudioElement.mozCaptureStream());
+ analyser.enableDebugCanvas();
+ return analyser.waitForAnalysisSuccess(array =>
+ array[analyser.binIndexForFrequency(50)] < 50 &&
+ array[analyser.binIndexForFrequency(TEST_AUDIO_FREQ)] > 200 &&
+ array[analyser.binIndexForFrequency(2500)] < 50);
+ })
+ .then(() => {
+ info("Audio flowing. Pausing.");
+ gUMAudioElement.pause();
+
+ return analyser.waitForAnalysisSuccess(array =>
+ array[analyser.binIndexForFrequency(50)] < 50 &&
+ array[analyser.binIndexForFrequency(TEST_AUDIO_FREQ)] < 50 &&
+ array[analyser.binIndexForFrequency(2500)] < 50);
+ })
+ .then(() => {
+ info("Audio stopped flowing. Playing.");
+ gUMAudioElement.play();
+
+ return analyser.waitForAnalysisSuccess(array =>
+ array[analyser.binIndexForFrequency(50)] < 50 &&
+ array[analyser.binIndexForFrequency(TEST_AUDIO_FREQ)] > 200 &&
+ array[analyser.binIndexForFrequency(2500)] < 50);
+ })
+ .then(() => {
+ info("Audio flowing. Removing source.");
+ var stream = gUMAudioElement.srcObject;
+ gUMAudioElement.srcObject = null;
+
+ return analyser.waitForAnalysisSuccess(array =>
+ array[analyser.binIndexForFrequency(50)] < 50 &&
+ array[analyser.binIndexForFrequency(TEST_AUDIO_FREQ)] < 50 &&
+ array[analyser.binIndexForFrequency(2500)] < 50)
+ .then(() => stream);
+ })
+ .then(stream => {
+ info("Audio stopped flowing. Setting source.");
+ gUMAudioElement.srcObject = stream;
+
+ return analyser.waitForAnalysisSuccess(array =>
+ array[analyser.binIndexForFrequency(50)] < 50 &&
+ array[analyser.binIndexForFrequency(TEST_AUDIO_FREQ)] > 200 &&
+ array[analyser.binIndexForFrequency(2500)] < 50);
+ })
+ .then(() => {
+ info("Audio flowing from new source. Adding a track.");
+ let oscillator = audioContext.createOscillator();
+ oscillator.type = 'sine';
+ oscillator.frequency.value = 2000;
+ oscillator.start();
+
+ let oscOut = audioContext.createMediaStreamDestination();
+ oscillator.connect(oscOut);
+
+ gUMAudioElement.srcObject.addTrack(oscOut.stream.getTracks()[0]);
+
+ return analyser.waitForAnalysisSuccess(array =>
+ array[analyser.binIndexForFrequency(50)] < 50 &&
+ array[analyser.binIndexForFrequency(TEST_AUDIO_FREQ)] > 200 &&
+ array[analyser.binIndexForFrequency(1500)] < 50 &&
+ array[analyser.binIndexForFrequency(2000)] > 200 &&
+ array[analyser.binIndexForFrequency(2500)] < 50);
+ })
+ .then(() => {
+ info("Audio flowing from new track. Removing a track.");
+
+ const gUMTrack = gUMAudioElement.srcObject.getTracks()[0];
+ gUMAudioElement.srcObject.removeTrack(gUMTrack);
+
+ is(gUMAudioElement.srcObject.getTracks().length, 1,
+ "A track should have been removed");
+
+ return analyser.waitForAnalysisSuccess(array =>
+ array[analyser.binIndexForFrequency(50)] < 50 &&
+ array[analyser.binIndexForFrequency(TEST_AUDIO_FREQ)] < 50 &&
+ array[analyser.binIndexForFrequency(1500)] < 50 &&
+ array[analyser.binIndexForFrequency(2000)] > 200 &&
+ array[analyser.binIndexForFrequency(2500)] < 50)
+ .then(() => [gUMTrack, ...gUMAudioElement.srcObject.getTracks()]
+ .forEach(t => t.stop()));
+ })
+ .then(() => ok(true, "Test passed."))
+ .catch(e => ok(false, "Test failed: " + e + (e.stack ? "\n" + e.stack : ""))));
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_mediaElementCapture_tracks.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_mediaElementCapture_tracks.html
new file mode 100644
index 0000000000..a747e75de9
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_mediaElementCapture_tracks.html
@@ -0,0 +1,179 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script>
+
+createHTML({
+ bug: "1259788",
+ title: "Test CaptureStream track output on HTMLMediaElement playing a gUM MediaStream",
+ visible: true
+});
+
+let audioElement;
+let audioCaptureStream;
+let videoElement;
+let videoCaptureStream;
+let untilEndedElement;
+let streamUntilEnded;
+const tracks = [];
+runTest(async () => {
+ try {
+ let stream = await getUserMedia({audio: true, video: true});
+ // We need to test with multiple tracks. We add an extra of each kind.
+ for (const track of stream.getTracks()) {
+ stream.addTrack(track.clone());
+ }
+
+ audioElement = createMediaElement("audio", "gUMAudio");
+ audioElement.srcObject = stream;
+
+ await haveEvent(audioElement, "loadedmetadata", wait(50000, new Error("Timeout")));
+
+ info("Capturing audio element (loadedmetadata -> captureStream)");
+ audioCaptureStream = audioElement.mozCaptureStream();
+
+ is(audioCaptureStream.getAudioTracks().length, 2,
+ "audio element should capture two audio tracks");
+ is(audioCaptureStream.getVideoTracks().length, 0,
+ "audio element should not capture any video tracks");
+
+ await haveNoEvent(audioCaptureStream, "addtrack");
+
+ videoElement = createMediaElement("video", "gUMVideo");
+
+ info("Capturing video element (captureStream -> loadedmetadata)");
+ videoCaptureStream = videoElement.mozCaptureStream();
+ videoElement.srcObject = audioElement.srcObject.clone();
+
+ is(videoCaptureStream.getTracks().length, 0,
+ "video element should have no tracks before metadata known");
+
+ await haveEventsButNoMore(
+ videoCaptureStream, "addtrack", 3, wait(50000, new Error("No event")));
+
+ is(videoCaptureStream.getAudioTracks().length, 2,
+ "video element should capture two audio tracks");
+ is(videoCaptureStream.getVideoTracks().length, 1,
+ "video element should capture one video track at most");
+
+ info("Testing dynamically adding audio track to audio element");
+ audioElement.srcObject.addTrack(
+ audioElement.srcObject.getAudioTracks()[0].clone());
+ await haveEventsButNoMore(
+ audioCaptureStream, "addtrack", 1, wait(50000, new Error("No event")));
+
+ is(audioCaptureStream.getAudioTracks().length, 3,
+ "Audio element should have three audio tracks captured.");
+
+ info("Testing dynamically adding video track to audio element");
+ audioElement.srcObject.addTrack(
+ audioElement.srcObject.getVideoTracks()[0].clone());
+ await haveNoEvent(audioCaptureStream, "addtrack");
+
+ is(audioCaptureStream.getVideoTracks().length, 0,
+ "Audio element should have no video tracks captured.");
+
+ info("Testing dynamically adding audio track to video element");
+ videoElement.srcObject.addTrack(
+ videoElement.srcObject.getAudioTracks()[0].clone());
+ await haveEventsButNoMore(
+ videoCaptureStream, "addtrack", 1, wait(50000, new Error("Timeout")));
+
+ is(videoCaptureStream.getAudioTracks().length, 3,
+ "Captured video stream should have three audio tracks captured.");
+
+ info("Testing dynamically adding video track to video element");
+ videoElement.srcObject.addTrack(
+ videoElement.srcObject.getVideoTracks()[0].clone());
+ await haveNoEvent(videoCaptureStream, "addtrack");
+
+ is(videoCaptureStream.getVideoTracks().length, 1,
+ "Captured video stream should have at most one video tracks captured.");
+
+ info("Testing track removal.");
+ tracks.push(...videoElement.srcObject.getTracks());
+ for (const track of videoElement.srcObject.getVideoTracks().reverse()) {
+ videoElement.srcObject.removeTrack(track);
+ }
+ is(videoCaptureStream.getVideoTracks().length, 1,
+ "Captured video should have still have one video track.");
+
+ await haveEvent(videoCaptureStream.getVideoTracks()[0], "ended",
+ wait(50000, new Error("Timeout")));
+ await haveEvent(videoCaptureStream, "removetrack",
+ wait(50000, new Error("Timeout")));
+
+ is(videoCaptureStream.getVideoTracks().length, 0,
+ "Captured video stream should have no video tracks after removal.");
+
+
+ info("Testing source reset.");
+ stream = await getUserMedia({audio: true, video: true});
+ videoElement.srcObject = stream;
+ for (const track of videoCaptureStream.getTracks()) {
+ await Promise.race(videoCaptureStream.getTracks().map(
+ t => haveEvent(t, "ended", wait(50000, new Error("Timeout"))))
+ );
+ await haveEvent(videoCaptureStream, "removetrack", wait(50000, new Error("Timeout")));
+ }
+ await haveEventsButNoMore(
+ videoCaptureStream, "addtrack", 2, wait(50000, new Error("Timeout")));
+ is(videoCaptureStream.getAudioTracks().length, 1,
+ "Captured video stream should have one audio track");
+
+ is(videoCaptureStream.getVideoTracks().length, 1,
+ "Captured video stream should have one video track");
+
+ info("Testing CaptureStreamUntilEnded");
+ untilEndedElement =
+ createMediaElement("video", "gUMVideoUntilEnded");
+ untilEndedElement.srcObject = audioElement.srcObject;
+
+ await haveEvent(untilEndedElement, "loadedmetadata",
+ wait(50000, new Error("Timeout")));
+
+ streamUntilEnded = untilEndedElement.mozCaptureStreamUntilEnded();
+
+ is(streamUntilEnded.getAudioTracks().length, 3,
+ "video element should capture all 3 audio tracks until ended");
+ is(streamUntilEnded.getVideoTracks().length, 1,
+ "video element should capture only 1 video track until ended");
+
+ for (const track of untilEndedElement.srcObject.getTracks()) {
+ track.stop();
+ }
+
+ await haveEvent(untilEndedElement, "ended", wait(50000, new Error("Timeout")));
+ for (const track of streamUntilEnded.getTracks()) {
+ await Promise.race(streamUntilEnded.getTracks().map(
+ t => haveEvent(t, "ended", wait(50000, new Error("Timeout"))))
+ );
+ await haveEvent(streamUntilEnded, "removetrack", wait(50000, new Error("Timeout")));
+ }
+
+ info("Element and tracks ended. Ensuring that new tracks aren't created.");
+ untilEndedElement.srcObject = videoElement.srcObject;
+ await haveEventsButNoMore(
+ untilEndedElement, "loadedmetadata", 1, wait(50000, new Error("Timeout")));
+
+ is(streamUntilEnded.getTracks().length, 0, "Should have no tracks");
+ } catch(e) {
+ ok(false, "Test failed: " + e + (e && e.stack ? "\n" + e.stack : ""));
+ } finally {
+ if (videoElement) {
+ tracks.push(...videoElement.srcObject.getTracks());
+ }
+ for(const track of tracks) {
+ track.stop();
+ }
+ }
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_mediaElementCapture_video.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_mediaElementCapture_video.html
new file mode 100644
index 0000000000..d177e93bfb
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_mediaElementCapture_video.html
@@ -0,0 +1,91 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script>
+
+createHTML({
+ bug: "1259788",
+ title: "Test CaptureStream video content on HTMLMediaElement playing a gUM MediaStream",
+ visible: true
+});
+
+var gUMVideoElement;
+var captureStreamElement;
+
+const pausedTimeout = 1000;
+let h;
+
+runTest(async () => {
+ try {
+ await pushPrefs(
+ // This test expects fake video devices, as it expects captured frames to
+ // shift over time, which is not currently provided by loopback devices
+ ['media.video_loopback_dev', ''],
+ ['media.navigator.streams.fake', true]);
+
+ let stream = await getUserMedia({video: true});
+ h = new VideoStreamHelper();
+ gUMVideoElement =
+ createMediaElement("video", "gUMVideo");
+ gUMVideoElement.srcObject = stream;
+ gUMVideoElement.play();
+
+ info("Capturing");
+ captureStreamElement =
+ createMediaElement("video", "captureStream");
+ captureStreamElement.srcObject = gUMVideoElement.mozCaptureStream();
+ captureStreamElement.play();
+
+ await h.checkVideoPlaying(captureStreamElement);
+
+ // Adding a dummy audio track to the stream will keep a consuming media
+ // element from ending.
+ // We could also solve it by repeatedly play()ing or autoplay, but then we
+ // wouldn't be sure the media element stopped rendering video because it
+ // went to the ended state or because there were no frames for the track.
+ let osc = createOscillatorStream(new AudioContext(), 1000);
+ captureStreamElement.srcObject.addTrack(osc.getTracks()[0]);
+
+ info("Video flowing. Pausing.");
+ gUMVideoElement.pause();
+ await h.checkVideoPaused(captureStreamElement, { time: pausedTimeout });
+
+ info("Video stopped flowing. Playing.");
+ gUMVideoElement.play();
+ await h.checkVideoPlaying(captureStreamElement);
+
+ info("Video flowing. Removing source.");
+ stream = gUMVideoElement.srcObject;
+ gUMVideoElement.srcObject = null;
+ await h.checkVideoPaused(captureStreamElement, { time: pausedTimeout });
+
+ info("Video stopped flowing. Setting source.");
+ gUMVideoElement.srcObject = stream;
+ await h.checkVideoPlaying(captureStreamElement);
+
+ info("Video flowing. Changing source by track manipulation. Remove first.");
+ let track = gUMVideoElement.srcObject.getTracks()[0];
+ gUMVideoElement.srcObject.removeTrack(track);
+ await h.checkVideoPaused(captureStreamElement, { time: pausedTimeout });
+
+ info("Video paused. Changing source by track manipulation. Add first.");
+ gUMVideoElement.srcObject.addTrack(track);
+ gUMVideoElement.play();
+ await h.checkVideoPlaying(captureStreamElement);
+
+ gUMVideoElement.srcObject.getTracks().forEach(t => t.stop());
+ ok(true, "Test passed.");
+ } catch (e) {
+ ok(false, "Test failed: " + e + (e.stack ? "\n" + e.stack : ""));
+ }
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_mediaStreamClone.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_mediaStreamClone.html
new file mode 100644
index 0000000000..029ce77dd0
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_mediaStreamClone.html
@@ -0,0 +1,258 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+"use strict";
+
+createHTML({
+ title: "MediaStream.clone()",
+ bug: "1208371"
+});
+
+runTest(async () => {
+ await pushPrefs(
+ ["media.getusermedia.camera.stop_on_disable.enabled", true],
+ ["media.getusermedia.camera.stop_on_disable.delay_ms", 0],
+ ["media.getusermedia.microphone.stop_on_disable.enabled", true],
+ ["media.getusermedia.microphone.stop_on_disable.delay_ms", 0]);
+
+ let gUMStream = await getUserMedia({audio: true, video: true});
+ {
+ info("Test clone()ing an audio/video gUM stream");
+ let clone = gUMStream.clone();
+
+ checkMediaStreamCloneAgainstOriginal(clone, gUMStream);
+ checkMediaStreamTrackCloneAgainstOriginal(clone.getAudioTracks()[0],
+ gUMStream.getAudioTracks()[0]);
+ checkMediaStreamTrackCloneAgainstOriginal(clone.getVideoTracks()[0],
+ gUMStream.getVideoTracks()[0]);
+
+ isnot(clone.id.length, 0, "Stream clone should have an id string");
+ isnot(clone.getAudioTracks()[0].id.length, 0,
+ "Audio track clone should have an id string");
+ isnot(clone.getVideoTracks()[0].id.length, 0,
+ "Audio track clone should have an id string");
+
+ info("Playing from track clones");
+ let test = createMediaElement('video', 'testClonePlayback');
+ let playback = new MediaStreamPlayback(test, clone);
+ await playback.playMedia(false);
+ }
+
+ {
+ info("Test addTrack()ing a video track to a stream without affecting its clone");
+ let stream = new MediaStream(gUMStream.getVideoTracks());
+ let otherStream = await getUserMedia({video: true});
+ let track = stream.getTracks()[0];
+ let otherTrack = otherStream.getTracks()[0];
+
+ let streamClone = stream.clone();
+ let trackClone = streamClone.getTracks()[0];
+ checkMediaStreamContains(streamClone, [trackClone], "Initial clone");
+
+ stream.addTrack(otherTrack);
+ checkMediaStreamContains(stream, [track, otherTrack],
+ "Added video to original");
+ checkMediaStreamContains(streamClone, [trackClone],
+ "Clone not affected");
+
+ stream.removeTrack(track);
+ streamClone.addTrack(track);
+ checkMediaStreamContains(streamClone, [trackClone, track],
+ "Added video to clone");
+ checkMediaStreamContains(stream, [otherTrack],
+ "Original not affected");
+
+ // Not part of streamClone. Does not get stopped by the playback test.
+ otherTrack.stop();
+
+ let test = createMediaElement('video', 'testClonePlayback');
+ let playback = new MediaStreamPlayback(test, streamClone);
+ await playback.playMedia(false);
+ }
+
+ {
+ info("Test cloning a stream into inception");
+ let stream = gUMStream.clone()
+ let clone = stream;
+ let clones = Array(10).fill().map(() => clone = clone.clone());
+ let inceptionClone = clones.pop();
+ checkMediaStreamCloneAgainstOriginal(inceptionClone, stream);
+ stream.getTracks().forEach(t => (stream.removeTrack(t),
+ inceptionClone.addTrack(t)));
+ is(inceptionClone.getAudioTracks().length, 2,
+ "The inception clone should contain the original audio track and a track clone");
+ is(inceptionClone.getVideoTracks().length, 2,
+ "The inception clone should contain the original video track and a track clone");
+
+ let test = createMediaElement('video', 'testClonePlayback');
+ let playback = new MediaStreamPlayback(test, inceptionClone);
+ await playback.playMedia(false);
+ clones.forEach(c => c.getTracks().forEach(t => t.stop()));
+ stream.getTracks().forEach(t => t.stop());
+ }
+
+ {
+ info("Test adding tracks from many stream clones to the original stream");
+ let stream = gUMStream.clone();
+
+ const LOOPS = 3;
+ for (let i = 0; i < LOOPS; i++) {
+ stream.clone().getTracks().forEach(t => stream.addTrack(t));
+ }
+ is(stream.getAudioTracks().length, Math.pow(2, LOOPS),
+ "The original track should contain the original audio track and all the audio clones");
+ is(stream.getVideoTracks().length, Math.pow(2, LOOPS),
+ "The original track should contain the original video track and all the video clones");
+ stream.getTracks().forEach(t1 => is(stream.getTracks()
+ .filter(t2 => t1.id == t2.id)
+ .length,
+ 1, "Each track should be unique"));
+
+ let test = createMediaElement('video', 'testClonePlayback');
+ let playback = new MediaStreamPlayback(test, stream);
+ await playback.playMedia(false);
+ }
+
+ {
+ info("Testing audio content routing with MediaStream.clone()");
+ let ac = new AudioContext();
+
+ let osc1kOriginal = createOscillatorStream(ac, 1000);
+ let audioTrack1kOriginal = osc1kOriginal.getTracks()[0];
+ let audioTrack1kClone = osc1kOriginal.clone().getTracks()[0];
+
+ let osc5kOriginal = createOscillatorStream(ac, 5000);
+ let audioTrack5kOriginal = osc5kOriginal.getTracks()[0];
+ let audioTrack5kClone = osc5kOriginal.clone().getTracks()[0];
+
+ info("Analysing audio output of original stream (1k + 5k)");
+ let stream = new MediaStream();
+ stream.addTrack(audioTrack1kOriginal);
+ stream.addTrack(audioTrack5kOriginal);
+
+ let analyser = new AudioStreamAnalyser(ac, stream);
+ await analyser.waitForAnalysisSuccess(array =>
+ array[analyser.binIndexForFrequency(50)] < 50 &&
+ array[analyser.binIndexForFrequency(1000)] > 200 &&
+ array[analyser.binIndexForFrequency(3000)] < 50 &&
+ array[analyser.binIndexForFrequency(5000)] > 200 &&
+ array[analyser.binIndexForFrequency(10000)] < 50);
+
+ info("Waiting for original tracks to stop");
+ stream.getTracks().forEach(t => t.stop());
+ await analyser.waitForAnalysisSuccess(array =>
+ array[analyser.binIndexForFrequency(50)] < 50 &&
+ // WebAudioDestination streams do not handle stop()
+ // XXX Should they? Plan to resolve that in bug 1208384.
+ // array[analyser.binIndexForFrequency(1000)] < 50 &&
+ array[analyser.binIndexForFrequency(3000)] < 50 &&
+ // array[analyser.binIndexForFrequency(5000)] < 50 &&
+ array[analyser.binIndexForFrequency(10000)] < 50);
+ analyser.disconnect();
+
+ info("Analysing audio output of stream clone (1k + 5k)");
+ stream = new MediaStream();
+ stream.addTrack(audioTrack1kClone);
+ stream.addTrack(audioTrack5kClone);
+
+ analyser = new AudioStreamAnalyser(ac, stream);
+ await analyser.waitForAnalysisSuccess(array =>
+ array[analyser.binIndexForFrequency(50)] < 50 &&
+ array[analyser.binIndexForFrequency(1000)] > 200 &&
+ array[analyser.binIndexForFrequency(3000)] < 50 &&
+ array[analyser.binIndexForFrequency(5000)] > 200 &&
+ array[analyser.binIndexForFrequency(10000)] < 50);
+ analyser.disconnect();
+
+ info("Analysing audio output of clone of clone (1k + 5k)");
+ stream = new MediaStream([audioTrack1kClone, audioTrack5kClone]).clone();
+
+ analyser = new AudioStreamAnalyser(ac, stream);
+ await analyser.waitForAnalysisSuccess(array =>
+ array[analyser.binIndexForFrequency(50)] < 50 &&
+ array[analyser.binIndexForFrequency(1000)] > 200 &&
+ array[analyser.binIndexForFrequency(3000)] < 50 &&
+ array[analyser.binIndexForFrequency(5000)] > 200 &&
+ array[analyser.binIndexForFrequency(10000)] < 50);
+ analyser.disconnect();
+
+ info("Analysing audio output of clone() + addTrack()ed tracks (1k + 5k)");
+ stream = new MediaStream(new MediaStream([ audioTrack1kClone
+ , audioTrack5kClone
+ ]).clone().getTracks());
+
+ analyser = new AudioStreamAnalyser(ac, stream);
+ await analyser.waitForAnalysisSuccess(array =>
+ array[analyser.binIndexForFrequency(50)] < 50 &&
+ array[analyser.binIndexForFrequency(1000)] > 200 &&
+ array[analyser.binIndexForFrequency(3000)] < 50 &&
+ array[analyser.binIndexForFrequency(5000)] > 200 &&
+ array[analyser.binIndexForFrequency(10000)] < 50);
+ analyser.disconnect();
+
+ info("Analysing audio output of clone()d tracks in original stream (1k) " +
+ "and clone()d tracks in stream clone (5k)");
+ stream = new MediaStream([audioTrack1kClone, audioTrack5kClone]);
+ let streamClone = stream.clone();
+
+ stream.getTracks().forEach(t => stream.removeTrack(t));
+ stream.addTrack(streamClone.getTracks()[0]);
+ streamClone.removeTrack(streamClone.getTracks()[0]);
+
+ analyser = new AudioStreamAnalyser(ac, stream);
+ await analyser.waitForAnalysisSuccess(array =>
+ array[analyser.binIndexForFrequency(50)] < 50 &&
+ array[analyser.binIndexForFrequency(1000)] > 200 &&
+ array[analyser.binIndexForFrequency(3000)] < 50 &&
+ array[analyser.binIndexForFrequency(5000)] < 50);
+ analyser.disconnect();
+
+ let cloneAnalyser = new AudioStreamAnalyser(ac, streamClone);
+ await cloneAnalyser.waitForAnalysisSuccess(array =>
+ array[cloneAnalyser.binIndexForFrequency(1000)] < 50 &&
+ array[cloneAnalyser.binIndexForFrequency(3000)] < 50 &&
+ array[cloneAnalyser.binIndexForFrequency(5000)] > 200 &&
+ array[cloneAnalyser.binIndexForFrequency(10000)] < 50);
+ cloneAnalyser.disconnect();
+
+ info("Analysing audio output enabled and disabled tracks that don't affect each other");
+ stream = new MediaStream([audioTrack1kClone, audioTrack5kClone]);
+ let clone = stream.clone();
+
+ stream.getTracks()[0].enabled = true;
+ stream.getTracks()[1].enabled = false;
+
+ clone.getTracks()[0].enabled = false;
+ clone.getTracks()[1].enabled = true;
+
+ analyser = new AudioStreamAnalyser(ac, stream);
+ await analyser.waitForAnalysisSuccess(array =>
+ array[analyser.binIndexForFrequency(50)] < 50 &&
+ array[analyser.binIndexForFrequency(1000)] > 200 &&
+ array[analyser.binIndexForFrequency(3000)] < 50 &&
+ array[analyser.binIndexForFrequency(5000)] < 50);
+ analyser.disconnect();
+
+ cloneAnalyser = new AudioStreamAnalyser(ac, clone);
+ await cloneAnalyser.waitForAnalysisSuccess(array =>
+ array[cloneAnalyser.binIndexForFrequency(1000)] < 50 &&
+ array[cloneAnalyser.binIndexForFrequency(3000)] < 50 &&
+ array[cloneAnalyser.binIndexForFrequency(5000)] > 200 &&
+ array[cloneAnalyser.binIndexForFrequency(10000)] < 50);
+ cloneAnalyser.disconnect();
+
+ // Restore original tracks
+ stream.getTracks().forEach(t => t.enabled = true);
+ }
+
+ gUMStream.getTracks().forEach(t => t.stop());
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_mediaStreamConstructors.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_mediaStreamConstructors.html
new file mode 100644
index 0000000000..4ea6e3f444
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_mediaStreamConstructors.html
@@ -0,0 +1,171 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ "use strict";
+
+ createHTML({
+ title: "MediaStream constructors with getUserMedia streams Test",
+ bug: "1070216"
+ });
+
+ var audioContext = new AudioContext();
+ var videoElement;
+
+ runTest(() => Promise.resolve()
+ .then(() => videoElement = createMediaElement('video', 'constructorsTest'))
+ .then(() => getUserMedia({video: true})).then(gUMStream => {
+ info("Test default constructor with video");
+ ok(gUMStream.active, "gUMStream with one track should be active");
+ var track = gUMStream.getTracks()[0];
+
+ var stream = new MediaStream();
+ ok(!stream.active, "New MediaStream should be inactive");
+ checkMediaStreamContains(stream, [], "Default constructed stream");
+
+ stream.addTrack(track);
+ ok(stream.active, "MediaStream should be active after adding a track");
+ checkMediaStreamContains(stream, [track], "Added video track");
+
+ var playback = new MediaStreamPlayback(videoElement, stream);
+ return playback.playMedia(false).then(() => {
+ ok(!gUMStream.active, "gUMStream should be inactive after stopping");
+ ok(!stream.active, "stream with stopped tracks should be inactive");
+ });
+ })
+ .then(() => getUserMedia({video: true})).then(gUMStream => {
+ info("Test copy constructor with gUM stream");
+ ok(gUMStream.active, "gUMStream with one track should be active");
+ var track = gUMStream.getTracks()[0];
+
+ var stream = new MediaStream(gUMStream);
+ ok(stream.active, "List constructed MediaStream should be active");
+ checkMediaStreamContains(stream, [track], "Copy constructed video track");
+
+ var playback = new MediaStreamPlayback(videoElement, stream);
+ return playback.playMedia(false).then(() => {
+ ok(!gUMStream.active, "gUMStream should be inactive after stopping");
+ ok(!stream.active, "stream with stopped tracks should be inactive");
+ });
+ })
+ .then(() => getUserMedia({video: true})).then(gUMStream => {
+ info("Test list constructor with empty list");
+ ok(gUMStream.active, "gUMStream with one track should be active");
+ var track = gUMStream.getTracks()[0];
+
+ var stream = new MediaStream([]);
+ ok(!stream.active, "Empty-list constructed MediaStream should be inactive");
+ checkMediaStreamContains(stream, [], "Empty-list constructed stream");
+
+ stream.addTrack(track);
+ ok(stream.active, "MediaStream should be active after adding a track");
+ checkMediaStreamContains(stream, [track], "Added video track");
+
+ var playback = new MediaStreamPlayback(videoElement, stream);
+ return playback.playMedia(false).then(() => {
+ ok(!gUMStream.active, "gUMStream should be inactive after stopping");
+ ok(!stream.active, "stream with stopped tracks should be inactive");
+ });
+ })
+ .then(() => getUserMedia({audio: true, video: true})).then(gUMStream => {
+ info("Test list constructor with a gUM audio/video stream");
+ ok(gUMStream.active, "gUMStream with two tracks should be active");
+ var audioTrack = gUMStream.getAudioTracks()[0];
+ var videoTrack = gUMStream.getVideoTracks()[0];
+
+ var stream = new MediaStream([audioTrack, videoTrack]);
+ ok(stream.active, "List constructed MediaStream should be active");
+ checkMediaStreamContains(stream, [audioTrack, videoTrack],
+ "List constructed audio and video tracks");
+
+ var playback = new MediaStreamPlayback(videoElement, stream);
+ return playback.playMedia(false).then(() => {
+ ok(!gUMStream.active, "gUMStream should be inactive after stopping");
+ ok(!stream.active, "stream with stopped tracks should be inactive");
+ });
+ })
+ .then(() => getUserMedia({video: true})).then(gUMStream => {
+ info("Test list constructor with gUM-video and WebAudio tracks");
+ ok(gUMStream.active, "gUMStream with one track should be active");
+ var audioStream = createOscillatorStream(audioContext, 2000);
+ ok(audioStream.active, "WebAudio stream should be active");
+
+ var audioTrack = audioStream.getTracks()[0];
+ var videoTrack = gUMStream.getTracks()[0];
+
+ var stream = new MediaStream([audioTrack, videoTrack]);
+ ok(stream.active, "List constructed MediaStream should be active");
+ checkMediaStreamContains(stream, [audioTrack, videoTrack],
+ "List constructed WebAudio and gUM-video tracks");
+
+ var playback = new MediaStreamPlayback(videoElement, stream);
+ return playback.playMedia(false).then(() => {
+ gUMStream.getTracks().forEach(t => t.stop());
+ ok(!gUMStream.active, "gUMStream should be inactive after stopping");
+ ok(!stream.active, "stream with stopped tracks should be inactive");
+ });
+ })
+ .then(() => {
+ var osc1k = createOscillatorStream(audioContext, 1000);
+ var audioTrack1k = osc1k.getTracks()[0];
+
+ var osc5k = createOscillatorStream(audioContext, 5000);
+ var audioTrack5k = osc5k.getTracks()[0];
+
+ var osc10k = createOscillatorStream(audioContext, 10000);
+ var audioTrack10k = osc10k.getTracks()[0];
+
+ return Promise.resolve().then(() => {
+ info("Analysing audio output with empty default constructed stream");
+ var stream = new MediaStream();
+ var analyser = new AudioStreamAnalyser(audioContext, stream);
+ return analyser.waitForAnalysisSuccess(array =>
+ array[analyser.binIndexForFrequency(1000)] < 50 &&
+ array[analyser.binIndexForFrequency(5000)] < 50 &&
+ array[analyser.binIndexForFrequency(10000)] < 50)
+ .then(() => analyser.disconnect());
+ }).then(() => {
+ info("Analysing audio output with copy constructed 5k stream");
+ var stream = new MediaStream(osc5k);
+ is(stream.active, osc5k.active,
+ "Copy constructed MediaStream should preserve active state");
+ var analyser = new AudioStreamAnalyser(audioContext, stream);
+ return analyser.waitForAnalysisSuccess(array =>
+ array[analyser.binIndexForFrequency(1000)] < 50 &&
+ array[analyser.binIndexForFrequency(5000)] > 200 &&
+ array[analyser.binIndexForFrequency(10000)] < 50)
+ .then(() => analyser.disconnect());
+ }).then(() => {
+ info("Analysing audio output with empty-list constructed stream");
+ var stream = new MediaStream([]);
+ var analyser = new AudioStreamAnalyser(audioContext, stream);
+ return analyser.waitForAnalysisSuccess(array =>
+ array[analyser.binIndexForFrequency(1000)] < 50 &&
+ array[analyser.binIndexForFrequency(5000)] < 50 &&
+ array[analyser.binIndexForFrequency(10000)] < 50)
+ .then(() => analyser.disconnect());
+ }).then(() => {
+ info("Analysing audio output with list constructed 1k, 5k and 10k tracks");
+ var stream = new MediaStream([audioTrack1k, audioTrack5k, audioTrack10k]);
+ ok(stream.active,
+ "List constructed MediaStream from WebAudio should be active");
+ var analyser = new AudioStreamAnalyser(audioContext, stream);
+ return analyser.waitForAnalysisSuccess(array =>
+ array[analyser.binIndexForFrequency(50)] < 50 &&
+ array[analyser.binIndexForFrequency(1000)] > 200 &&
+ array[analyser.binIndexForFrequency(2500)] < 50 &&
+ array[analyser.binIndexForFrequency(5000)] > 200 &&
+ array[analyser.binIndexForFrequency(7500)] < 50 &&
+ array[analyser.binIndexForFrequency(10000)] > 200 &&
+ array[analyser.binIndexForFrequency(11000)] < 50)
+ .then(() => analyser.disconnect());
+ });
+ }));
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_mediaStreamTrackClone.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_mediaStreamTrackClone.html
new file mode 100644
index 0000000000..e5e0764427
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_mediaStreamTrackClone.html
@@ -0,0 +1,170 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ "use strict";
+
+ createHTML({
+ title: "MediaStreamTrack.clone()",
+ bug: "1208371"
+ });
+
+ var testSingleTrackClonePlayback = constraints =>
+ getUserMedia(constraints).then(stream => {
+ info("Test clone()ing an " + constraints + " gUM track");
+ var track = stream.getTracks()[0];
+ var clone = track.clone();
+
+ checkMediaStreamTrackCloneAgainstOriginal(clone, track);
+
+ info("Stopping original track");
+ track.stop();
+
+ info("Creating new stream for clone");
+ var cloneStream = new MediaStream([clone]);
+ checkMediaStreamContains(cloneStream, [clone]);
+
+ info("Testing playback of track clone");
+ var test = createMediaElement('video', 'testClonePlayback');
+ var playback = new MediaStreamPlayback(test, cloneStream);
+ return playback.playMedia(false);
+ });
+
+ runTest(() => Promise.resolve()
+ .then(() => testSingleTrackClonePlayback({audio: true}))
+ .then(() => testSingleTrackClonePlayback({video: true}))
+ .then(() => getUserMedia({video: true})).then(stream => {
+ info("Test cloning a track into inception");
+ var track = stream.getTracks()[0];
+ var clone = track;
+ var clones = Array(10).fill().map(() => clone = clone.clone());
+ var inceptionClone = clones.pop();
+ checkMediaStreamTrackCloneAgainstOriginal(inceptionClone, track);
+
+ var cloneStream = new MediaStream();
+ cloneStream.addTrack(inceptionClone);
+
+ // cloneStream is now essentially the same as stream.clone();
+ checkMediaStreamCloneAgainstOriginal(cloneStream, stream);
+
+ var test = createMediaElement('video', 'testClonePlayback');
+ var playback = new MediaStreamPlayback(test, cloneStream);
+ return playback.playMedia(false).then(() => {
+ info("Testing that clones of ended tracks are ended");
+ cloneStream.clone().getTracks().forEach(t =>
+ is(t.readyState, "ended", "Track " + t.id + " should be ended"));
+ })
+ .then(() => {
+ clones.forEach(t => t.stop());
+ track.stop();
+ });
+ })
+ .then(() => getUserMedia({audio: true, video: true})).then(stream => {
+ info("Test adding many track clones to the original stream");
+
+ const LOOPS = 3;
+ for (var i = 0; i < LOOPS; i++) {
+ stream.getTracks().forEach(t => stream.addTrack(t.clone()));
+ }
+ is(stream.getVideoTracks().length, Math.pow(2, LOOPS),
+ "The original track should contain the original video track and all the video clones");
+ stream.getTracks().forEach(t1 => is(stream.getTracks()
+ .filter(t2 => t1.id == t2.id)
+ .length,
+ 1, "Each track should be unique"));
+
+ var test = createMediaElement('video', 'testClonePlayback');
+ var playback = new MediaStreamPlayback(test, stream);
+ return playback.playMedia(false);
+ })
+ .then(() => {
+ info("Testing audio content routing with MediaStreamTrack.clone()");
+ var ac = new AudioContext();
+
+ var osc1kOriginal = createOscillatorStream(ac, 1000);
+ var audioTrack1kOriginal = osc1kOriginal.getTracks()[0];
+ var audioTrack1kClone = audioTrack1kOriginal.clone();
+
+ var osc5kOriginal = createOscillatorStream(ac, 5000);
+ var audioTrack5kOriginal = osc5kOriginal.getTracks()[0];
+ var audioTrack5kClone = audioTrack5kOriginal.clone();
+
+ return Promise.resolve().then(() => {
+ info("Analysing audio output enabled and disabled tracks that don't affect each other");
+ audioTrack1kOriginal.enabled = true;
+ audioTrack5kOriginal.enabled = false;
+
+ audioTrack1kClone.enabled = false;
+ audioTrack5kClone.enabled = true;
+
+ var analyser =
+ new AudioStreamAnalyser(ac, new MediaStream([audioTrack1kOriginal,
+ audioTrack5kOriginal]));
+ return analyser.waitForAnalysisSuccess(array =>
+ array[analyser.binIndexForFrequency(50)] < 50 &&
+ array[analyser.binIndexForFrequency(1000)] > 200 &&
+ array[analyser.binIndexForFrequency(3000)] < 50 &&
+ array[analyser.binIndexForFrequency(5000)] < 50)
+ .then(() => analyser.disconnect())
+ .then(() => {
+ var cloneAnalyser =
+ new AudioStreamAnalyser(ac, new MediaStream([audioTrack1kClone,
+ audioTrack5kClone]));
+ return cloneAnalyser.waitForAnalysisSuccess(array =>
+ array[cloneAnalyser.binIndexForFrequency(1000)] < 50 &&
+ array[cloneAnalyser.binIndexForFrequency(3000)] < 50 &&
+ array[cloneAnalyser.binIndexForFrequency(5000)] > 200 &&
+ array[cloneAnalyser.binIndexForFrequency(10000)] < 50)
+ .then(() => cloneAnalyser.disconnect());
+ })
+ // Restore original tracks
+ .then(() => [audioTrack1kOriginal,
+ audioTrack5kOriginal,
+ audioTrack1kClone,
+ audioTrack5kClone].forEach(t => t.enabled = true));
+ }).then(() => {
+ info("Analysing audio output of 1k original and 5k clone.");
+ var stream = new MediaStream();
+ stream.addTrack(audioTrack1kOriginal);
+ stream.addTrack(audioTrack5kClone);
+
+ var analyser = new AudioStreamAnalyser(ac, stream);
+ return analyser.waitForAnalysisSuccess(array =>
+ array[analyser.binIndexForFrequency(50)] < 50 &&
+ array[analyser.binIndexForFrequency(1000)] > 200 &&
+ array[analyser.binIndexForFrequency(3000)] < 50 &&
+ array[analyser.binIndexForFrequency(5000)] > 200 &&
+ array[analyser.binIndexForFrequency(10000)] < 50)
+ .then(() => {
+ info("Waiting for tracks to stop");
+ stream.getTracks().forEach(t => t.stop());
+ return analyser.waitForAnalysisSuccess(array =>
+ array[analyser.binIndexForFrequency(50)] < 50 &&
+ array[analyser.binIndexForFrequency(1000)] < 50 &&
+ array[analyser.binIndexForFrequency(3000)] < 50 &&
+ array[analyser.binIndexForFrequency(5000)] < 50 &&
+ array[analyser.binIndexForFrequency(10000)] < 50);
+ }).then(() => analyser.disconnect());
+ }).then(() => {
+ info("Analysing audio output of clones of clones (1kx2 + 5kx4)");
+ var stream = new MediaStream([audioTrack1kClone.clone(),
+ audioTrack5kOriginal.clone().clone().clone().clone()]);
+
+ var analyser = new AudioStreamAnalyser(ac, stream);
+ return analyser.waitForAnalysisSuccess(array =>
+ array[analyser.binIndexForFrequency(50)] < 50 &&
+ array[analyser.binIndexForFrequency(1000)] > 200 &&
+ array[analyser.binIndexForFrequency(3000)] < 50 &&
+ array[analyser.binIndexForFrequency(5000)] > 200 &&
+ array[analyser.binIndexForFrequency(10000)] < 50)
+ .then(() => analyser.disconnect());
+ });
+ }));
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_nonDefaultRate.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_nonDefaultRate.html
new file mode 100644
index 0000000000..8a6ac8c62b
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_nonDefaultRate.html
@@ -0,0 +1,37 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ title: "getUserMedia feed to a graph with non default rate",
+ bug: "1387454",
+ });
+
+ /**
+ * Run a test to verify that when we use the streem from a gUM to an AudioContext
+ * with non default rate the connection fails. (gUM is always on default rate).
+ */
+ runTest(async () => {
+ // Since we do not examine the stream we do not need loopback.
+ DISABLE_LOOPBACK_TONE = true;
+ const stream = await getUserMedia({audio: true});
+ const nonDefaultRate = 32000;
+ const ac = new AudioContext({sampleRate: nonDefaultRate});
+ mustThrowWith(
+ "Connect stream with graph of different sample rate",
+ "NotSupportedError", () => {
+ ac.createMediaStreamSource(stream);
+ }
+ );
+ for (let t of stream.getTracks()) {
+ t.stop();
+ }
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_peerIdentity.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_peerIdentity.html
new file mode 100644
index 0000000000..c4dfb9acb8
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_peerIdentity.html
@@ -0,0 +1,51 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+ <script type="application/javascript" src="blacksilence.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+ title: "Test getUserMedia peerIdentity Constraint",
+ bug: "942367"
+});
+async function theTest() {
+ async function testPeerIdentityConstraint(withConstraint) {
+ const config = { audio: true, video: true };
+ if (withConstraint) {
+ config.peerIdentity = 'user@example.com';
+ }
+ info('getting media with constraints: ' + JSON.stringify(config));
+ const stream = await getUserMedia(config);
+ for (const track of stream.getTracks()) {
+ const recorder = new MediaRecorder(new MediaStream([track]));
+ try {
+ recorder.start();
+ ok(!withConstraint,
+ `gUM ${track.kind} track without peerIdentity must not throw`);
+ recorder.stop();
+ } catch(e) {
+ ok(withConstraint,
+ `gUM ${track.kind} track with peerIdentity must throw`);
+ }
+ }
+ await Promise.all([
+ audioIsSilence(withConstraint, stream),
+ videoIsBlack(withConstraint, stream),
+ ]);
+ stream.getTracks().forEach(t => t.stop());
+ };
+
+ // both without and with the constraint
+ await testPeerIdentityConstraint(false);
+ await testPeerIdentityConstraint(true);
+}
+
+runTest(theTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_permission.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_permission.html
new file mode 100644
index 0000000000..cd02c7326c
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_permission.html
@@ -0,0 +1,104 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({ title: "Test getUserMedia in iframes", bug: "1371741" });
+/**
+ Tests covering enumerateDevices API and deviceId constraint. Exercise code.
+*/
+
+// Call gUM in iframe.
+async function iframeGum(dict, iframe = document.createElement("iframe")) {
+ Object.assign(iframe, dict);
+ if (dict.src) {
+ info(`<iframe src="${dict.src}" sandbox="${dict.sandbox}">`);
+ } else {
+ info(`<iframe srcdoc sandbox="${dict.sandbox}">`);
+ }
+ document.documentElement.appendChild(iframe);
+
+ const once = (t, msg) => new Promise(r => t.addEventListener(msg, r, { once: true }));
+ const haveMessage = once(window, "message");
+ await new Promise(resolve => iframe.onload = resolve);
+ return (await haveMessage).data;
+};
+
+runTest(async () => {
+ const path = "/tests/dom/media/webrtc/tests/mochitests/test_getUserMedia_permission_iframe.html";
+
+ async function sourceFn() {
+ try {
+ const gUM = c => navigator.mediaDevices.getUserMedia(c);
+ let message;
+ let stream;
+ try {
+ stream = await gUM({ video: true });
+ message = 'success';
+ } catch(e) {
+ message = e.name;
+ }
+ parent.postMessage(message, 'https://example.com:443');
+
+ if (message == "success") {
+ stream.getTracks().forEach(track => track.stop());
+ }
+ } catch (e) {
+ setTimeout(() => { throw e; });
+ }
+ }
+
+ const source = `<html\><script\>(${sourceFn.toString()})()</script\></html\>`;
+
+ // Test gUM in sandboxed vs. regular iframe.
+
+ for (const origin of [window.location.origin, "https://test1.example.com"]) {
+ const src = origin + path;
+ is(await iframeGum({ src, sandbox: "allow-scripts" }),
+ "NotAllowedError", "gUM fails in sandboxed iframe " + origin);
+ }
+ is(await iframeGum({
+ src: path,
+ sandbox: "allow-scripts allow-same-origin",
+ }),
+ "success", "gUM works in regular same-origin iframe");
+ is(await iframeGum({
+ src: `https://test1.example.com${path}`,
+ sandbox: "allow-scripts allow-same-origin",
+ }),
+ "NotAllowedError", "gUM fails in regular cross-origin iframe");
+
+ // Test gUM in sandboxed vs regular srcdoc iframe
+
+ const iframeSrcdoc = document.createElement("iframe");
+ iframeSrcdoc.srcdoc = source;
+ is(await iframeGum({ sandbox: "allow-scripts" }, iframeSrcdoc),
+ "NotAllowedError", "gUM fails in sandboxed srcdoc iframe");
+ is(await iframeGum({ sandbox: "allow-scripts allow-same-origin" }, iframeSrcdoc),
+ "success", "gUM works in regular srcdoc iframe");
+
+ // Test gUM in sandboxed vs regular blob iframe
+
+ const blob = new Blob([source], {type : "text/html"});
+ let src = URL.createObjectURL(blob);
+ is(await iframeGum({ src, sandbox: "allow-scripts" }),
+ "NotAllowedError", "gUM fails in sandboxed blob iframe");
+ is(await iframeGum({ src, sandbox: "allow-scripts allow-same-origin"}),
+ "success", "gUM works in regular blob iframe");
+ URL.revokeObjectURL(src);
+
+ // data iframes always have null-principals
+
+ src = `data:text/html;base64,${btoa(source)}`;
+ is(await iframeGum({ src, sandbox: "allow-scripts" }),
+ "NotAllowedError", "gUM fails in sandboxed data iframe");
+ is(await iframeGum({ src, sandbox: "allow-scripts allow-same-origin"}),
+ "NotAllowedError", "gUM fails in regular data iframe");
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_permission_iframe.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_permission_iframe.html
new file mode 100644
index 0000000000..732c2cf98c
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_permission_iframe.html
@@ -0,0 +1,30 @@
+<!DOCTYPE HTML>
+<html>
+<body>
+<pre id="test">
+<script type="application/javascript">
+/**
+ Runs inside iframe in test_getUserMedia_permission.html.
+*/
+
+const gUM = c => navigator.mediaDevices.getUserMedia(c);
+
+(async () => {
+ let message;
+ let stream;
+ try {
+ stream = await gUM({ video: true });
+ message = "success";
+ } catch(e) {
+ message = e.name;
+ }
+ parent.postMessage(message, "https://example.com:443");
+
+ if (message == "success") {
+ stream.getTracks().forEach(track => track.stop());
+ }
+})().catch(e => setTimeout(() => { throw e; }));
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_playAudioTwice.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_playAudioTwice.html
new file mode 100644
index 0000000000..30d168bf38
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_playAudioTwice.html
@@ -0,0 +1,25 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({title: "getUserMedia Play Audio Twice", bug: "822109" });
+ /**
+ * Run a test that we can complete an audio playback cycle twice in a row.
+ */
+ runTest(function () {
+ return getUserMedia({audio: true}).then(audioStream => {
+ var testAudio = createMediaElement('audio', 'testAudio');
+ var playback = new MediaStreamPlayback(testAudio, audioStream);
+
+ return playback.playMediaWithoutStoppingTracks(false)
+ .then(() => playback.playMedia(true));
+ });
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_playVideoAudioTwice.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_playVideoAudioTwice.html
new file mode 100644
index 0000000000..7b5e6effd1
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_playVideoAudioTwice.html
@@ -0,0 +1,26 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({title: "getUserMedia Play Video and Audio Twice", bug: "822109" });
+ /**
+ * Run a test that we can complete a video playback cycle twice in a row.
+ */
+ runTest(function () {
+ return getUserMedia({video: true, audio: true}).then(stream => {
+ var testVideo = createMediaElement('video', 'testVideo');
+ var playback = new MediaStreamPlayback(testVideo, stream);
+
+ return playback.playMediaWithoutStoppingTracks(false)
+ .then(() => playback.playMedia(true));
+ });
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_playVideoTwice.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_playVideoTwice.html
new file mode 100644
index 0000000000..2890f45eab
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_playVideoTwice.html
@@ -0,0 +1,26 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({ title: "getUserMedia Play Video Twice", bug: "822109" });
+ /**
+ * Run a test that we can complete a video playback cycle twice in a row.
+ */
+ runTest(function () {
+ return getUserMedia({video: true}).then(stream => {
+ var testVideo = createMediaElement('video', 'testVideo');
+ var streamPlayback = new MediaStreamPlayback(testVideo, stream);
+
+ return streamPlayback.playMediaWithoutStoppingTracks(false)
+ .then(() => streamPlayback.playMedia(true));
+ });
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_scarySources.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_scarySources.html
new file mode 100644
index 0000000000..782110823e
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_scarySources.html
@@ -0,0 +1,51 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+
+createHTML({title: "Detect screensharing sources that are firefox", bug: "1311048"});
+
+const Services = SpecialPowers.Services;
+
+let observe = topic => new Promise(r => Services.obs.addObserver(function o(...args) {
+ Services.obs.removeObserver(o, topic);
+ r(args.map(x => SpecialPowers.wrap(x)));
+}, topic));
+
+let getDevices = async constraints => {
+ SpecialPowers.wrap(document).notifyUserGestureActivation();
+ let [{ windowID, innerWindowID, callID, devices }] = await Promise.race([
+ getUserMedia(constraints),
+ observe("getUserMedia:request")
+ ]);
+ let window = Services.wm.getOuterWindowWithId(windowID);
+ return devices.map(SpecialPowers.wrapCallback(d => d.QueryInterface(Ci.nsIMediaDevice)));
+};
+
+runTest(async () => {
+ await pushPrefs(["media.navigator.permission.disabled", true],
+ ["media.navigator.permission.force", true]);
+ let devices = await getDevices({video: { mediaSource: "window" }});
+ ok(devices.length, "Found one or more windows.");
+ devices = Array.prototype.filter.call(devices, d => d.scary);
+ ok(devices.length, "Found one or more scary windows (our own counts).");
+ devices = devices.filter(d => d.rawName.includes("MochiTest"));
+ ok(devices.length,
+ "Our own window is among the scary: "
+ + devices.map(d => `"${d.rawName}"`));
+
+ devices = await getDevices({video: { mediaSource: "screen" }});
+ let numScreens = devices.length;
+ ok(numScreens, "Found one or more screens.");
+ devices = Array.prototype.filter.call(devices, d => d.scary);
+ is(devices.length, numScreens, "All screens are scary.");
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_spinEventLoop.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_spinEventLoop.html
new file mode 100644
index 0000000000..ae691785f5
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_spinEventLoop.html
@@ -0,0 +1,28 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({ title: "getUserMedia Basic Audio Test", bug: "1208656" });
+ /**
+ * Run a test to verify that we can spin the event loop from within a mozGUM callback.
+ */
+ runTest(() => {
+ var testAudio = createMediaElement('audio', 'testAudio');
+ return new Promise((resolve, reject) => {
+ navigator.mozGetUserMedia({ audio: true }, stream => {
+ SpecialPowers.spinEventLoop(window);
+ ok(true, "Didn't crash");
+ stream.getTracks().forEach(t => t.stop());
+ resolve();
+ }, () => {});
+ });
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_trackCloneCleanup.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_trackCloneCleanup.html
new file mode 100644
index 0000000000..60077ec73b
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_trackCloneCleanup.html
@@ -0,0 +1,32 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ "use strict";
+
+ createHTML({
+ title: "Stopping a MediaStreamTrack and its clones should deallocate the device",
+ bug: "1294605"
+ });
+
+ runTest(async () => {
+ await pushPrefs(["media.navigator.permission.fake", true]);
+ const stream = await getUserMedia({audio: true, video: true, fake: true});
+ const clone = stream.clone();
+ stream.getTracks().forEach(t => t.stop());
+ stream.clone().getTracks().forEach(t => stream.addTrack(t));
+ is(stream.getTracks().filter(t => t.readyState == "live").length, 0,
+ "Cloning ended tracks should make them ended");
+ [...stream.getTracks(), ...clone.getTracks()].forEach(t => t.stop());
+
+ // Bug 1295352: better to be explicit about noGum here wrt future refactoring.
+ return noGum();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_trackEnded.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_trackEnded.html
new file mode 100644
index 0000000000..b275f4555f
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_trackEnded.html
@@ -0,0 +1,68 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<iframe id="iframe" onload="iframeLoaded()" srcdoc="
+ <script type='application/javascript'>
+ document.gUM = (constraints, success, failure) =>
+ navigator.mediaDevices.getUserMedia(constraints).then(success, failure);
+ </script>">
+</iframe>
+<script type="application/javascript">
+ "use strict";
+ let iframeLoadedPromise = {};
+ iframeLoadedPromise.promise = new Promise(r => {
+ iframeLoadedPromise.resolve = r;
+ });;
+ function iframeLoaded() {
+ iframeLoadedPromise.resolve();
+ }
+
+ createHTML({
+ title: "getUserMedia MediaStreamTrack 'ended' event on navigating",
+ bug: "1208373",
+ });
+
+ runTest(async () => {
+ await iframeLoadedPromise.promise;
+ let iframe = document.getElementById("iframe");
+ let stream;
+ // We're passing callbacks into a method in the iframe here, because
+ // a Promise created in the iframe is unusable after the iframe has
+ // navigated away (see bug 1269400 for details).
+ return new Promise((resolve, reject) =>
+ iframe.contentDocument.gUM({audio: true, video: true}, resolve, reject))
+ .then(s => {
+ // We're cloning a stream containing identical tracks (an original
+ // and its clone) to test that ended works both for originals
+ // clones when they're both owned by the same MediaStream.
+ // (Bug 1274221)
+ stream = new MediaStream([].concat(s.getTracks(), s.getTracks())
+ .map(t => t.clone())).clone();
+ var allTracksEnded = Promise.all(stream.getTracks().map(t => {
+ info("Set up ended handler for track " + t.id);
+ return haveEvent(t, "ended", wait(50000))
+ .then(event => {
+ info("ended handler invoked for track " + t.id);
+ is(event.target, t, "Target should be correct");
+ }, e => e ? Promise.reject(e)
+ : ok(false, "ended event never raised for track " + t.id));
+ }));
+ stream.getTracks().forEach(t =>
+ is(t.readyState, "live",
+ "Non-ended track should have readyState 'live'"));
+ iframe.srcdoc = "";
+ info("iframe has been reset. Waiting for tracks to end.");
+ return allTracksEnded;
+ })
+ .then(() => stream.getTracks().forEach(t =>
+ is(t.readyState, "ended",
+ "Ended track should have readyState 'ended'")));
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_groupId.html b/dom/media/webrtc/tests/mochitests/test_groupId.html
new file mode 100644
index 0000000000..f2aefe5e80
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_groupId.html
@@ -0,0 +1,53 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({ title: "Test group id of MediaDeviceInfo", bug: "1213453" });
+
+async function getDefaultDevices() {
+ const devices = await navigator.mediaDevices.enumerateDevices();
+ is(devices.length, 2, "Two fake devices found.");
+
+ devices.forEach(d => isnot(d.groupId, "", "GroupId is included in every device"));
+
+ const videos = devices.filter(d => d.kind == "videoinput");
+ is(videos.length, 1, "One video device found.");
+ const audios = devices.filter(d => d.kind == "audioinput");
+ is(audios.length, 1, "One microphone device found.");
+
+ return {audio: audios[0], video: videos[0]};
+}
+
+runTest(async () => {
+ // Force fake devices in order to be able to change camera name by pref.
+ await pushPrefs(["media.navigator.streams.fake", true],
+ ["media.audio_loopback_dev", ""],
+ ["media.video_loopback_dev", ""]);
+
+ const afterGum = await navigator.mediaDevices.getUserMedia({
+ video: true, audio: true
+ });
+ afterGum.getTracks().forEach(track => track.stop());
+
+ let {audio, video} = await getDefaultDevices();
+
+ /* The low level method to correlate groupIds is by device names.
+ * Use a similar comparison here to verify that it works.
+ * Multiple devices of the same device name are not expected in
+ * automation. */
+ isnot(audio.label, video.label, "Audio label differs from video");
+ isnot(audio.groupId, video.groupId, "Not the same groupIds");
+ // Change video name to match.
+ await pushPrefs(["media.getusermedia.fake-camera-name", audio.label]);
+ ({audio, video} = await getDefaultDevices());
+ is(audio.label, video.label, "Audio label matches video");
+ is(audio.groupId, video.groupId, "GroupIds should be the same");
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_multi_mics.html b/dom/media/webrtc/tests/mochitests/test_multi_mics.html
new file mode 100644
index 0000000000..95bcbfd3e4
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_multi_mics.html
@@ -0,0 +1,66 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+"use strict";
+
+createHTML({
+ title: "Test the ability of opening multiple microphones via gUM",
+ bug: "1238038",
+});
+
+runTest(async () => {
+ // Ensure we use the real microphones by disabling loopback devices and fake devices.
+ await pushPrefs(["media.audio_loopback_dev", ""], ["media.navigator.streams.fake", false]);
+
+ try {
+ let devices = await navigator.mediaDevices.enumerateDevices();
+ // Create constraints
+ let constraints = [];
+ devices.forEach((device) => {
+ if (device.kind === "audioinput") {
+ constraints.push({
+ audio: { deviceId: { exact: device.deviceId } },
+ });
+ }
+ });
+ if (constraints.length >= 2) {
+ // Create constraints
+ let constraints = [];
+ devices.forEach((device) => {
+ if (device.kind === "audioinput") {
+ constraints.push({
+ audio: { deviceId: { exact: device.deviceId } },
+ });
+ }
+ });
+ // Open microphones by the constraints
+ let mediaStreams = [];
+ for (let c of constraints) {
+ let stream = await navigator.mediaDevices.getUserMedia(c);
+ dump("MediaStream: " + stream.id + " for device: " + c.audio.deviceId.exact + " is created\n");
+ mediaStreams.push(stream);
+ }
+ // Close microphones
+ for (let stream of mediaStreams) {
+ for (let track of stream.getTracks()) {
+ track.stop();
+ }
+ dump("Stop all tracks in MediaStream: " + stream.id + "\n");
+ }
+ mediaStreams = [];
+ } else {
+ dump("Skip test since we need at least two microphones\n");
+ }
+ } catch (e) {
+ ok(false, e.name + ": " + e.message);
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_ondevicechange.html b/dom/media/webrtc/tests/mochitests/test_ondevicechange.html
new file mode 100644
index 0000000000..4358d9d748
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_ondevicechange.html
@@ -0,0 +1,180 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<script type="application/javascript">
+"use strict";
+
+createHTML({
+ title: "ondevicechange tests",
+ bug: "1152383"
+});
+
+async function resolveOnEvent(target, name) {
+ return new Promise(r => target.addEventListener(name, r, {once: true}));
+}
+let eventCount = 0;
+async function triggerVideoDevicechange() {
+ ++eventCount;
+ // "media.getusermedia.fake-camera-name" specifies the name of the single
+ // fake video camera.
+ // Changing the pref imitates replacing one device with another.
+ return pushPrefs(["media.getusermedia.fake-camera-name",
+ `devicechange ${eventCount}`])
+}
+function addIframe() {
+ const iframe = document.createElement("iframe");
+ // Workaround for bug 1743933
+ iframe.loadPromise = resolveOnEvent(iframe, "load");
+ document.documentElement.appendChild(iframe);
+ return iframe;
+}
+
+runTest(async () => {
+ // A toplevel Window and an iframe Windows are compared for devicechange
+ // events.
+ const iframe1 = addIframe();
+ const iframe2 = addIframe();
+ await Promise.all([
+ iframe1.loadPromise,
+ iframe2.loadPromise,
+ pushPrefs(
+ // Use the fake video backend to trigger devicechange events.
+ ["media.navigator.streams.fake", true],
+ // Loopback would override fake.
+ ["media.video_loopback_dev", ""],
+ // Make fake devices count as real, permission-wise, or devicechange
+ // events won't be exposed
+ ["media.navigator.permission.fake", true],
+ // For gUM.
+ ["media.navigator.permission.disabled", true]
+ ),
+ ]);
+ const topDevices = navigator.mediaDevices;
+ const frame1Devices = iframe1.contentWindow.navigator.mediaDevices;
+ const frame2Devices = iframe2.contentWindow.navigator.mediaDevices;
+ // Initialization of MediaDevices::mLastPhysicalDevices is triggered when
+ // ondevicechange is set but tests "media.getusermedia.fake-camera-name"
+ // asynchronously. Wait for getUserMedia() completion to ensure that the
+ // pref has been read before doDevicechanges() changes it.
+ frame1Devices.ondevicechange = () => {};
+ const topEventPromise = resolveOnEvent(topDevices, "devicechange");
+ const frame2EventPromise = resolveOnEvent(frame2Devices, "devicechange");
+ (await frame1Devices.getUserMedia({video: true})).getTracks()[0].stop();
+
+ await Promise.all([
+ resolveOnEvent(frame1Devices, "devicechange"),
+ triggerVideoDevicechange(),
+ ]);
+ ok(true,
+ "devicechange event is fired when gUM has been in use");
+ // The number of devices has not changed. Race a settled Promise to check
+ // that no devicechange event has been received in frame2.
+ const racer = {};
+ is(await Promise.race([frame2EventPromise, racer]), racer,
+ "devicechange event is NOT fired in iframe2 for replaced device when " +
+ "gUM has NOT been in use");
+ // getUserMedia() is invoked on frame2Devices after a first device list
+ // change but before returning to the previous state, in order to test that
+ // the device set is compared with the set after previous device list
+ // changes regardless of whether a "devicechange" event was previously
+ // dispatched.
+ (await frame2Devices.getUserMedia({video: true})).getTracks()[0].stop();
+ // Revert device list change.
+ await Promise.all([
+ resolveOnEvent(frame1Devices, "devicechange"),
+ resolveOnEvent(frame2Devices, "devicechange"),
+ SpecialPowers.popPrefEnv(),
+ ]);
+ ok(true,
+ "devicechange event is fired on return to previous list " +
+ "after gUM has been is use");
+
+ const frame1EventPromise1 = resolveOnEvent(frame1Devices, "devicechange");
+ while (true) {
+ const racePromise = Promise.race([
+ frame1EventPromise1,
+ // 100ms is half the coalescing time in MediaManager::DeviceListChanged().
+ wait(100, {type: "wait done"}),
+ ]);
+ await triggerVideoDevicechange();
+ if ((await racePromise).type == "devicechange") {
+ ok(true,
+ "devicechange event is fired even when hardware changes continue");
+ break;
+ }
+ }
+
+ is(await Promise.race([topEventPromise, racer]), racer,
+ "devicechange event is NOT fired for device replacements when " +
+ "gUM has NOT been in use");
+
+ if (navigator.userAgent.includes("Android")) {
+ todo(false, "test assumes Firefox-for-Desktop specific API and behavior");
+ return;
+ }
+ // Open a new tab, which is expected to receive focus and hide the first tab.
+ const tab = window.open();
+ SimpleTest.registerCleanupFunction(() => tab.close());
+ await Promise.all([
+ resolveOnEvent(document, 'visibilitychange'),
+ resolveOnEvent(tab, 'focus'),
+ ]);
+ ok(tab.document.hasFocus(), "tab.document.hasFocus()");
+ await Promise.all([
+ resolveOnEvent(tab, 'blur'),
+ SpecialPowers.spawnChrome([], function focusUrlBar() {
+ this.browsingContext.topChromeWindow.gURLBar.focus();
+ }),
+ ]);
+ ok(!tab.document.hasFocus(), "!tab.document.hasFocus()");
+ is(document.visibilityState, 'hidden', 'visibilityState')
+ const frame1EventPromise2 = resolveOnEvent(frame1Devices, "devicechange");
+ const tabDevices = tab.navigator.mediaDevices;
+ tabDevices.ondevicechange = () => {};
+ const tabStream = await tabDevices.getUserMedia({video: true});
+ // Trigger and await two devicechanges on tabDevices to wait long enough to
+ // provide that a devicechange on another MediaDevices would be received.
+ for (let i = 0; i < 2; ++i) {
+ await Promise.all([
+ resolveOnEvent(tabDevices, "devicechange"),
+ triggerVideoDevicechange(),
+ ]);
+ };
+ is(await Promise.race([frame1EventPromise2, racer]), racer,
+ "devicechange event is NOT fired while tab is in background");
+ tab.close();
+ await resolveOnEvent(document, 'visibilitychange');
+ is(document.visibilityState, 'visible', 'visibilityState')
+ await frame1EventPromise2;
+ ok(true, "devicechange event IS fired when tab returns to foreground");
+
+ const audioLoopbackDev =
+ SpecialPowers.getCharPref("media.audio_loopback_dev", "");
+ if (!navigator.userAgent.includes("Linux")) {
+ todo_isnot(audioLoopbackDev, "", "audio_loopback_dev");
+ return;
+ }
+ isnot(audioLoopbackDev, "", "audio_loopback_dev");
+ await Promise.all([
+ resolveOnEvent(topDevices, "devicechange"),
+ pushPrefs(["media.audio_loopback_dev", "none"]),
+ ]);
+ ok(true,
+ "devicechange event IS fired when last audio device is removed and " +
+ "gUM has NOT been in use");
+ await Promise.all([
+ resolveOnEvent(topDevices, "devicechange"),
+ pushPrefs(["media.audio_loopback_dev", audioLoopbackDev]),
+ ]);
+ ok(true,
+ "devicechange event IS fired when first audio device is added and " +
+ "gUM has NOT been in use");
+});
+
+</script>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_addAudioTrackToExistingVideoStream.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_addAudioTrackToExistingVideoStream.html
new file mode 100644
index 0000000000..b09d7ffeb5
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_addAudioTrackToExistingVideoStream.html
@@ -0,0 +1,55 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1246310",
+ title: "Renegotiation: add audio track to existing video-only stream",
+ });
+
+ runNetworkTest(function (options) {
+ SimpleTest.requestCompleteLog();
+ const test = new PeerConnectionTest(options);
+ test.chain.replace("PC_LOCAL_GUM",
+ [
+ function PC_LOCAL_GUM_ATTACH_VIDEO_ONLY(test) {
+ var localConstraints = {audio: true, video: true};
+ test.setMediaConstraints([{video: true}], []);
+ return getUserMedia(localConstraints)
+ .then(s => test.originalGumStream = s)
+ .then(() => is(test.originalGumStream.getAudioTracks().length, 1,
+ "Should have 1 audio track"))
+ .then(() => is(test.originalGumStream.getVideoTracks().length, 1,
+ "Should have 1 video track"))
+ .then(() => test.pcLocal.attachLocalTrack(
+ test.originalGumStream.getVideoTracks()[0],
+ test.originalGumStream));
+ },
+ ]
+ );
+ addRenegotiation(test.chain,
+ [
+ function PC_LOCAL_ATTACH_SECOND_TRACK_AUDIO(test) {
+ test.setMediaConstraints([{audio: true, video: true}], []);
+ return test.pcLocal.attachLocalTrack(
+ test.originalGumStream.getAudioTracks()[0],
+ test.originalGumStream);
+ },
+ ],
+ [
+ function PC_CHECK_REMOTE_AUDIO_FLOW(test) {
+ return test.pcRemote.checkReceivingToneFrom(new AudioContext(), test.pcLocal);
+ }
+ ]
+ );
+
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_addDataChannel.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_addDataChannel.html
new file mode 100644
index 0000000000..c7536214e5
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_addDataChannel.html
@@ -0,0 +1,33 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1017888",
+ title: "Renegotiation: add DataChannel"
+ });
+
+ runNetworkTest(function (options) {
+ const test = new PeerConnectionTest(options);
+ addRenegotiation(test.chain,
+ commandsCreateDataChannel,
+ commandsCheckDataChannel);
+
+ // Insert before the second PC_LOCAL_WAIT_FOR_MEDIA_FLOW
+ test.chain.insertBefore('PC_LOCAL_WAIT_FOR_MEDIA_FLOW',
+ commandsWaitForDataChannel,
+ false,
+ 1);
+
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ return test.run();
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_addDataChannelNoBundle.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_addDataChannelNoBundle.html
new file mode 100644
index 0000000000..6ad754336c
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_addDataChannelNoBundle.html
@@ -0,0 +1,44 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1017888",
+ title: "Renegotiation: add DataChannel"
+ });
+
+ runNetworkTest(function (options) {
+ options = options || { };
+ options.bundle = false;
+ const test = new PeerConnectionTest(options);
+ addRenegotiation(test.chain,
+ commandsCreateDataChannel.concat(
+ [
+ function PC_LOCAL_EXPECT_ICE_CHECKING(test) {
+ test.pcLocal.expectIceChecking();
+ },
+ function PC_REMOTE_EXPECT_ICE_CHECKING(test) {
+ test.pcRemote.expectIceChecking();
+ },
+ ]
+ ),
+ commandsCheckDataChannel);
+
+ // Insert before the second PC_LOCAL_WAIT_FOR_MEDIA_FLOW
+ test.chain.insertBefore('PC_LOCAL_WAIT_FOR_MEDIA_FLOW',
+ commandsWaitForDataChannel,
+ false,
+ 1);
+
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ return test.run();
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_addSecondAudioStream.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_addSecondAudioStream.html
new file mode 100644
index 0000000000..61a0250887
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_addSecondAudioStream.html
@@ -0,0 +1,45 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1017888",
+ title: "Renegotiation: add second audio stream"
+ });
+
+ runNetworkTest(function (options) {
+ const test = new PeerConnectionTest(options);
+ addRenegotiation(test.chain,
+ [
+ function PC_LOCAL_ADD_SECOND_STREAM(test) {
+ test.setMediaConstraints([{audio: true}, {audio: true}],
+ [{audio: true}]);
+ return test.pcLocal.getAllUserMediaAndAddStreams([{audio: true}]);
+ },
+ ],
+ [
+ function PC_REMOTE_CHECK_ADDED_TRACK(test) {
+ // We test both tracks to avoid an ordering problem
+ is(test.pcRemote._pc.getReceivers().length, 2,
+ "pcRemote should have two receivers");
+ return Promise.all(test.pcRemote._pc.getReceivers().map(r => {
+ const analyser = new AudioStreamAnalyser(
+ new AudioContext(), new MediaStream([r.track]));
+ const freq = analyser.binIndexForFrequency(TEST_AUDIO_FREQ);
+ return analyser.waitForAnalysisSuccess(arr => arr[freq] > 200);
+ }));
+ },
+ ]
+ );
+
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_addSecondAudioStreamNoBundle.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_addSecondAudioStreamNoBundle.html
new file mode 100644
index 0000000000..32d0564717
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_addSecondAudioStreamNoBundle.html
@@ -0,0 +1,53 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1017888",
+ title: "Renegotiation: add second audio stream, no bundle"
+ });
+
+ runNetworkTest(function (options = {}) {
+ options.bundle = false;
+ const test = new PeerConnectionTest(options);
+ addRenegotiation(test.chain,
+ [
+ function PC_LOCAL_ADD_SECOND_STREAM(test) {
+ test.setMediaConstraints([{audio: true}, {audio: true}],
+ [{audio: true}]);
+ // Since this is a NoBundle variant, adding a track will cause us to
+ // go back to checking.
+ test.pcLocal.expectIceChecking();
+ return test.pcLocal.getAllUserMediaAndAddStreams([{audio: true}]);
+ },
+ function PC_REMOTE_EXPECT_ICE_CHECKING(test) {
+ test.pcRemote.expectIceChecking();
+ },
+ ],
+ [
+ function PC_REMOTE_CHECK_ADDED_TRACK(test) {
+ // We test both tracks to avoid an ordering problem
+ is(test.pcRemote._pc.getReceivers().length, 2,
+ "pcRemote should have two receivers");
+ return Promise.all(test.pcRemote._pc.getReceivers().map(r => {
+ const analyser = new AudioStreamAnalyser(
+ new AudioContext(), new MediaStream([r.track]));
+ const freq = analyser.binIndexForFrequency(TEST_AUDIO_FREQ);
+ return analyser.waitForAnalysisSuccess(arr => arr[freq] > 200);
+ }));
+ },
+ ]
+ );
+
+ // TODO(bug 1093835): figure out how to verify if media flows through the new stream
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_addSecondVideoStream.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_addSecondVideoStream.html
new file mode 100644
index 0000000000..1565958d01
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_addSecondVideoStream.html
@@ -0,0 +1,53 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1017888",
+ title: "Renegotiation: add second video stream"
+ });
+
+ runNetworkTest(async function (options) {
+ // Use fake video here since the native fake device on linux doesn't
+ // change color as needed by checkVideoPlaying() below.
+ await pushPrefs(
+ ['media.video_loopback_dev', ''],
+ ['media.navigator.streams.fake', true]);
+ // [TODO] re-enable HW decoder after bug 1526207 is fixed.
+ if (navigator.userAgent.includes("Android")) {
+ await pushPrefs(["media.navigator.mediadatadecoder_vpx_enabled", false],
+ ["media.webrtc.hw.h264.enabled", false]);
+ }
+
+ const test = new PeerConnectionTest(options);
+ addRenegotiation(test.chain,
+ [
+ function PC_LOCAL_ADD_SECOND_STREAM(test) {
+ test.setMediaConstraints([{video: true}, {video: true}],
+ [{video: true}]);
+ return test.pcLocal.getAllUserMediaAndAddStreams([{video: true}]);
+ },
+ ],
+ [
+ function PC_REMOTE_CHECK_VIDEO_FLOW(test) {
+ const h = new VideoStreamHelper();
+ is(test.pcRemote.remoteMediaElements.length, 2,
+ "Should have two remote media elements after renegotiation");
+ return Promise.all(test.pcRemote.remoteMediaElements.map(video =>
+ h.checkVideoPlaying(video)));
+ },
+ ]
+ );
+
+ test.setMediaConstraints([{video: true, fake: true}], [{video: true}]);
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_addSecondVideoStreamNoBundle.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_addSecondVideoStreamNoBundle.html
new file mode 100644
index 0000000000..2857100998
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_addSecondVideoStreamNoBundle.html
@@ -0,0 +1,60 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1017888",
+ title: "Renegotiation: add second video stream, no bundle"
+ });
+
+ runNetworkTest(async function (options = {}) {
+ // Use fake video here since the native fake device on linux doesn't
+ // change color as needed by checkVideoPlaying() below.
+ await pushPrefs(
+ ['media.video_loopback_dev', ''],
+ ['media.navigator.streams.fake', true]);
+ // [TODO] re-enable HW decoder after bug 1526207 is fixed.
+ if (navigator.userAgent.includes("Android")) {
+ await pushPrefs(["media.navigator.mediadatadecoder_vpx_enabled", false],
+ ["media.webrtc.hw.h264.enabled", false]);
+ }
+
+ options.bundle = false;
+ const test = new PeerConnectionTest(options);
+ addRenegotiation(test.chain,
+ [
+ function PC_LOCAL_ADD_SECOND_STREAM(test) {
+ test.setMediaConstraints([{video: true}, {video: true}],
+ [{video: true}]);
+ // Since this is a NoBundle variant, adding a track will cause us to
+ // go back to checking.
+ test.pcLocal.expectIceChecking();
+ return test.pcLocal.getAllUserMediaAndAddStreams([{video: true}]);
+ },
+ function PC_REMOTE_EXPECT_ICE_CHECKING(test) {
+ test.pcRemote.expectIceChecking();
+ },
+ ],
+ [
+ function PC_REMOTE_CHECK_VIDEO_FLOW(test) {
+ const h = new VideoStreamHelper();
+ is(test.pcRemote.remoteMediaElements.length, 2,
+ "Should have two remote media elements after renegotiation");
+ return Promise.all(test.pcRemote.remoteMediaElements.map(video =>
+ h.checkVideoPlaying(video)));
+ },
+ ]
+ );
+
+ test.setMediaConstraints([{video: true}], [{video: true}]);
+ await test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_addtrack_removetrack_events.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_addtrack_removetrack_events.html
new file mode 100644
index 0000000000..ff9ca9a772
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_addtrack_removetrack_events.html
@@ -0,0 +1,75 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+"use strict";
+
+createHTML({
+ title: "MediaStream's 'addtrack' and 'removetrack' events with gUM",
+ bug: "1208328"
+});
+
+runNetworkTest(function (options) {
+ let test = new PeerConnectionTest(options);
+ let eventsPromise;
+ addRenegotiation(test.chain,
+ [
+ function PC_LOCAL_SWAP_VIDEO_TRACKS(test) {
+ return getUserMedia({video: true}).then(stream => {
+ var videoTransceiver = test.pcLocal._pc.getTransceivers()[1];
+ is(videoTransceiver.currentDirection, "sendonly",
+ "Video transceiver's current direction is sendonly");
+ is(videoTransceiver.direction, "sendrecv",
+ "Video transceiver's desired direction is sendrecv");
+
+ const localStream = test.pcLocal._pc.getLocalStreams()[0];
+ ok(localStream, "Should have local stream");
+
+ const remoteStream = test.pcRemote._pc.getRemoteStreams()[0];
+ ok(remoteStream, "Should have remote stream");
+
+ const newTrack = stream.getTracks()[0];
+
+ const videoSenderIndex =
+ test.pcLocal._pc.getSenders().findIndex(s => s.track.kind == "video");
+ isnot(videoSenderIndex, -1, "Should have video sender");
+
+ test.pcLocal.removeSender(videoSenderIndex);
+ is(videoTransceiver.direction, "recvonly",
+ "Video transceiver should be recvonly after removeTrack");
+ test.pcLocal.attachLocalTrack(stream.getTracks()[0], localStream);
+ is(videoTransceiver.direction, "recvonly",
+ "Video transceiver should be recvonly after addTrack");
+
+ eventsPromise = haveEvent(remoteStream, "addtrack",
+ wait(50000, new Error("No addtrack event for " + newTrack.id)))
+ .then(trackEvent => {
+ ok(trackEvent instanceof MediaStreamTrackEvent,
+ "Expected event to be instance of MediaStreamTrackEvent");
+ is(trackEvent.type, "addtrack",
+ "Expected addtrack event type");
+ is(trackEvent.track.readyState, "live",
+ "added track should be live");
+ })
+ .then(() => haveNoEvent(remoteStream, "addtrack"));
+ });
+ },
+ ],
+ [
+ function PC_REMOTE_CHECK_EVENTS(test) {
+ return eventsPromise;
+ },
+ ]
+ );
+
+ test.setMediaConstraints([{audio: true, video: true}], []);
+ return test.run();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_answererAddSecondAudioStream.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_answererAddSecondAudioStream.html
new file mode 100644
index 0000000000..d9b01bf722
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_answererAddSecondAudioStream.html
@@ -0,0 +1,32 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1017888",
+ title: "Renegotiation: answerer adds second audio stream"
+ });
+
+ runNetworkTest(function (options) {
+ const test = new PeerConnectionTest(options);
+ addRenegotiationAnswerer(test.chain,
+ [
+ function PC_LOCAL_ADD_SECOND_STREAM(test) {
+ test.setMediaConstraints([{audio: true}, {audio: true}],
+ [{audio: true}]);
+ return test.pcLocal.getAllUserMediaAndAddStreams([{audio: true}]);
+ },
+ ]
+ );
+
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_audioChannels.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_audioChannels.html
new file mode 100644
index 0000000000..f6e77f8271
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_audioChannels.html
@@ -0,0 +1,102 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+
+createHTML({
+ bug: "1765005",
+ title: "Verify audio channel limits for each negotiated audio codec",
+});
+
+const matchesChannels = (sdp, codec, channels) =>
+ !!sdp.match(new RegExp(`a=rtpmap:\\d* ${codec}\/\\d*\/${channels}\r\n`, "g")) ||
+ (channels <= 1 &&
+ !!sdp.match(new RegExp(`a=rtpmap:\\d* ${codec}\/\\d*\r\n`, "g")));
+
+async function testAudioChannels(track, codec, channels, accepted, expectedChannels) {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ try {
+ pc1.addTrack(track);
+ pc1.onicecandidate = e => pc2.addIceCandidate(e.candidate);
+ pc2.onicecandidate = e => pc1.addIceCandidate(e.candidate);
+ await pc1.setLocalDescription();
+ await pc2.setRemoteDescription(pc1.localDescription);
+ let {type, sdp} = await pc2.createAnswer();
+ sdp = sdp.replace(new RegExp(`a=rtpmap:(\\d*) ${codec}\/(\\d*)\/?\\d*\r\n`, "g"),
+ `a=rtpmap:$1 ${codec}/$2/${channels}\r\n`);
+ const payloadType = Number(sdputils.findCodecId(sdp, codec));
+ sdp = sdputils.removeAllButPayloadType(sdp, payloadType);
+ ok(matchesChannels(sdp, codec, channels), "control");
+ await pc2.setLocalDescription({type, sdp});
+ is(matchesChannels(pc2.localDescription.sdp, codec, channels),
+ accepted,
+ `test pc2.localDescription`);
+ try {
+ await pc1.setRemoteDescription(pc2.localDescription);
+ ok(expectedChannels, "SRD should succeed iff we're expecting channels");
+ const [receiver] = pc2.getReceivers();
+ await new Promise(r => receiver.track.onunmute = r);
+ let stats = await receiver.getStats();
+ let inboundStat = [...stats.values()].find(({type}) => type == "inbound-rtp");
+ if (!inboundStat) {
+ info("work around bug 1765215"); // TODO bug 1765215
+ await new Promise(r => setTimeout(r, 200));
+ stats = await receiver.getStats();
+ inboundStat = [...stats.values()].find(({type}) => type == "inbound-rtp");
+ }
+ ok(inboundStat, "has inbound-rtp stats after track unmute (w/workaround)");
+ const codecStat = stats.get(inboundStat.codecId);
+ ok(codecStat.mimeType.includes(codec), "correct codec");
+ is(codecStat.payloadType, payloadType, "correct payloadType");
+ is(codecStat.channels, expectedChannels, "expected channels");
+ } catch (e) {
+ ok(!expectedChannels, "SRD should fail iff we're not expecting channels");
+ }
+ } finally {
+ pc1.close();
+ pc2.close();
+ }
+}
+
+runNetworkTest(async () => {
+ const [track] = (await navigator.mediaDevices.getUserMedia({audio: true}))
+ .getAudioTracks();
+ try {
+ for (let [codec, channels, accepted, expectedChannels] of [
+ ["opus", 2, true, 2],
+ ["opus", 1, true, 0],
+ ["opus", 1000, true, 0],
+ ["G722", 1, true, 1],
+ ["G722", 2, true, 0],
+ ["G722", 1000, true, 0],
+ ["PCMU", 1, true, 1],
+ ["PCMU", 2, false, 1],
+ ["PCMU", 1000, false, 1],
+ ["PCMA", 1, true, 1],
+ ["PCMA", 2, false, 1],
+ ["PCMA", 1000, false, 1]
+ ]) {
+ const testName = `${codec} with ${channels} channel(s) is ` +
+ `${accepted? "accepted" : "ignored"} and produces ` +
+ `${expectedChannels || "no"} channels`;
+ try {
+ info(`Testing that ${testName}`);
+ await testAudioChannels(track, codec, channels, accepted, expectedChannels);
+ } catch (e) {
+ ok(false, `Error testing that ${testName}: ${e}\n${e.stack}`);
+ }
+ }
+ } finally {
+ track.stop();
+ }
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_audioCodecs.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_audioCodecs.html
new file mode 100644
index 0000000000..8874436e3b
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_audioCodecs.html
@@ -0,0 +1,81 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1395853",
+ title: "Verify audio content over WebRTC for every audio codec",
+ });
+
+ // We match the format member against the sdp to figure out the payload type,
+ // So all other present codecs can be removed.
+ const codecs = [ "opus", "G722", "PCMU", "PCMA" ];
+
+ async function testAudioCodec(options = {}, codec) {
+ // sdputils checks for opus as part of its sdp sanity test
+ options.opus = codec == "opus";
+
+ let test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{audio: true}], []);
+
+ test.chain.insertBefore("PC_LOCAL_SET_LOCAL_DESCRIPTION", [
+ function PC_LOCAL_FILTER_OUT_CODECS() {
+ let otherCodec = codecs.find(c => c != codec);
+ let otherId = sdputils.findCodecId(test.originalOffer.sdp, otherCodec);
+
+ let id = sdputils.findCodecId(test.originalOffer.sdp, codec);
+ test.originalOffer.sdp =
+ sdputils.removeAllButPayloadType(test.originalOffer.sdp, id);
+
+ ok(!test.originalOffer.sdp.match(new RegExp(`m=.*UDP/TLS/RTP/SAVPF.* ${otherId}[^0-9]`, "gi")),
+ `Other codec ${otherId} should be removed after filtering`);
+ ok(test.originalOffer.sdp.match(new RegExp(`m=.*UDP/TLS/RTP/SAVPF.* ${id}[^0-9]`, "gi")),
+ `Tested codec ${id} should remain after filtering`);
+
+ for (let c of codecs.filter(c => c != codec)) {
+ // Remove rtpmaps for the other codecs so sdp sanity tests pass.
+ let id = sdputils.findCodecId(test.originalOffer.sdp, c);
+ test.originalOffer.sdp =
+ sdputils.removeRtpMapForPayloadType(test.originalOffer.sdp, id);
+ }
+
+ ok(!test.originalOffer.sdp.match(new RegExp(`a=rtpmap:${otherId}.*\\r\\n`, "gi")),
+ `Rtpmap of other codec ${otherId} should be removed after filtering`);
+ ok(test.originalOffer.sdp.match(new RegExp(`a=rtpmap:${id}.*\\r\\n`, "gi")),
+ `Rtpmap of tested codec should remain after filtering`);
+ },
+ ]);
+
+ test.chain.append([
+ async function CHECK_AUDIO_FLOW() {
+ try {
+ await test.pcRemote.checkReceivingToneFrom(new AudioContext(), test.pcLocal);
+ ok(true, "input and output audio data matches");
+ } catch(e) {
+ ok(false, `No audio flow: ${e}`);
+ }
+ },
+ ]);
+
+ await test.run();
+ }
+
+ runNetworkTest(async (options) => {
+ for (let codec of codecs) {
+ info(`Testing audio for codec ${codec}`);
+ try {
+ await testAudioCodec(options, codec);
+ } catch(e) {
+ ok(false, `Error in test for codec ${codec}: ${e}\n${e.stack}`);
+ }
+ info(`Tested audio for codec ${codec}`);
+ }
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_audioContributingSources.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_audioContributingSources.html
new file mode 100644
index 0000000000..333b40a888
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_audioContributingSources.html
@@ -0,0 +1,144 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1363667",
+ title: "Test audio receiver getContributingSources"
+ });
+
+ // test_peerConnection_audioSynchronizationSources.html tests
+ // much of the functionality of getContributingSources as the implementation
+ // is shared.
+ var testGetContributingSources = async (test) => {
+ const remoteReceiver = test.pcRemote.getReceivers()[0];
+ const localReceiver = test.pcLocal.getReceivers()[0];
+
+ // Check that getContributingSources is empty as there is no MCU
+ is(remoteReceiver.getContributingSources().length, 0,
+ "remote contributing sources is empty");
+ is(localReceiver.getContributingSources().length, 0,
+ "local contributing sources is empty");
+ // Wait for the next JS event loop iteration, to clear the cache
+ await Promise.resolve().then();
+ // Insert new entries as if there were an MCU
+ const csrc0 = 124756;
+ const timestamp0 = performance.now() + performance.timeOrigin;
+ const rtpTimestamp0 = 11111;
+ const hasAudioLevel0 = true;
+ // Audio level as expected to be received in RTP
+ const audioLevel0 = 34;
+ // Audio level as expected to be returned
+ const expectedAudioLevel0 = 10 ** (-audioLevel0 / 20);
+
+ SpecialPowers.wrap(remoteReceiver).mozInsertAudioLevelForContributingSource(
+ csrc0,
+ timestamp0,
+ rtpTimestamp0,
+ hasAudioLevel0,
+ audioLevel0);
+
+ const csrc1 = 5786;
+ const timestamp1 = timestamp0 - 200;
+ const rtpTimestamp1 = 22222;
+ const hasAudioLevel1 = false;
+ const audioLevel1 = 0;
+
+ SpecialPowers.wrap(remoteReceiver).mozInsertAudioLevelForContributingSource(
+ csrc1,
+ timestamp1,
+ rtpTimestamp1,
+ hasAudioLevel1,
+ audioLevel1);
+
+ const csrc2 = 93487;
+ const timestamp2 = timestamp0 - 200;
+ const rtpTimestamp2 = 333333;
+ const hasAudioLevel2 = true;
+ const audioLevel2 = 127;
+
+ SpecialPowers.wrap(remoteReceiver).mozInsertAudioLevelForContributingSource(
+ csrc2,
+ timestamp2,
+ rtpTimestamp2,
+ hasAudioLevel2,
+ audioLevel2);
+
+ const contributingSources = remoteReceiver.getContributingSources();
+ is(contributingSources.length, 3,
+ "Expected number of contributing sources");
+
+ // Check that both inserted were returned
+ const source0 = contributingSources.find(c => c.source == csrc0);
+ ok(source0, "first csrc was found");
+
+ const source1 = contributingSources.find(c => c.source == csrc1);
+ ok(source1, "second csrsc was found");
+
+ // Add a small margin of error in the timestamps
+ const compareTimestamps = (ts1, ts2) => Math.abs(ts1 - ts2) < 100;
+
+ // Check the CSRC with audioLevel
+ const isWithinErr = Math.abs(source0.audioLevel - expectedAudioLevel0)
+ < expectedAudioLevel0 / 50;
+ ok(isWithinErr,
+ `Contributing source has correct audio level. (${source0.audioLevel})`);
+ ok(compareTimestamps(source0.timestamp, timestamp0),
+ `Contributing source has correct timestamp (got ${source0.timestamp}), expected ${timestamp0}`);
+ is(source0.rtpTimestamp, rtpTimestamp0,
+ `Contributing source has correct RTP timestamp (${source0.rtpTimestamp}`);
+ // Check the CSRC without audioLevel
+ is(source1.audioLevel, undefined,
+ `Contributing source has no audio level. (${source1.audioLevel})`);
+ ok(compareTimestamps(source1.timestamp, timestamp1),
+ `Contributing source has correct timestamp (got ${source1.timestamp}, expected ${timestamp1})`);
+ is(source1.rtpTimestamp, rtpTimestamp1,
+ `Contributing source has correct RTP timestamp (${source1.rtpTimestamp}`);
+ // Check that a received RTP audio level 127 is exactly 0
+ const source2 = contributingSources.find(c => c.source == csrc2);
+ ok(source2, "third csrc was found");
+ is(source2.audioLevel, 0,
+ `Contributing source has audio level of 0 when RTP audio level is 127`);
+ // Check caching
+ is(JSON.stringify(contributingSources),
+ JSON.stringify(remoteReceiver.getContributingSources()),
+ "getContributingSources is cached");
+ // Check that sources are sorted in descending order by time stamp
+ const timestamp3 = performance.now() + performance.timeOrigin;
+ const rtpTimestamp3 = 44444;
+ // Larger offsets are further back in time
+ const testOffsets = [3, 7, 5, 6, 1, 4];
+ for (const offset of testOffsets) {
+ SpecialPowers.wrap(localReceiver).mozInsertAudioLevelForContributingSource(
+ offset, // Using offset for SSRC for convenience
+ timestamp3 - offset,
+ rtpTimestamp3,
+ true,
+ offset);
+ }
+ const sources = localReceiver.getContributingSources();
+ const sourceOffsets = sources.map(s => s.source);
+ is(JSON.stringify(sourceOffsets),
+ JSON.stringify([...testOffsets].sort((a, b) => a - b)),
+ `Contributing sources are sorted in descending order by timestamp:`
+ + ` ${JSON.stringify(sources)}`);
+ };
+
+ var test;
+ runNetworkTest(async function(options) {
+ test = new PeerConnectionTest(options);
+ test.chain.insertAfter("PC_REMOTE_WAIT_FOR_MEDIA_FLOW",
+ [testGetContributingSources]);
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ test.pcLocal.audioElementsOnly = true;
+ await pushPrefs(["privacy.reduceTimerPrecision", false]);
+ await test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_audioRenegotiationInactiveAnswer.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_audioRenegotiationInactiveAnswer.html
new file mode 100644
index 0000000000..6d3a23b57a
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_audioRenegotiationInactiveAnswer.html
@@ -0,0 +1,69 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="sdpUtils.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1213773",
+ title: "Renegotiation: answerer uses a=inactive for audio"
+ });
+
+ runNetworkTest(function (options) {
+ const helper = new AudioStreamHelper();
+
+ const test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{audio: true}], []);
+ let haveFirstUnmuteEvent;
+
+ test.chain.insertBefore("PC_REMOTE_SET_LOCAL_DESCRIPTION", [
+ function PC_REMOTE_SETUP_ONUNMUTE_1() {
+ haveFirstUnmuteEvent = haveEvent(test.pcRemote._pc.getReceivers()[0].track, "unmute");
+ }
+ ]);
+
+ test.chain.append([
+ function PC_REMOTE_CHECK_AUDIO_UNMUTED() {
+ return haveFirstUnmuteEvent;
+ },
+ function PC_REMOTE_CHECK_AUDIO_FLOWING() {
+ return helper.checkAudioFlowing(test.pcRemote._pc.getRemoteStreams()[0]);
+ }
+ ]);
+
+ addRenegotiation(test.chain, []);
+
+ test.chain.insertAfter("PC_LOCAL_GET_ANSWER", [
+ function PC_LOCAL_REWRITE_REMOTE_SDP_INACTIVE(test) {
+ test._remote_answer.sdp =
+ sdputils.setAllMsectionsInactive(test._remote_answer.sdp);
+ }
+ ], false, 1);
+
+ test.chain.append([
+ function PC_REMOTE_CHECK_AUDIO_NOT_FLOWING() {
+ return helper.checkAudioNotFlowing(test.pcRemote._pc.getRemoteStreams()[0]);
+ }
+ ]);
+
+ test.chain.remove("PC_REMOTE_CHECK_STATS", 1);
+ test.chain.remove("PC_LOCAL_CHECK_STATS", 1);
+ test.chain.remove("PC_REMOTE_WAIT_FOR_MEDIA_FLOW", 1);
+
+ addRenegotiation(test.chain, []);
+
+ test.chain.append([
+ function PC_REMOTE_CHECK_AUDIO_FLOWING_2() {
+ return helper.checkAudioFlowing(test.pcRemote._pc.getRemoteStreams()[0]);
+ }
+ ]);
+
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_audioSynchronizationSources.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_audioSynchronizationSources.html
new file mode 100644
index 0000000000..32603b2e40
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_audioSynchronizationSources.html
@@ -0,0 +1,95 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1363667",
+ title: "Test audio receiver getSynchronizationSources"
+ });
+
+ var waitForSyncSources = async (test) => {
+ let receivers = [...test.pcRemote.getReceivers(),
+ ...test.pcLocal.getReceivers()];
+ is(receivers.length, 2, "Expected number of receivers");
+ // Wait for sync sources
+ while (true) {
+ if (receivers[0].getSynchronizationSources().length &&
+ receivers[1].getSynchronizationSources().length) {
+ break;
+ }
+ await wait(250);
+ }
+ };
+
+ var testGetSynchronizationSources = async (test) => {
+ await waitForSyncSources(test);
+ let receivers = [...test.pcRemote.getReceivers(),
+ ...test.pcLocal.getReceivers()];
+ is(receivers.length, 2,
+ `Expected number of receivers is 2. (${receivers.length})`);
+ for (let recv of receivers) {
+ let syncSources = recv.getSynchronizationSources();
+ ok(syncSources,
+ "Receiver has Synchronization sources " + JSON.stringify(syncSources));
+ is(syncSources.length, 1, "Each receiver has only a single sync source");
+ let source = recv.getSynchronizationSources()[0];
+ ok(source.audioLevel !== null,
+ `Synchronization source has audio level. (${source.audioLevel})`);
+ ok(source.audioLevel >= 0.0,
+ `Synchronization source audio level >= 0.0 (${source.audioLevel})`);
+ ok(source.audioLevel <= 1.0,
+ `Synchronization source audio level <= 1.0 (${source.audioLevel})`);
+ ok(source.timestamp,
+ `Synchronization source has timestamp (${source.timestamp})`);
+ const ageSeconds =
+ (window.performance.now() + window.performance.timeOrigin -
+ source.timestamp) / 1000;
+ ok(ageSeconds >= 0,
+ `Synchronization source timestamp is in the past`);
+ ok(ageSeconds < 2.5,
+ `Synchronization source timestamp is close to now`);
+ is(source.voiceActivityFlag, undefined,
+ "Synchronization source unsupported voiceActivity is undefined");
+ }
+ };
+
+ var testSynchronizationSourceCached = async (test) => {
+ await waitForSyncSources(test);
+ let receivers = [...test.pcRemote.getReceivers(),
+ ...test.pcLocal.getReceivers()];
+ is(receivers.length, 2,
+ `Expected number of receivers is 2. (${receivers.length})`);
+ let sourceSets = [[],[]];
+ for (let sourceSet of sourceSets) {
+ for (let recv of receivers) {
+ let sources = recv.getSynchronizationSources();
+ is(sources.length, 1,
+ `Expected number of sources is 1. (${sources.length})`);
+ sourceSet.push(sources);
+ }
+ // Busy wait 1s before trying again
+ let endTime = performance.now() + 1000;
+ while (performance.now() < endTime) {};
+ }
+ is(JSON.stringify(sourceSets[0]), JSON.stringify(sourceSets[1]),
+ "Subsequent getSynchronizationSources calls are cached.");
+ };
+
+ var test;
+ runNetworkTest(function(options) {
+ test = new PeerConnectionTest(options);
+ test.chain.insertAfter("PC_REMOTE_WAIT_FOR_MEDIA_FLOW",
+ [testGetSynchronizationSources,
+ testSynchronizationSourceCached]);
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ test.pcLocal.audioElementsOnly = true;
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_audioSynchronizationSourcesUnidirectional.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_audioSynchronizationSourcesUnidirectional.html
new file mode 100644
index 0000000000..6d66614e91
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_audioSynchronizationSourcesUnidirectional.html
@@ -0,0 +1,54 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1439001",
+ title: "Test audio unidirectional getSynchronizationSources"
+ });
+
+ var waitForSyncSources = async (test) => {
+ let receiver = test.pcRemote.getReceivers()[0];
+ ok(receiver, "Remote has a receiver");
+ // Wait for remote sync source
+ while (!receiver.getSynchronizationSources().length) {
+ await wait(250);
+ }
+ is(receiver.getSynchronizationSources().length, 1,
+ "Remote receiver has a synchronization source");
+ // Make sure local has no sync source
+ is(test.pcLocal.getReceivers()[0].getSynchronizationSources().length, 0,
+ "Local receiver has no synchronization source");
+ };
+ /*
+ * Test to make sure that in unidirectional calls, the receiving end has
+ * synchronization sources with audio levels, and the sending end has none.
+ */
+ var testGetSynchronizationSourcesUnidirectional = async (test) => {
+ await waitForSyncSources(test);
+ let receiver = test.pcRemote.getReceivers()[0];
+ let syncSources = receiver.getSynchronizationSources();
+ ok(syncSources.length,
+ "Receiver has Synchronization sources " + JSON.stringify(syncSources));
+ is(syncSources.length, 1, "Receiver has only a single sync source");
+ let syncSource = syncSources[0];
+ ok(syncSource.audioLevel !== undefined, "SynchronizationSource has audioLevel");
+ };
+
+ var test;
+ runNetworkTest(function(options) {
+ test = new PeerConnectionTest(options);
+ test.chain.insertAfter("PC_REMOTE_WAIT_FOR_MEDIA_FLOW",
+ [testGetSynchronizationSourcesUnidirectional]);
+ test.setMediaConstraints([{audio: true}], []);
+ test.pcLocal.audioElementsOnly = true;
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudio.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudio.html
new file mode 100644
index 0000000000..5fd10a67f9
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudio.html
@@ -0,0 +1,25 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "796892",
+ title: "Basic audio-only peer connection"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ // pc.js uses video elements by default, we want to test audio elements here
+ test.pcLocal.audioElementsOnly = true;
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioDynamicPtMissingRtpmap.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioDynamicPtMissingRtpmap.html
new file mode 100644
index 0000000000..a076bf80f1
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioDynamicPtMissingRtpmap.html
@@ -0,0 +1,36 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1246011",
+ title: "Offer with dynamic PT but missing rtpmap"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ options = options || { };
+ // we want Opus to get selected and 101 to be ignored
+ options.opus = true;
+ test = new PeerConnectionTest(options);
+ test.chain.insertBefore("PC_REMOTE_GET_OFFER", [
+ function PC_LOCAL_REDUCE_MLINE_REMOVE_RTPMAPS(test) {
+ test.originalOffer.sdp =
+ sdputils.reduceAudioMLineToDynamicPtAndOpus(test.originalOffer.sdp);
+ test.originalOffer.sdp =
+ sdputils.removeAllRtpMaps(test.originalOffer.sdp);
+ test.originalOffer.sdp = test.originalOffer.sdp + "a=rtpmap:109 opus/48000/2\r\n";
+ info("SDP with dyn PT and no Rtpmap: " + JSON.stringify(test.originalOffer));
+ }
+ ]);
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNATRelay.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNATRelay.html
new file mode 100644
index 0000000000..180abc075a
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNATRelay.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="nonTrickleIce.js"></script>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+ bug: "1231975",
+ title: "Basic audio-only peer connection with port dependent NAT, for verifying UDP relay"
+});
+
+// This test uses the NAT simulator, which doesn't work in https, so we turn
+// on getUserMedia in http, which requires a reload.
+if (!("mediaDevices" in navigator)) {
+ SpecialPowers.pushPrefEnv({set: [['media.devices.insecure.enabled', true]]},
+ () => location.reload());
+} else {
+ runNetworkTest(async (options = {}) => {
+ await pushPrefs(
+ ['media.peerconnection.ice.obfuscate_host_addresses', false],
+ ['media.peerconnection.nat_simulator.filtering_type', 'PORT_DEPENDENT'],
+ ['media.peerconnection.nat_simulator.mapping_type', 'PORT_DEPENDENT'],
+ ['media.peerconnection.nat_simulator.block_tcp', true],
+ ['media.getusermedia.insecure.enabled', true]);
+ options.expectedLocalCandidateType = "srflx";
+ options.expectedRemoteCandidateType = "relay";
+ // If both have TURN, it is a toss-up which one will end up using a
+ // relay.
+ options.turn_disabled_local = true;
+ const test = new PeerConnectionTest(options);
+ // Make sure we don't end up choosing the wrong thing due to delays in
+ // trickle. Once we are willing to accept trickle after ICE success, we
+ // can maybe wait a bit to allow things to stabilize.
+ // TODO(bug 1238249)
+ makeOffererNonTrickle(test.chain);
+ makeAnswererNonTrickle(test.chain);
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ await test.run();
+ }, { useIceServer: true });
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNATRelayTCP.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNATRelayTCP.html
new file mode 100644
index 0000000000..7bb51764bd
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNATRelayTCP.html
@@ -0,0 +1,42 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+ bug: "1231975",
+ title: "Basic audio-only peer connection with port dependent NAT that blocks UDP"
+});
+
+// This test uses the NAT simulator, which doesn't work in https, so we turn
+// on getUserMedia in http, which requires a reload.
+if (!("mediaDevices" in navigator)) {
+ SpecialPowers.pushPrefEnv({set: [['media.devices.insecure.enabled', true]]},
+ () => location.reload());
+} else {
+ runNetworkTest(async (options = {}) => {
+ await pushPrefs(
+ ['media.peerconnection.ice.obfuscate_host_addresses', false],
+ ['media.peerconnection.nat_simulator.filtering_type', 'PORT_DEPENDENT'],
+ ['media.peerconnection.nat_simulator.mapping_type', 'PORT_DEPENDENT'],
+ ['media.peerconnection.nat_simulator.block_udp', true],
+ ['media.peerconnection.nat_simulator.block_tcp', false],
+ ['media.peerconnection.nat_simulator.block_tls', true],
+ ['media.peerconnection.ice.loopback', true],
+ ['media.getusermedia.insecure.enabled', true]);
+ options.expectedLocalCandidateType = "relay-tcp";
+ options.expectedRemoteCandidateType = "relay-tcp";
+ // No reason to wait for gathering to complete like the other NAT tests,
+ // since relayed-tcp is the only thing that can work.
+ const test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ await test.run();
+ }, { useIceServer: true });
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNATRelayTCPWithStun300.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNATRelayTCPWithStun300.html
new file mode 100644
index 0000000000..43ea6aaea7
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNATRelayTCPWithStun300.html
@@ -0,0 +1,54 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="nonTrickleIce.js"></script>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+ bug: "857668",
+ title: "Basic audio-only peer connection with UDP-blocking NAT, for verifying TCP relay with STUN 300 responses"
+});
+
+// This test uses the NAT simulator, which doesn't work in https, so we turn
+// on getUserMedia in http, which requires a reload.
+if (!("mediaDevices" in navigator)) {
+ SpecialPowers.pushPrefEnv({set: [['media.devices.insecure.enabled', true]]},
+ () => location.reload());
+} else {
+ runNetworkTest(async (options = {}) => {
+ await pushPrefs(
+ ['media.peerconnection.ice.obfuscate_host_addresses', false],
+ ['media.peerconnection.nat_simulator.filtering_type', 'ENDPOINT_INDEPENDENT'],
+ ['media.peerconnection.nat_simulator.mapping_type', 'ENDPOINT_INDEPENDENT'],
+ ['media.peerconnection.nat_simulator.block_udp', true],
+ ['media.peerconnection.nat_simulator.block_tcp', false],
+ ['media.peerconnection.ice.loopback', true],
+ ['media.getusermedia.insecure.enabled', true]);
+ options.expectedLocalCandidateType = "relay-tcp";
+ options.expectedRemoteCandidateType = "relay-tcp";
+ const turnServer = iceServersArray.find(server => "username" in server);
+ const turnRedirectPort = turnServer.turn_redirect_port;
+ const turnHostname = getTurnHostname(turnServer.urls[0]);
+ turnServer.urls = [`turn:${turnHostname}:${turnRedirectPort}?transport=tcp`];
+ // Override turn servers so we can test simulated redirects
+ options.config_local = {iceServers: [turnServer]};
+ options.config_remote = {iceServers: [turnServer]};
+ const test = new PeerConnectionTest(options);
+ // Make sure we don't end up choosing the wrong thing due to delays in
+ // trickle. Once we are willing to accept trickle after ICE success, we
+ // can maybe wait a bit to allow things to stabilize.
+ // TODO(bug 1238249)
+ makeOffererNonTrickle(test.chain);
+ makeAnswererNonTrickle(test.chain);
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ await test.run();
+ await SpecialPowers.popPrefEnv();
+ }, { useIceServer: true });
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNATRelayTLS.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNATRelayTLS.html
new file mode 100644
index 0000000000..7446401f87
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNATRelayTLS.html
@@ -0,0 +1,41 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+ bug: "1231975",
+ title: "Basic audio-only peer connection with port dependent NAT that blocks STUN"
+});
+
+// This test uses the NAT simulator, which doesn't work in https, so we turn
+// on getUserMedia in http, which requires a reload.
+if (!("mediaDevices" in navigator)) {
+ SpecialPowers.pushPrefEnv({set: [['media.devices.insecure.enabled', true]]},
+ () => location.reload());
+} else {
+ runNetworkTest(async (options = {}) => {
+ await pushPrefs(
+ ['media.peerconnection.ice.obfuscate_host_addresses', false],
+ ['media.peerconnection.nat_simulator.filtering_type', 'PORT_DEPENDENT'],
+ ['media.peerconnection.nat_simulator.mapping_type', 'PORT_DEPENDENT'],
+ ['media.peerconnection.nat_simulator.block_udp', true],
+ ['media.peerconnection.nat_simulator.block_tcp', true],
+ ['media.peerconnection.ice.loopback', true],
+ ['media.getusermedia.insecure.enabled', true]);
+ options.expectedLocalCandidateType = "relay-tls";
+ options.expectedRemoteCandidateType = "relay-tls";
+ // No reason to wait for gathering to complete like the other NAT tests,
+ // since relayed-tcp is the only thing that can work.
+ const test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ await test.run();
+ }, { useIceServer: true });
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNATRelayWithStun300.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNATRelayWithStun300.html
new file mode 100644
index 0000000000..286e67bc2f
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNATRelayWithStun300.html
@@ -0,0 +1,53 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="nonTrickleIce.js"></script>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+ bug: "857668",
+ title: "Basic audio-only peer connection with port dependent NAT, for verifying UDP relay with STUN 300 responses"
+});
+
+// This test uses the NAT simulator, which doesn't work in https, so we turn
+// on getUserMedia in http, which requires a reload.
+if (!("mediaDevices" in navigator)) {
+ SpecialPowers.pushPrefEnv({set: [['media.devices.insecure.enabled', true]]},
+ () => location.reload());
+} else {
+ runNetworkTest(async (options = {}) => {
+ await pushPrefs(
+ ['media.peerconnection.ice.obfuscate_host_addresses', false],
+ ['media.peerconnection.nat_simulator.filtering_type', 'PORT_DEPENDENT'],
+ ['media.peerconnection.nat_simulator.mapping_type', 'PORT_DEPENDENT'],
+ ['media.peerconnection.nat_simulator.block_tcp', true],
+ ['media.getusermedia.insecure.enabled', true]);
+ options.expectedLocalCandidateType = "srflx";
+ options.expectedRemoteCandidateType = "relay";
+ const turnServer = iceServersArray.find(server => "username" in server);
+ const turnRedirectPort = turnServer.turn_redirect_port;
+ const turnHostname = getTurnHostname(turnServer.urls[0]);
+ turnServer.urls = [`turn:${turnHostname}:${turnRedirectPort}`];
+ // Override turn servers so we can test redirects
+ options.config_remote = {iceServers: [turnServer]};
+ // If both have TURN, it is a toss-up which one will end up using a
+ // relay, so we disable TURN for one side.
+ options.turn_disabled_local = true;
+ const test = new PeerConnectionTest(options);
+ // Make sure we don't end up choosing the wrong thing due to delays in
+ // trickle. Once we are willing to accept trickle after ICE success, we
+ // can maybe wait a bit to allow things to stabilize.
+ // TODO(bug 1238249)
+ makeOffererNonTrickle(test.chain);
+ makeAnswererNonTrickle(test.chain);
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ await test.run();
+ }, { useIceServer: true });
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNATSrflx.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNATSrflx.html
new file mode 100644
index 0000000000..78fa8bcb2c
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNATSrflx.html
@@ -0,0 +1,44 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="nonTrickleIce.js"></script>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+ bug: "1231975",
+ title: "Basic audio-only peer connection with endpoint independent NAT, for verifying UDP srflx"
+});
+
+// This test uses the NAT simulator, which doesn't work in https, so we turn
+// on getUserMedia in http, which requires a reload.
+if (!("mediaDevices" in navigator)) {
+ SpecialPowers.pushPrefEnv({set: [['media.devices.insecure.enabled', true]]},
+ () => location.reload());
+} else {
+ runNetworkTest(async (options = {}) => {
+ await pushPrefs(
+ ['media.peerconnection.ice.obfuscate_host_addresses', false],
+ ['media.peerconnection.nat_simulator.filtering_type', 'ENDPOINT_INDEPENDENT'],
+ ['media.peerconnection.nat_simulator.mapping_type', 'ENDPOINT_INDEPENDENT'],
+ ['media.peerconnection.nat_simulator.block_tcp', true],
+ ['media.getusermedia.insecure.enabled', true]);
+ options.expectedLocalCandidateType = "srflx";
+ options.expectedRemoteCandidateType = "srflx";
+ const test = new PeerConnectionTest(options);
+ // Make sure we don't end up choosing the wrong thing due to delays in
+ // trickle. Once we are willing to accept trickle after ICE success, we
+ // can maybe wait a bit to allow things to stabilize.
+ // TODO(bug 1238249)
+ makeOffererNonTrickle(test.chain);
+ makeAnswererNonTrickle(test.chain);
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ await test.run();
+ }, { useIceServer: true });
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNoisyUDPBlock.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNoisyUDPBlock.html
new file mode 100644
index 0000000000..297121cd94
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNoisyUDPBlock.html
@@ -0,0 +1,41 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+ bug: "1231975",
+ title: "Basic audio-only peer connection where UDP sockets return errors on send"
+});
+
+// This test uses the NAT simulator, which doesn't work in https, so we turn
+// on getUserMedia in http, which requires a reload.
+if (!("mediaDevices" in navigator)) {
+ SpecialPowers.pushPrefEnv({set: [['media.devices.insecure.enabled', true]]},
+ () => location.reload());
+} else {
+ runNetworkTest(async (options = {}) => {
+ await pushPrefs(
+ ['media.peerconnection.ice.obfuscate_host_addresses', false],
+ ['media.peerconnection.nat_simulator.filtering_type', 'PORT_DEPENDENT'],
+ ['media.peerconnection.nat_simulator.mapping_type', 'PORT_DEPENDENT'],
+ ['media.peerconnection.nat_simulator.block_udp', true],
+ ['media.peerconnection.nat_simulator.error_code_for_drop', 3 /*R_INTERNAL*/],
+ ['media.peerconnection.nat_simulator.block_tls', true],
+ ['media.getusermedia.insecure.enabled', true]);
+ options.expectedLocalCandidateType = "relay-tcp";
+ options.expectedRemoteCandidateType = "relay-tcp";
+ // No reason to wait for gathering to complete like the other NAT tests,
+ // since relayed-tcp is the only thing that can work.
+ const test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ await test.run();
+ }, { useIceServer: true });
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioPcmaPcmuOnly.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioPcmaPcmuOnly.html
new file mode 100644
index 0000000000..f0fe721b8e
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioPcmaPcmuOnly.html
@@ -0,0 +1,39 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1221837",
+ title: "Only offer PCMA and PMCU in mline (no rtpmaps)"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ options = options || { };
+ options.opus = false;
+ test = new PeerConnectionTest(options);
+ test.chain.insertBefore("PC_REMOTE_GET_OFFER", [
+ function PC_LOCAL_REDUCE_MLINE_REMOVE_RTPMAPS(test) {
+ test.originalOffer.sdp =
+ sdputils.reduceAudioMLineToPcmuPcma(test.originalOffer.sdp);
+ test.originalOffer.sdp =
+ sdputils.removeAllRtpMaps(test.originalOffer.sdp);
+ info("SDP without Rtpmaps: " + JSON.stringify(test.originalOffer));
+ }
+ ]);
+ test.chain.insertAfter("PC_REMOTE_SANE_LOCAL_SDP", [
+ function PC_REMOTE_VERIFY_PCMU(test) {
+ ok(test._remote_answer.sdp.includes("a=rtpmap:0 PCMU/8000"), "PCMU codec is present in SDP");
+ }
+ ]);
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioRelayPolicy.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioRelayPolicy.html
new file mode 100644
index 0000000000..ced57ff8a3
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioRelayPolicy.html
@@ -0,0 +1,83 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+ bug: "1663746",
+ title: "Basic tests for relay ice policy"
+});
+
+runNetworkTest(async () => {
+ await pushPrefs(
+ // Enable mDNS, since there are some checks we want to run with that
+ ['media.peerconnection.ice.obfuscate_host_addresses', true]);
+
+ const offerer = new RTCPeerConnection({iceServers: iceServersArray, iceTransportPolicy: 'relay'});
+ const answerer = new RTCPeerConnection({iceServers: iceServersArray});
+
+ offerer.onicecandidate = e => {
+ if (e.candidate) {
+ ok(!e.candidate.candidate.includes(' host '), 'IceTransportPolicy \"relay\" should prevent the advertisement of host candidates');
+ ok(!e.candidate.candidate.includes(' srflx '), 'IceTransportPolicy \"relay\" should prevent the advertisement of srflx candidates');
+ }
+ answerer.addIceCandidate(e.candidate);
+ };
+
+ answerer.onicecandidate = e => {
+ if (e.candidate && e.candidate.candidate.includes(' host ')) {
+ ok(e.candidate.candidate.includes('.local'), 'When obfuscate_host_addresses is true, we expect host candidates to use mDNS');
+ }
+ offerer.addIceCandidate(e.candidate);
+ };
+
+ const offererConnected = new Promise(r => {
+ offerer.oniceconnectionstatechange = () => {
+ if (offerer.iceConnectionState == 'connected') {
+ r();
+ }
+ };
+ });
+
+ const answererConnected = new Promise(r => {
+ answerer.oniceconnectionstatechange = () => {
+ if (answerer.iceConnectionState == 'connected') {
+ r();
+ }
+ };
+ });
+
+ const offer = await offerer.createOffer({offerToReceiveAudio: true});
+ await Promise.all([offerer.setLocalDescription(offer), answerer.setRemoteDescription(offer)]);
+ const answer = await answerer.createAnswer();
+ await Promise.all([answerer.setLocalDescription(answer), offerer.setRemoteDescription(answer)]);
+
+ info('Waiting for ICE to connect');
+ await Promise.all([offererConnected, answererConnected]);
+
+ const offererStats = await offerer.getStats();
+ const localCandidates = [...offererStats.values()].filter(stat => stat.type == 'local-candidate');
+ const remoteCandidates = [...offererStats.values()].filter(stat => stat.type == 'remote-candidate');
+ isnot(localCandidates, []);
+ isnot(remoteCandidates, []);
+
+ const localNonRelayCandidates =
+ localCandidates.filter(cand => cand.candidateType != 'relay');
+ is(localNonRelayCandidates.length, 0, `There should only be local relay candidates, because we are using the "relay" IceTransportPolicy, but we got ${JSON.stringify(localNonRelayCandidates)}`);
+
+ const remoteHostCandidates =
+ remoteCandidates.filter(cand => cand.candidateType == 'host');
+ is(remoteHostCandidates.length, 0, `There should be no remote host candidates in the stats, because mDNS resolution should have been disabled by the "relay" IceTransportPolicy, but we got ${JSON.stringify(remoteHostCandidates)}`);
+
+ offerer.close();
+ answerer.close();
+
+ await SpecialPowers.popPrefEnv();
+}, { useIceServer: true });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioRequireEOC.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioRequireEOC.html
new file mode 100644
index 0000000000..afad4550d4
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioRequireEOC.html
@@ -0,0 +1,35 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1167443",
+ title: "Basic audio-only peer connection which waits for end-of-candidates"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ test = new PeerConnectionTest(options);
+ test.chain.replace("PC_LOCAL_VERIFY_SDP_AFTER_END_OF_TRICKLE", [
+ function PC_LOCAL_REQUIRE_SDP_AFTER_END_OF_TRICKLE(test) {
+ return test.pcLocal.endOfTrickleSdp.then(sdp =>
+ sdputils.checkSdpAfterEndOfTrickle(sdp, test.testOptions, test.pcLocal.label));
+ }
+ ]);
+ test.chain.replace("PC_REMOTE_VERIFY_SDP_AFTER_END_OF_TRICKLE", [
+ function PC_REMOTE_REQUIRE_SDP_AFTER_END_OF_TRICKLE(test) {
+ return test.pcRemote.endOfTrickleSdp.then(sdp =>
+ sdputils.checkSdpAfterEndOfTrickle(sdp, test.testOptions, test.pcRemote.label));
+ }
+ ]);
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVerifyRtpHeaderExtensions.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVerifyRtpHeaderExtensions.html
new file mode 100644
index 0000000000..f28a990bd2
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVerifyRtpHeaderExtensions.html
@@ -0,0 +1,63 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="parser_rtp.js"></script>
+ <script type="application/javascript" src="sdpUtils.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1416932",
+ title: "Basic audio-only peer connection and verify rtp header extensions"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ // pc.js uses video elements by default, we want to test audio elements here
+ test.pcLocal.audioElementsOnly = true;
+
+ let getRtpPacket = (pc) => {
+ // we only examine received packets
+ let sending = false;
+ pc.mozEnablePacketDump(0, "rtp", sending);
+ return new Promise((res, rej) =>
+ pc.mozSetPacketCallback((...args) => {
+ res([...args]);
+ pc.mozSetPacketCallback(() => {});
+ pc.mozDisablePacketDump(0, "rtp", sending);
+ })
+ );
+ }
+
+ const pc = SpecialPowers.wrap(test.pcRemote._pc);
+ const haveFirstPacket = getRtpPacket(pc);
+
+ test.chain.insertBefore('PC_REMOTE_WAIT_FOR_MEDIA_FLOW', [
+ async function PC_REMOTE_CHECK_RTP_HEADER_EXTS_AGAINST_SDP() {
+
+ const sdpExtmapIds = sdputils.findExtmapIds(test.originalAnswer.sdp);
+
+ const [level, type, sending, data] = await haveFirstPacket;
+ const extensions = ParseRtpPacket(data).header.extensions;
+
+ // make sure we got the same number of rtp header extensions in
+ // the received packet as were negotiated in the sdp. Then
+ // check to make sure each of the received extension ids were in
+ // the sdp.
+ is(sdpExtmapIds.length, extensions.length, "number of received ids match sdp ids");
+ // note, we are comparing a number (from the parsed rtp packet)
+ // and a string (from the answer sdp)
+ ok(extensions.every((ext) => sdpExtmapIds.includes(""+ext.id)), "extension id arrays equivalent");
+ }
+ ]);
+
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideo.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideo.html
new file mode 100644
index 0000000000..c2c2d43f09
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideo.html
@@ -0,0 +1,24 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "796890",
+ title: "Basic audio/video (separate) peer connection"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{audio: true}, {video: true}],
+ [{audio: true}, {video: true}]);
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoCombined.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoCombined.html
new file mode 100644
index 0000000000..02a561f9b8
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoCombined.html
@@ -0,0 +1,24 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "796890",
+ title: "Basic audio/video (combined) peer connection"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{audio: true, video: true}],
+ [{audio: true, video: true}]);
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoNoBundle.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoNoBundle.html
new file mode 100644
index 0000000000..cae7f6617f
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoNoBundle.html
@@ -0,0 +1,25 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1016476",
+ title: "Basic audio/video peer connection with no Bundle"
+ });
+
+ runNetworkTest(options => {
+ options = options || { };
+ options.bundle = false;
+ var test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{audio: true}, {video: true}],
+ [{audio: true}, {video: true}]);
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoNoBundleNoRtcpMux.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoNoBundleNoRtcpMux.html
new file mode 100644
index 0000000000..49b0136752
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoNoBundleNoRtcpMux.html
@@ -0,0 +1,39 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1167443",
+ title: "Basic audio & video call with disabled bundle and disabled RTCP-Mux"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ options = options || { };
+ options.bundle = false;
+ options.rtcpmux = false;
+ test = new PeerConnectionTest(options);
+ test.chain.replace("PC_LOCAL_VERIFY_SDP_AFTER_END_OF_TRICKLE", [
+ function PC_LOCAL_REQUIRE_SDP_AFTER_END_OF_TRICKLE(test) {
+ return test.pcLocal.endOfTrickleSdp .then(sdp =>
+ sdputils.checkSdpAfterEndOfTrickle(sdp, test.testOptions, test.pcLocal.label));
+ }
+ ]);
+ test.chain.replace("PC_REMOTE_VERIFY_SDP_AFTER_END_OF_TRICKLE", [
+ function PC_REMOTE_REQUIRE_SDP_AFTER_END_OF_TRICKLE(test) {
+ return test.pcRemote.endOfTrickleSdp .then(sdp =>
+ sdputils.checkSdpAfterEndOfTrickle(sdp, test.testOptions, test.pcRemote.label));
+ }
+ ]);
+ test.setMediaConstraints([{audio: true}, {video: true}],
+ [{audio: true}, {video: true}]);
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoNoRtcpMux.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoNoRtcpMux.html
new file mode 100644
index 0000000000..48524604ba
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoNoRtcpMux.html
@@ -0,0 +1,38 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1167443",
+ title: "Basic audio & video call with disabled RTCP-Mux"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ options = options || { };
+ options.rtcpmux = false;
+ test = new PeerConnectionTest(options);
+ test.chain.replace("PC_LOCAL_VERIFY_SDP_AFTER_END_OF_TRICKLE", [
+ function PC_LOCAL_REQUIRE_SDP_AFTER_END_OF_TRICKLE(test) {
+ return test.pcLocal.endOfTrickleSdp .then(sdp =>
+ sdputils.checkSdpAfterEndOfTrickle(sdp, test.testOptions, test.pcLocal.label));
+ }
+ ]);
+ test.chain.replace("PC_REMOTE_VERIFY_SDP_AFTER_END_OF_TRICKLE", [
+ function PC_REMOTE_REQUIRE_SDP_AFTER_END_OF_TRICKLE(test) {
+ return test.pcRemote.endOfTrickleSdp .then(sdp =>
+ sdputils.checkSdpAfterEndOfTrickle(sdp, test.testOptions, test.pcRemote.label));
+ }
+ ]);
+ test.setMediaConstraints([{audio: true}, {video: true}],
+ [{audio: true}, {video: true}]);
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoTransceivers.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoTransceivers.html
new file mode 100644
index 0000000000..181d089d26
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoTransceivers.html
@@ -0,0 +1,31 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1290948",
+ title: "Basic audio/video with addTransceiver"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{audio: true}, {video: true}],
+ [{audio: true}, {video: true}]);
+ test.chain.replace("PC_LOCAL_GUM",
+ [
+ function PC_LOCAL_GUM_TRANSCEIVERS(test) {
+ return test.pcLocal.getAllUserMediaAndAddTransceivers(test.pcLocal.constraints);
+ }
+ ]);
+
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoVerifyExtmap.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoVerifyExtmap.html
new file mode 100644
index 0000000000..e3da00bfa5
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoVerifyExtmap.html
@@ -0,0 +1,97 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1406529",
+ title: "Verify SDP extmap attribute for sendrecv connection"
+ });
+
+ var test;
+ runNetworkTest(async function (options) {
+ await pushPrefs(["media.navigator.video.use_transport_cc", true]);
+
+ test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{audio: true}, {video: true}],
+ [{audio: true}, {video: true}]);
+
+ test.chain.insertAfter('PC_LOCAL_SET_LOCAL_DESCRIPTION', [
+ async function PC_LOCAL_CHECK_SDP_OFFER_EXTMAP() {
+ sdputils.verify_unique_extmap_ids(test.originalOffer.sdp);
+
+ const audio = sdputils.findExtmapIdsUrnsDirections(
+ sdputils.getAudioMSections(test.originalOffer.sdp));
+ const expected_audio = [
+ /* Please modify this list when you add or remove RTP header
+ extensions. */
+ ["1", "urn:ietf:params:rtp-hdrext:ssrc-audio-level", ""],
+ ["2", "urn:ietf:params:rtp-hdrext:csrc-audio-level", "recvonly"],
+ ["3", "urn:ietf:params:rtp-hdrext:sdes:mid", ""],
+ ];
+ // *Ugh* ...
+ ok(JSON.stringify(audio) ===
+ JSON.stringify(expected_audio),
+ "List of offer audio URNs meets expected values");
+
+ const video = sdputils.findExtmapIdsUrnsDirections(
+ sdputils.getVideoMSections(test.originalOffer.sdp));
+ const expected_video = [
+ /* Please modify this list when you add or remove RTP header
+ extensions. */
+ ["3", "urn:ietf:params:rtp-hdrext:sdes:mid", ""],
+ ["4", "http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time", ""],
+ ["5", "urn:ietf:params:rtp-hdrext:toffset", ""],
+ ["6", "http://www.webrtc.org/experiments/rtp-hdrext/playout-delay", "recvonly"],
+ ["7", "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01", ""],
+ ];
+ // *Ugh* ...
+ ok(JSON.stringify(video) ===
+ JSON.stringify(expected_video),
+ "List of offer video URNs meets expected values");
+ }
+ ]);
+
+ test.chain.removeAfter('PC_REMOTE_SET_LOCAL_DESCRIPTION');
+ test.chain.append([
+ async function PC_REMOTE_CHECK_SDP_ANSWER_EXTMAP() {
+ sdputils.verify_unique_extmap_ids(test.originalAnswer.sdp);
+
+ const audio = sdputils.findExtmapIdsUrnsDirections(
+ sdputils.getAudioMSections(test.originalAnswer.sdp));
+ const expected_audio = [
+ /* Please modify this list when you add or remove RTP header
+ extensions. */
+ ["1", "urn:ietf:params:rtp-hdrext:ssrc-audio-level",""],
+ ["3", "urn:ietf:params:rtp-hdrext:sdes:mid",""],
+ ];
+ // *Ugh* ...
+ ok(JSON.stringify(audio) ===
+ JSON.stringify(expected_audio),
+ "List of answer audio URNs meets expected values");
+
+ const video = sdputils.findExtmapIdsUrnsDirections(
+ sdputils.getVideoMSections(test.originalAnswer.sdp));
+ const expected_video = [
+ /* Please modify this list when you add or remove RTP header
+ extensions. */
+ ["3", "urn:ietf:params:rtp-hdrext:sdes:mid",""],
+ ["4", "http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time",""],
+ ["5", "urn:ietf:params:rtp-hdrext:toffset",""],
+ ["7", "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01", ""],
+ ];
+ ok(JSON.stringify(video) ===
+ JSON.stringify(expected_video),
+ "List of answer video URNs meets expected values");
+ }
+ ]);
+
+ await test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoVerifyExtmapSendonly.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoVerifyExtmapSendonly.html
new file mode 100644
index 0000000000..6cbc9e4c00
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoVerifyExtmapSendonly.html
@@ -0,0 +1,97 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1406529",
+ title: "Verify SDP extmap attribute for sendonly connection"
+ });
+
+ var test;
+ runNetworkTest(async function (options) {
+ await pushPrefs(["media.navigator.video.use_transport_cc", true]);
+
+ test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{audio: true}, {video: true}],
+ []);
+
+ test.chain.insertAfter('PC_LOCAL_SET_LOCAL_DESCRIPTION', [
+ async function PC_LOCAL_CHECK_SDP_OFFER_EXTMAP() {
+ sdputils.verify_unique_extmap_ids(test.originalOffer.sdp);
+
+ const audio = sdputils.findExtmapIdsUrnsDirections(
+ sdputils.getAudioMSections(test.originalOffer.sdp));
+ const expected_audio = [
+ /* Please modify this list when you add or remove RTP header
+ extensions. */
+ ["1", "urn:ietf:params:rtp-hdrext:ssrc-audio-level", ""],
+ ["2", "urn:ietf:params:rtp-hdrext:csrc-audio-level", "recvonly"],
+ ["3", "urn:ietf:params:rtp-hdrext:sdes:mid", ""],
+ ];
+ // *Ugh* ...
+ ok(JSON.stringify(audio) ===
+ JSON.stringify(expected_audio),
+ "List of offer audio URNs meets expected values");
+
+ const video = sdputils.findExtmapIdsUrnsDirections(
+ sdputils.getVideoMSections(test.originalOffer.sdp));
+ const expected_video = [
+ /* Please modify this list when you add or remove RTP header
+ extensions. */
+ ["3", "urn:ietf:params:rtp-hdrext:sdes:mid", ""],
+ ["4", "http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time", ""],
+ ["5", "urn:ietf:params:rtp-hdrext:toffset", ""],
+ ["6", "http://www.webrtc.org/experiments/rtp-hdrext/playout-delay", "recvonly"],
+ ["7", "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01", ""],
+ ];
+ // *Ugh* ...
+ ok(JSON.stringify(video) ===
+ JSON.stringify(expected_video),
+ "List of offer video URNs meets expected values");
+ }
+ ]);
+
+ test.chain.removeAfter('PC_REMOTE_SET_LOCAL_DESCRIPTION');
+ test.chain.append([
+ async function PC_REMOTE_CHECK_SDP_ANSWER_EXTMAP() {
+ sdputils.verify_unique_extmap_ids(test.originalAnswer.sdp);
+
+ const audio = sdputils.findExtmapIdsUrnsDirections(
+ sdputils.getAudioMSections(test.originalAnswer.sdp));
+ const expected_audio = [
+ /* Please modify this list when you add or remove RTP header
+ extensions. */
+ ["1", "urn:ietf:params:rtp-hdrext:ssrc-audio-level",""],
+ ["3", "urn:ietf:params:rtp-hdrext:sdes:mid",""],
+ ];
+ // *Ugh* ...
+ ok(JSON.stringify(audio) ===
+ JSON.stringify(expected_audio),
+ "List of answer audio URNs meets expected values");
+
+ const video = sdputils.findExtmapIdsUrnsDirections(
+ sdputils.getVideoMSections(test.originalAnswer.sdp));
+ const expected_video = [
+ /* Please modify this list when you add or remove RTP header
+ extensions. */
+ ["3", "urn:ietf:params:rtp-hdrext:sdes:mid",""],
+ ["4", "http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time",""],
+ ["5", "urn:ietf:params:rtp-hdrext:toffset",""],
+ ["7", "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01", ""],
+ ];
+ ok(JSON.stringify(video) ===
+ JSON.stringify(expected_video),
+ "List of answer video URNs meets expected values");
+ }
+ ]);
+
+ await test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoVerifyTooLongMidFails.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoVerifyTooLongMidFails.html
new file mode 100644
index 0000000000..70d27b48c6
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoVerifyTooLongMidFails.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1427009",
+ title: "Test mid longer than 16 characters fails"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ options = options || { };
+ options.bundle = false;
+ test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{audio: true}, {video: true}],
+ [{audio: true}, {video: true}]);
+
+ test.chain.replaceAfter("PC_LOCAL_CREATE_OFFER",
+ [
+ function PC_LOCAL_MUNGE_OFFER_SDP(test) {
+ test.originalOffer.sdp =
+ test.originalOffer.sdp.replace(/a=mid:.*\r\n/g,
+ "a=mid:really_long_mid_over_16_chars\r\n");
+ },
+ function PC_LOCAL_EXPECT_SET_LOCAL_DESCRIPTION_FAIL(test) {
+ return test.setLocalDescription(test.pcLocal,
+ test.originalOffer,
+ HAVE_LOCAL_OFFER)
+ .then(() => ok(false, "setLocalDescription must fail"),
+ // This needs to be RTCError once we support it, and once we
+ // stop allowing any modification, InvalidModificationError
+ e => is(e.name, "OperationError",
+ "setLocalDescription must fail and did"));
+ }
+ ], 0 // first occurance
+ );
+
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudio_forced_higher_rate.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudio_forced_higher_rate.html
new file mode 100644
index 0000000000..95bfb06514
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudio_forced_higher_rate.html
@@ -0,0 +1,19 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="peerconnection_audio_forced_sample_rate.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+ bug: "1437366",
+ title: "Basic audio-only peer connection, with the MTG running at a rate not supported by the MediaPipeline (49000Hz)"
+});
+
+test_peerconnection_audio_forced_sample_rate(49000);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudio_forced_lower_rate.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudio_forced_lower_rate.html
new file mode 100644
index 0000000000..aab9778971
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudio_forced_lower_rate.html
@@ -0,0 +1,19 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="peerconnection_audio_forced_sample_rate.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+ bug: "1437366",
+ title: "Basic audio-only peer connection, with the MTG running at a rate not supported by the MediaPipeline (24000Hz)"
+});
+
+test_peerconnection_audio_forced_sample_rate(24000);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicH264Video.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicH264Video.html
new file mode 100644
index 0000000000..072c35da39
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicH264Video.html
@@ -0,0 +1,26 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1040346",
+ title: "Basic H.264 GMP video-only peer connection"
+ });
+
+ var test;
+ runNetworkTest(async function (options) {
+ matchPlatformH264CodecPrefs();
+ options = options || { };
+ options.h264 = true;
+ test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{video: true}], [{video: true}]);
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicScreenshare.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicScreenshare.html
new file mode 100644
index 0000000000..93148ac5fd
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicScreenshare.html
@@ -0,0 +1,56 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1039666",
+ title: "Basic screenshare-only peer connection"
+ });
+
+ async function supportedVideoPayloadTypes() {
+ const pc = new RTCPeerConnection();
+ const offer = await pc.createOffer({offerToReceiveVideo: true});
+ return sdputils.getPayloadTypes(offer.sdp);
+ }
+
+ async function testScreenshare(payloadType) {
+ const options = {};
+ options.h264 = payloadType == 97 || payloadType == 126;
+ const test = new PeerConnectionTest(options);
+ const constraints = {
+ video: { mediaSource: "screen" },
+ };
+ test.setMediaConstraints([constraints], []);
+ test.chain.insertAfterEach("PC_LOCAL_CREATE_OFFER", [
+ function PC_LOCAL_ISOLATE_CODEC() {
+ info(`Forcing payload type ${payloadType}. Note that other associated ` +
+ `payload types, like RTX, are removed too.`);
+ test.originalOffer.sdp =
+ sdputils.removeAllButPayloadType(test.originalOffer.sdp, payloadType);
+ },
+ ]);
+ await test.run();
+ }
+
+ runNetworkTest(async () => {
+ await matchPlatformH264CodecPrefs();
+ const pts = await supportedVideoPayloadTypes();
+ ok(pts.includes("120"), "VP8 is supported");
+ ok(pts.includes("121"), "VP9 is supported");
+ if (pts.length > 2) {
+ is(pts.length, 4, "Expected VP8, VP9 and two variants of H264");
+ ok(pts.includes("97"), "H264 with no packetization-mode is supported");
+ ok(pts.includes("126"), "H264 with packetization-mode=1 is supported");
+ }
+ for (const pt of pts) {
+ await testScreenshare(pt);
+ }
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicVideo.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicVideo.html
new file mode 100644
index 0000000000..4a0655d696
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicVideo.html
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "796888",
+ title: "Basic video-only peer connection"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{video: true}], [{video: true}]);
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicVideoVerifyRtpHeaderExtensions.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicVideoVerifyRtpHeaderExtensions.html
new file mode 100644
index 0000000000..7874e52a10
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicVideoVerifyRtpHeaderExtensions.html
@@ -0,0 +1,82 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="parser_rtp.js"></script>
+ <script type="application/javascript" src="sdpUtils.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1416932",
+ title: "Basic video-only peer connection and verify rtp header extensions"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{video: true}], [{video: true}]);
+
+ let getRtpPacketWithExtension = (pc, extension) => {
+ // we only examine received packets
+ let sending = false;
+ pc.mozEnablePacketDump(0, "rtp", sending);
+ return new Promise((res, rej) =>
+ pc.mozSetPacketCallback((...args) => {
+ const packet = ParseRtpPacket(args[3]);
+ info(`midId = ${extension} packet = ${JSON.stringify(packet, null, 2)}`);
+ if (packet.header.extensions.find(e => e.id == extension) !== undefined) {
+ res(packet);
+ pc.mozSetPacketCallback(() => {});
+ pc.mozDisablePacketDump(0, "rtp", sending);
+ }
+ })
+ );
+ }
+
+ let havePacketWithMid;
+ let sdpExtmaps;
+
+ // MID can stop being sent when acked causing failures if packets are checked later.
+ // Starting packet sniffer before PC_LOCAL_SET_REMOTE_DESCRIPTION to be ready
+ // to inspect packets ahead of any packets arriving.
+ test.chain.insertBefore('PC_LOCAL_SET_REMOTE_DESCRIPTION', [
+ function PC_REMOTE_FIND_RTP_PACKETS_WITH_MIDID() {
+
+ sdpExtmaps = sdputils.findExtmapIdsUrnsDirections(test.originalAnswer.sdp);
+ const [midId] = sdpExtmaps.find(([, urn]) => urn == "urn:ietf:params:rtp-hdrext:sdes:mid");
+ const pc = SpecialPowers.wrap(test.pcRemote._pc);
+ havePacketWithMid = getRtpPacketWithExtension(pc, midId);
+ }
+ ]);
+
+ test.chain.insertBefore('PC_REMOTE_WAIT_FOR_MEDIA_FLOW', [
+ async function PC_REMOTE_CHECK_RTP_HEADER_EXTS_AGAINST_SDP() {
+
+ const sdpExtmapIds = sdpExtmaps.map(e => e[0]);
+ const packet = await havePacketWithMid;
+ const extIds = packet.header.extensions.map(e => `${e.id}`);
+ // make sure we got the same number of rtp header extensions in
+ // the received packet as were negotiated in the sdp. Then
+ // check to make sure each of the received extension ids were in
+ // the sdp.
+ is(sdpExtmapIds.length, extIds.length,
+ `number of sdp ids match received ids ` +
+ `${JSON.stringify(sdpExtmapIds)} == ${JSON.stringify(extIds)}\n` +
+ `sdp = ${test.originalAnswer.sdp}\n` +
+ `packet = ${JSON.stringify(packet, null, 2)}`);
+ // note, we are comparing a number (from the parsed rtp packet)
+ // and a string (from the answer sdp)
+ ok(extIds.every(id => sdpExtmapIds.includes(id)) &&
+ sdpExtmapIds.every(id => extIds.includes(id)),
+ `extension id arrays equivalent`);
+ }
+ ]);
+
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicWindowshare.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicWindowshare.html
new file mode 100644
index 0000000000..1cfb0797db
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicWindowshare.html
@@ -0,0 +1,25 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1038926",
+ title: "Basic windowshare-only peer connection"
+ });
+
+ runNetworkTest(function (options) {
+ const test = new PeerConnectionTest(options);
+ const constraints = {
+ video: { mediaSource: "window" },
+ };
+ test.setMediaConstraints([constraints], []);
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_bug1013809.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_bug1013809.html
new file mode 100644
index 0000000000..a8c7004793
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_bug1013809.html
@@ -0,0 +1,25 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1013809",
+ title: "Audio-only peer connection with swapped setLocal and setRemote steps"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ test = new PeerConnectionTest(options);
+ var sld = test.chain.remove("PC_REMOTE_SET_LOCAL_DESCRIPTION");
+ test.chain.insertAfter("PC_LOCAL_SET_REMOTE_DESCRIPTION", sld);
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_bug1042791.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_bug1042791.html
new file mode 100644
index 0000000000..a84dcf9d09
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_bug1042791.html
@@ -0,0 +1,36 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1040346",
+ title: "Basic H.264 GMP video-only peer connection"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ options = options || { };
+ options.h264 = true;
+ test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{video: true}], [{video: true}]);
+ test.chain.removeAfter("PC_LOCAL_CREATE_OFFER");
+
+ test.chain.append([
+ function PC_LOCAL_VERIFY_H264_OFFER(test) {
+ ok(!test.pcLocal._latest_offer.sdp.toLowerCase().includes("profile-level-id=0x42e0"),
+ "H264 offer does not contain profile-level-id=0x42e0");
+ ok(test.pcLocal._latest_offer.sdp.toLowerCase().includes("profile-level-id=42e0"),
+ "H264 offer contains profile-level-id=42e0");
+ }
+ ]);
+
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_bug1227781.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_bug1227781.html
new file mode 100644
index 0000000000..41e4aec457
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_bug1227781.html
@@ -0,0 +1,37 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1227781",
+ title: "Test with invalid TURN server"
+ });
+
+ const turnConfig = {
+ iceServers: [
+ {
+ username: "mozilla",
+ credential: "mozilla",
+ url: "turn:test@10.0.0.1",
+ },
+ ],
+ };
+ runNetworkTest(function (options) {
+ let exception = false;
+ try {
+ new RTCPeerConnection(turnConfig);
+ } catch (e) {
+ info(e);
+ exception = true;
+ }
+ is(exception, true, "Exception fired");
+ ok("Success");
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_bug1512281.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_bug1512281.html
new file mode 100644
index 0000000000..e6451becea
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_bug1512281.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1512281",
+ title: "Test that RTCP sender and receiver stats are not swapped"
+ });
+
+const ensure_missing_rtcp = async stats => {
+ const rtcp_stats = [...stats.values()].filter(
+ s => s.type.endsWith("bound-rtp") &&
+ s.isRemote == true).map(s => JSON.stringify(s))
+ is(rtcp_stats, [],
+ "There are no RTCP stats when RTCP reception is turned off");
+};
+
+const PC_LOCAL_TEST_FOR_MISSING_RTCP = async test =>
+ await ensure_missing_rtcp(await test.pcLocal.getStats());
+
+const PC_REMOTE_TEST_FOR_MISSING_RTCP = async test =>
+ await ensure_missing_rtcp(await test.pcRemote.getStats());
+
+runNetworkTest(async options => {
+ await pushPrefs(["media.webrtc.net.force_disable_rtcp_reception", true]);
+
+ const test = new PeerConnectionTest(options);
+
+ test.chain.insertAfter("PC_LOCAL_WAIT_FOR_MEDIA_FLOW",
+ [PC_LOCAL_TEST_FOR_MISSING_RTCP]);
+
+ test.chain.insertAfter("PC_REMOTE_WAIT_FOR_MEDIA_FLOW",
+ [PC_REMOTE_TEST_FOR_MISSING_RTCP]);
+
+ test.setMediaConstraints([{audio: true}, {video: true}],
+ [{audio: true}, {video: true}]);
+ await test.run();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_bug1773067.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_bug1773067.html
new file mode 100644
index 0000000000..9e6d79a107
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_bug1773067.html
@@ -0,0 +1,32 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1773067",
+ title: "getStats on a closed peer connection should fail, not hang, " +
+ " until bug 1056433 is fixed"
+ });
+
+ // TODO: Bug 1056433 removes the need for this test
+ runNetworkTest(async function () {
+ let errorName;
+ try {
+ const pc = new RTCPeerConnection();
+ pc.close();
+ await pc.getStats();
+ } catch(e) {
+ errorName = e.name;
+ }
+ is(errorName,
+ "InvalidStateError",
+ "getStats on closed peer connection fails instead of hanging");
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_bug822674.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_bug822674.html
new file mode 100644
index 0000000000..fceb2c2a1d
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_bug822674.html
@@ -0,0 +1,26 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "822674",
+ title: "RTCPeerConnection isn't a true javascript object as it should be"
+ });
+
+ runNetworkTest(function () {
+ var pc = new RTCPeerConnection();
+
+ pc.thereIsNeverGoingToBeAPropertyWithThisNameOnThisInterface = 1;
+ is(pc.thereIsNeverGoingToBeAPropertyWithThisNameOnThisInterface, 1,
+ "Can set expandos on an RTCPeerConnection");
+
+ pc = null;
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_bug825703.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_bug825703.html
new file mode 100644
index 0000000000..5cd168af8a
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_bug825703.html
@@ -0,0 +1,140 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "825703",
+ title: "RTCConfiguration valid/invalid permutations"
+ });
+
+// ^^^ Don't insert data above this line without adjusting line number below!
+var lineNumberAndFunction = {
+// <--- 16 is the line this must be.
+ line: 17, func: () => new RTCPeerConnection().onaddstream = () => {}
+};
+
+var makePC = (config, expected_error) => {
+ var exception;
+ try {
+ new RTCPeerConnection(config).close();
+ } catch (e) {
+ exception = e;
+ }
+ is((exception? exception.name : "success"), expected_error || "success",
+ "RTCPeerConnection(" + JSON.stringify(config) + ")");
+};
+
+// The order of properties in objects is not guaranteed in JavaScript, so this
+// transform produces json-comparable dictionaries. The resulting copy is only
+// meant to be used in comparisons (e.g. array-ness is not preserved).
+
+var toComparable = o =>
+ (typeof o != 'object' || !o)? o : Object.keys(o).sort().reduce((co, key) => {
+ co[key] = toComparable(o[key]);
+ return co;
+}, {});
+
+// This is a test of the iceServers parsing code + readable errors
+runNetworkTest(() => {
+ var exception = null;
+
+ try {
+ new RTCPeerConnection().close();
+ } catch (e) {
+ exception = e;
+ }
+ ok(!exception, "RTCPeerConnection() succeeds");
+ exception = null;
+
+ // Some overlap still with WPT RTCConfiguration-iceServers.html
+
+ makePC({ iceServers: [
+ { urls:"stun:127.0.0.1" },
+ { urls:"stun:localhost", foo:"" },
+ { urls: ["stun:127.0.0.1", "stun:localhost"] },
+ { urls:"stuns:localhost", foo:"" },
+ { urls:"turn:[::1]:3478", username:"p", credential:"p" },
+ { urls:"turn:[::1]:3478", username:"", credential:"" },
+ { urls:"turns:[::1]:3478", username:"", credential:"" },
+ { urls:"turn:localhost:3478?transport=udp", username:"p", credential:"p" },
+ { urls: ["turn:[::1]:3478", "turn:localhost"], username:"p", credential:"p" },
+ { urls:"turns:localhost:3478?transport=udp", username:"p", credential:"p" },
+ { url:"stun:localhost", foo:"" },
+ { url:"turn:localhost", username:"p", credential:"p" }
+ ]});
+
+ makePC({ iceServers: [{ urls:"http:0.0.0.0" }] }, "SyntaxError");
+
+ try {
+ new RTCPeerConnection({ iceServers: [{ url:"http:0.0.0.0" }] }).close();
+ } catch (e) {
+ ok(e.message.indexOf("http") > 0,
+ "RTCPeerConnection() constructor has readable exceptions");
+ }
+
+ // Test getConfiguration
+ const config = {
+ bundlePolicy: "max-bundle",
+ iceTransportPolicy: "relay",
+ peerIdentity: null,
+ certificates: [],
+ iceServers: [
+ { urls: ["stun:127.0.0.1", "stun:localhost"], credentialType:"password" },
+ { urls: ["turn:[::1]:3478"], username:"p", credential:"p", credentialType:"password" },
+ ],
+ };
+ // Make sure sdpSemantics is not exposed in getConfiguration
+ const configWithExtraProps = Object.assign({},
+ config,
+ {sdpSemantics: "plan-b"});
+ ok("sdpSemantics" in configWithExtraProps, "sdpSemantics control");
+
+ const pc = new RTCPeerConnection(configWithExtraProps);
+ is(JSON.stringify(toComparable(pc.getConfiguration())),
+ JSON.stringify(toComparable(config)), "getConfiguration");
+ pc.close();
+
+ var push = prefs => SpecialPowers.pushPrefEnv(prefs);
+
+ return Promise.resolve()
+ // This set of tests are setting the about:config User preferences for default
+ // ice servers and checking the outputs when RTCPeerConnection() is
+ // invoked. See Bug 1167922 for more information.
+ .then(() => push({ set: [['media.peerconnection.default_iceservers', ""]] })
+ .then(() => makePC())
+ .then(() => push({ set: [['media.peerconnection.default_iceservers', "k"]] }))
+ .then(() => makePC())
+ .then(() => push({ set: [['media.peerconnection.default_iceservers', "[{\"urls\": [\"stun:stun.services.mozilla.com\"]}]"]] }))
+ .then(() => makePC()))
+ // This set of tests check that warnings work. See Bug 1254839 for more.
+ .then(() => {
+ let promise = new Promise(resolve => {
+ SpecialPowers.registerConsoleListener(msg => {
+ if (msg.message.includes("onaddstream")) {
+ SpecialPowers.postConsoleSentinel();
+ resolve(msg.message);
+ }
+ });
+ });
+ lineNumberAndFunction.func();
+ return promise;
+ }).then(warning => {
+ is(warning.split('"')[1],
+ "WebRTC: onaddstream is deprecated! Use peerConnection.ontrack instead.",
+ "warning logged");
+ var remainder = warning.split('"').slice(2).join('"');
+ info(remainder);
+ ok(remainder.includes('file: "' + window.location + '"'),
+ "warning has this file");
+ ok(remainder.includes('line: ' + lineNumberAndFunction.line),
+ "warning has correct line number");
+ });
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_bug827843.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_bug827843.html
new file mode 100644
index 0000000000..06cfde9e5d
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_bug827843.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "827843",
+ title: "Ensure that localDescription and remoteDescription are null after close"
+ });
+
+var steps = [
+ function CHECK_SDP_ON_CLOSED_PC(test) {
+ var description;
+ var exception = null;
+
+ test.pcLocal.close();
+
+ try { description = test.pcLocal.localDescription; } catch (e) { exception = e; }
+ ok(exception, "Attempt to access localDescription of pcLocal after close throws exception");
+ exception = null;
+
+ try { description = test.pcLocal.remoteDescription; } catch (e) { exception = e; }
+ ok(exception, "Attempt to access remoteDescription of pcLocal after close throws exception");
+ exception = null;
+
+ test.pcRemote.close();
+
+ try { description = test.pcRemote.localDescription; } catch (e) { exception = e; }
+ ok(exception, "Attempt to access localDescription of pcRemote after close throws exception");
+ exception = null;
+
+ try { description = test.pcRemote.remoteDescription; } catch (e) { exception = e; }
+ ok(exception, "Attempt to access remoteDescription of pcRemote after close throws exception");
+ }
+];
+
+var test;
+runNetworkTest(() => {
+ test = new PeerConnectionTest();
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ test.chain.append(steps);
+ return test.run();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_bug834153.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_bug834153.html
new file mode 100644
index 0000000000..6d8ca2a7ce
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_bug834153.html
@@ -0,0 +1,36 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "834153",
+ title: "Queue CreateAnswer in PeerConnection.js"
+ });
+
+ runNetworkTest(function () {
+ var pc1 = new RTCPeerConnection();
+ var pc2 = new RTCPeerConnection();
+
+ return pc1.createOffer({ offerToReceiveAudio: true }).then(offer => {
+ // The whole point of this test is not to wait for the
+ // setRemoteDescription call to succesfully complete, so we
+ // don't wait for it to succeed.
+ pc2.setRemoteDescription(offer);
+ return pc2.createAnswer();
+ })
+ .then(answer => is(answer.type, "answer", "CreateAnswer created an answer"))
+ .catch(reason => ok(false, reason.message))
+ .then(() => {
+ pc1.close();
+ pc2.close();
+ })
+ .catch(reason => ok(false, reason.message));
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_callbacks.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_callbacks.html
new file mode 100644
index 0000000000..4c890e4400
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_callbacks.html
@@ -0,0 +1,86 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ title: "PeerConnection using callback functions",
+ bug: "1119593",
+ visible: true
+ });
+
+// This still aggressively uses promises, but it is testing that the callback functions
+// are properly in place.
+
+// wrapper that turns a callback-based function call into a promise
+function pcall(o, f, beforeArg) {
+ return new Promise((resolve, reject) => {
+ var args = [resolve, reject];
+ if (typeof beforeArg !== 'undefined') {
+ args.unshift(beforeArg);
+ }
+ info('Calling ' + f.name);
+ f.apply(o, args);
+ });
+}
+
+var pc1 = new RTCPeerConnection();
+var pc2 = new RTCPeerConnection();
+
+var pc2_haveRemoteOffer = new Promise(resolve => {
+ pc2.onsignalingstatechange =
+ e => (e.target.signalingState == "have-remote-offer") && resolve();
+});
+var pc1_stable = new Promise(resolve => {
+ pc1.onsignalingstatechange =
+ e => (e.target.signalingState == "stable") && resolve();
+});
+
+pc1.onicecandidate = e => {
+ pc2_haveRemoteOffer
+ .then(() => !e.candidate || pcall(pc2, pc2.addIceCandidate, e.candidate))
+ .catch(generateErrorCallback());
+};
+pc2.onicecandidate = e => {
+ pc1_stable
+ .then(() => !e.candidate || pcall(pc1, pc1.addIceCandidate, e.candidate))
+ .catch(generateErrorCallback());
+};
+
+var v1, v2;
+var delivered = new Promise(resolve => {
+ pc2.onaddstream = e => {
+ v2.srcObject = e.stream;
+ resolve(e.stream);
+ };
+});
+
+runNetworkTest(function() {
+ v1 = createMediaElement('video', 'v1');
+ v2 = createMediaElement('video', 'v2');
+ var canPlayThrough = new Promise(resolve => v2.canplaythrough = resolve);
+ is(v2.currentTime, 0, "v2.currentTime is zero at outset");
+
+ // not testing legacy gUM here
+ return navigator.mediaDevices.getUserMedia({ video: true, audio: true })
+ .then(stream => pc1.addStream(v1.srcObject = stream))
+ .then(() => pcall(pc1, pc1.createOffer))
+ .then(offer => pcall(pc1, pc1.setLocalDescription, offer))
+ .then(() => pcall(pc2, pc2.setRemoteDescription, pc1.localDescription))
+ .then(() => pcall(pc2, pc2.createAnswer))
+ .then(answer => pcall(pc2, pc2.setLocalDescription, answer))
+ .then(() => pcall(pc1, pc1.setRemoteDescription, pc2.localDescription))
+ .then(() => delivered)
+ // .then(() => canPlayThrough) // why doesn't this fire?
+ .then(() => waitUntil(() => v2.currentTime > 0))
+ .then(() => ok(v2.currentTime > 0, "v2.currentTime is moving (" + v2.currentTime + ")"))
+ .then(() => ok(true, "Connected."))
+ .then(() => { v1.pause(); v2.pause(); });
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_captureStream_canvas_2d.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_captureStream_canvas_2d.html
new file mode 100644
index 0000000000..db3a735008
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_captureStream_canvas_2d.html
@@ -0,0 +1,81 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+ bug: "1032848",
+ title: "Canvas(2D)::CaptureStream as video-only input to peerconnection",
+ visible: true
+});
+
+runNetworkTest(async () => {
+ // [TODO] re-enable HW decoder after bug 1526207 is fixed.
+ if (navigator.userAgent.includes("Android")) {
+ await pushPrefs(["media.navigator.mediadatadecoder_vpx_enabled", false],
+ ["media.webrtc.hw.h264.enabled", false]);
+ }
+
+ var test = new PeerConnectionTest();
+ var mediaElement;
+ var h = new CaptureStreamTestHelper2D();
+ var canvas = document.createElement('canvas');
+ var stream;
+ canvas.id = 'source_canvas';
+ canvas.width = canvas.height = 16;
+ document.getElementById('content').appendChild(canvas);
+
+ test.setMediaConstraints([{video: true}], []);
+ test.chain.replace("PC_LOCAL_GUM", [
+ function PC_LOCAL_CANVAS_CAPTURESTREAM(test) {
+ h.drawColor(canvas, h.green);
+ stream = canvas.captureStream(0);
+ test.pcLocal.attachLocalStream(stream);
+ stream.requestFrame();
+ var i = 0;
+ return setInterval(function() {
+ try {
+ info("draw " + i ? "green" : "red");
+ h.drawColor(canvas, i ? h.green : h.red);
+ i = 1 - i;
+ stream.requestFrame();
+ } catch (e) {
+ // ignore; stream might have shut down, and we don't bother clearing
+ // the setInterval.
+ }
+ }, 500);
+ }
+ ]);
+ test.chain.append([
+ function PC_REMOTE_WAIT_FOR_REMOTE_GREEN() {
+ mediaElement = test.pcRemote.remoteMediaElements[0];
+ ok(!!mediaElement, "Should have remote video element for pcRemote");
+ return h.pixelMustBecome(mediaElement, h.green, {
+ threshold: 128,
+ infoString: "pcRemote's remote should become green",
+ });
+ },
+ function PC_LOCAL_DRAW_LOCAL_RED() {
+ // After requesting a frame it will be captured at the time of next render.
+ // Next render will happen at next stable state, at the earliest,
+ // i.e., this order of `requestFrame(); draw();` should work.
+ stream.requestFrame();
+ h.drawColor(canvas, h.red);
+ },
+ function PC_REMOTE_WAIT_FOR_REMOTE_RED() {
+ return h.pixelMustBecome(mediaElement, h.red, {
+ threshold: 128,
+ infoString: "pcRemote's remote should become red",
+ });
+ }
+ ]);
+ await test.run();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_captureStream_canvas_2d_noSSRC.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_captureStream_canvas_2d_noSSRC.html
new file mode 100644
index 0000000000..e33a7e8886
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_captureStream_canvas_2d_noSSRC.html
@@ -0,0 +1,83 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+ title: "Canvas(2D)::CaptureStream as video-only input to peerconnection with no a=ssrc",
+ visible: true
+});
+
+var test;
+runNetworkTest(async (options) => {
+ // [TODO] re-enable HW decoder after bug 1526207 is fixed.
+ if (navigator.userAgent.includes("Android")) {
+ await pushPrefs(["media.navigator.mediadatadecoder_vpx_enabled", false],
+ ["media.webrtc.hw.h264.enabled", false]);
+ }
+
+ options = options || { };
+ options.ssrc = false;
+ test = new PeerConnectionTest(options);
+ var mediaElement;
+ var h = new CaptureStreamTestHelper2D();
+ var canvas = document.createElement('canvas');
+ var stream;
+ canvas.id = 'source_canvas';
+ canvas.width = canvas.height = 16;
+ document.getElementById('content').appendChild(canvas);
+
+ test.setMediaConstraints([{video: true}], []);
+ test.chain.replace("PC_LOCAL_GUM", [
+ function PC_LOCAL_CANVAS_CAPTURESTREAM(test) {
+ h.drawColor(canvas, h.green);
+ stream = canvas.captureStream(0);
+ test.pcLocal.attachLocalStream(stream);
+ stream.requestFrame();
+ var i = 0;
+ return setInterval(function() {
+ try {
+ info("draw " + i ? "green" : "red");
+ h.drawColor(canvas, i ? h.green : h.red);
+ i = 1 - i;
+ stream.requestFrame();
+ } catch (e) {
+ // ignore; stream might have shut down, and we don't bother clearing
+ // the setInterval.
+ }
+ }, 500);
+ }
+ ]);
+ test.chain.append([
+ function PC_REMOTE_WAIT_FOR_REMOTE_GREEN() {
+ mediaElement = test.pcRemote.remoteMediaElements[0];
+ ok(!!mediaElement, "Should have remote video element for pcRemote");
+ return h.pixelMustBecome(mediaElement, h.green, {
+ threshold: 128,
+ infoString: "pcRemote's remote should become green",
+ });
+ },
+ function PC_LOCAL_DRAW_LOCAL_RED() {
+ // After requesting a frame it will be captured at the time of next render.
+ // Next render will happen at next stable state, at the earliest,
+ // i.e., this order of `requestFrame(); draw();` should work.
+ stream.requestFrame();
+ h.drawColor(canvas, h.red);
+ },
+ function PC_REMOTE_WAIT_FOR_REMOTE_RED() {
+ return h.pixelMustBecome(mediaElement, h.red, {
+ threshold: 128,
+ infoString: "pcRemote's remote should become red",
+ });
+ }
+ ]);
+ await test.run();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_captureStream_canvas_webgl.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_captureStream_canvas_webgl.html
new file mode 100644
index 0000000000..167379fb37
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_captureStream_canvas_webgl.html
@@ -0,0 +1,130 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script>
+ <script type="application/javascript" src="/tests/dom/canvas/test/webgl-mochitest/webgl-util.js"></script>
+</head>
+<body>
+<pre id="test">
+<script id="v-shader" type="x-shader/x-vertex">
+ attribute vec2 aPosition;
+ void main() {
+ gl_Position = vec4(aPosition, 0, 1);
+}
+</script>
+<script id="f-shader" type="x-shader/x-fragment">
+ precision mediump float;
+ uniform vec4 uColor;
+ void main() { gl_FragColor = uColor; }
+</script>
+<script type="application/javascript">
+createHTML({
+ bug: "1032848",
+ title: "Canvas(WebGL)::CaptureStream as video-only input to peerconnection"
+});
+
+runNetworkTest(async () => {
+ // [TODO] re-enable HW decoder after bug 1526207 is fixed.
+ if (navigator.userAgent.includes("Android")) {
+ await pushPrefs(["media.navigator.mediadatadecoder_vpx_enabled", false],
+ ["media.webrtc.hw.h264.enabled", false]);
+ }
+
+ var test = new PeerConnectionTest();
+ var vremote;
+ var h = new CaptureStreamTestHelperWebGL();
+ var canvas = document.createElement('canvas');
+ canvas.id = 'source_canvas';
+ canvas.width = canvas.height = 16;
+ canvas.style.display = 'none';
+ document.getElementById('content').appendChild(canvas);
+
+ var gl = canvas.getContext('webgl');
+ if (!gl) {
+ todo(false, "WebGL unavailable.");
+ networkTestFinished();
+ return;
+ }
+
+ test.setMediaConstraints([{video: true}], []);
+ test.chain.replace("PC_LOCAL_GUM", [
+ function WEBGL_SETUP(test) {
+ var program = WebGLUtil.createProgramByIds(gl, 'v-shader', 'f-shader');
+
+ if (!program) {
+ ok(false, "Program should link");
+ return Promise.reject("Program should link");
+ }
+ gl.useProgram(program);
+
+ var uColorLocation = gl.getUniformLocation(program, "uColor");
+ h.setFragmentColorLocation(uColorLocation);
+
+ var squareBuffer = gl.createBuffer();
+ gl.bindBuffer(gl.ARRAY_BUFFER, squareBuffer);
+
+ var vertices = [ 0, 0,
+ -1, 0,
+ 0, 1,
+ -1, 1 ];
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
+ squareBuffer.itemSize = 2;
+ squareBuffer.numItems = 4;
+
+ program.aPosition = gl.getAttribLocation(program, "aPosition");
+ gl.enableVertexAttribArray(program.aPosition);
+ gl.vertexAttribPointer(program.aPosition, squareBuffer.itemSize, gl.FLOAT, false, 0, 0);
+ },
+ function PC_LOCAL_CANVAS_CAPTURESTREAM(test) {
+ h.drawColor(canvas, h.green);
+ test.pcLocal.canvasStream = canvas.captureStream(0.0);
+ is(test.pcLocal.canvasStream.canvas, canvas, "Canvas attribute is correct");
+ test.pcLocal.attachLocalStream(test.pcLocal.canvasStream);
+ var i = 0;
+ return setInterval(function() {
+ try {
+ info("draw " + i ? "green" : "red");
+ h.drawColor(canvas, i ? h.green : h.red);
+ i = 1 - i;
+ test.pcLocal.canvasStream.requestFrame();
+ } catch (e) {
+ // ignore; stream might have shut down, and we don't bother clearing
+ // the setInterval.
+ }
+ }, 500);
+ }
+ ]);
+ test.chain.append([
+ function FIND_REMOTE_VIDEO() {
+ vremote = test.pcRemote.remoteMediaElements[0];
+ ok(!!vremote, "Should have remote video element for pcRemote");
+ },
+ function WAIT_FOR_REMOTE_GREEN() {
+ return h.pixelMustBecome(vremote, h.green, {
+ threshold: 128,
+ infoString: "pcRemote's remote should become green",
+ });
+ },
+ function REQUEST_FRAME(test) {
+ // After requesting a frame it will be captured at the time of next render.
+ // Next render will happen at next stable state, at the earliest,
+ // i.e., this order of `requestFrame(); draw();` should work.
+ test.pcLocal.canvasStream.requestFrame();
+ },
+ function DRAW_LOCAL_RED() {
+ h.drawColor(canvas, h.red);
+ },
+ function WAIT_FOR_REMOTE_RED() {
+ return h.pixelMustBecome(vremote, h.red, {
+ threshold: 128,
+ infoString: "pcRemote's remote should become red",
+ });
+ }
+ ]);
+ await test.run();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_capturedVideo.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_capturedVideo.html
new file mode 100644
index 0000000000..f6f48ba429
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_capturedVideo.html
@@ -0,0 +1,81 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script src="pc.js"></script>
+ <script src="../../../test/manifest.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+(async () => {
+ await createHTML({
+ bug: "1081409",
+ title: "Captured video-only over peer connection",
+ visible: true
+ });
+
+ // Run tests in sequence for log readability.
+ PARALLEL_TESTS = 1;
+ const manager = new MediaTestManager;
+
+ async function startTest(media, token) {
+ manager.started(token);
+ info(`Starting test for ${media.name}`);
+ const video = document.createElement('video');
+ video.id = "id_" + media.name;
+ video.width = 160;
+ video.height = 120;
+ video.muted = true;
+ video.controls = true;
+ video.preload = "metadata";
+ video.src = "../../../test/" + media.name;
+
+ document.getElementById("content").appendChild(video);
+
+ const onerror = new Promise(r => video.onerror = r).then(_ =>
+ new Error(`${media.name} failed in playback. code=${video.error.code}`));
+
+ await Promise.race([
+ new Promise(res => video.onloadedmetadata = res),
+ onerror,
+ ]);
+ onerror.catch(e => ok(false, e));
+ setupEnvironment();
+ await testConfigured;
+ const stream = video.mozCaptureStream();
+ const test = new PeerConnectionTest(
+ {
+ config_local: { label_suffix: media.name },
+ config_remote: { label_suffix: media.name },
+ }
+ );
+ test.setOfferOptions(
+ {
+ offerToReceiveVideo: false,
+ offerToReceiveAudio: false,
+ }
+ );
+ const hasVideo = !!stream.getVideoTracks().length;
+ const hasAudio = !!stream.getAudioTracks().length;
+ test.setMediaConstraints([{ video: hasVideo, audio: hasAudio }], []);
+ test.chain.replace("PC_LOCAL_GUM", [
+ function PC_LOCAL_CAPTUREVIDEO(test) {
+ test.pcLocal.attachLocalStream(stream);
+ },
+ ]);
+ test.chain.insertBefore("PC_LOCAL_WAIT_FOR_MEDIA_FLOW", [
+ function PC_LOCAL_START_MEDIA(test) {
+ video.play();
+ },
+ ]);
+ await test.run();
+ removeNodeAndSource(video);
+ manager.finished(token);
+ }
+
+ manager.runTests(getPlayableVideos(gLongerTests), startTest);
+})();
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_certificates.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_certificates.html
new file mode 100644
index 0000000000..561f285f60
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_certificates.html
@@ -0,0 +1,185 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1172785",
+ title: "Certificate management"
+ });
+
+ function badCertificate(config, expectedError, message) {
+ return RTCPeerConnection.generateCertificate(config)
+ .then(() => ok(false, message),
+ e => is(e.name, expectedError, message));
+ }
+
+ // Checks a handful of obviously bad options to RTCCertificate.create(). Most
+ // of the checking is done by the WebCrypto code underpinning this, hence the
+ // baffling error codes, but a sanity check is still in order.
+ function checkBadParameters() {
+ return Promise.all([
+ badCertificate({
+ name: "RSASSA-PKCS1-v1_5",
+ hash: "SHA-256",
+ modulusLength: 1023,
+ publicExponent: new Uint8Array([1, 0, 1])
+ }, "NotSupportedError", "1023-bit is too small to succeed"),
+
+ badCertificate({
+ name: "RSASSA-PKCS1-v1_5",
+ hash: "SHA-384",
+ modulusLength: 2048,
+ publicExponent: new Uint8Array([1, 0, 1])
+ }, "NotSupportedError", "SHA-384 isn't supported yet"),
+
+ // A SyntaxError happens in the "generate key operation" step, but
+ // webrtc-pc does not say to reject the promise if this step fails.
+ // It does say to throw NotSupportedError if we have passed "an
+ // algorithm that the user agent cannot or will not use to generate a
+ // certificate".
+ badCertificate({
+ name: "ECDH",
+ namedCurve: "P-256"
+ }, "NotSupportedError", "ECDH is rejected because the usage is neither \"deriveKey\" or \"deriveBits\""),
+
+ badCertificate({
+ name: "not a valid algorithm"
+ }, "NotSupportedError", "not a valid algorithm"),
+
+ badCertificate("ECDSA", "NotSupportedError", "a bare name is not enough"),
+
+ badCertificate({
+ name: "ECDSA",
+ namedCurve: "not a curve"
+ }, "NotSupportedError", "ECDSA with an unknown curve")
+ ]);
+ }
+
+ function createDB() {
+ var openDB = indexedDB.open("genericstore");
+ openDB.onupgradeneeded = e => {
+ var db = e.target.result;
+ db.createObjectStore("data");
+ };
+ return new Promise(resolve => {
+ openDB.onsuccess = e => resolve(e.target.result);
+ });
+ }
+
+ function resultPromise(tx, op) {
+ return new Promise((resolve, reject) => {
+ op.onsuccess = e => resolve(e.target.result);
+ op.onerror = () => reject(op.error);
+ tx.onabort = () => reject(tx.error);
+ });
+ }
+
+ function store(db, value) {
+ var tx = db.transaction("data", "readwrite");
+ var store = tx.objectStore("data");
+ return resultPromise(tx, store.put(value, "value"));
+ }
+
+ function retrieve(db) {
+ var tx = db.transaction("data", "readonly");
+ var store = tx.objectStore("data");
+ return resultPromise(tx, store.get("value"));
+ }
+
+ // Creates a database, stores a value, retrieves it.
+ function storeAndRetrieve(value) {
+ return createDB().then(db => {
+ return store(db, value)
+ .then(() => retrieve(db))
+ .then(retrieved => {
+ db.close();
+ return retrieved;
+ });
+ });
+ }
+
+ var test;
+ runNetworkTest(function (options) {
+ var expiredCert;
+ return Promise.resolve()
+ .then(() => RTCPeerConnection.generateCertificate({
+ name: "ECDSA",
+ namedCurve: "P-256",
+ expires: 1 // smallest possible expiration window
+ }))
+ .then(cert => {
+ ok(!isNaN(cert.expires), 'cert has expiration time');
+ info('Expires at ' + new Date(cert.expires));
+ expiredCert = cert;
+ })
+
+ .then(() => checkBadParameters())
+
+ .then(() => {
+ var delay = expiredCert.expires - Date.now();
+ // Hopefully this delay is never needed.
+ if (delay > 0) {
+ return new Promise(r => setTimeout(r, delay));
+ }
+ })
+ .then(() => {
+ ok(expiredCert.expires <= Date.now(), 'Cert should be at or past expiration');
+ try {
+ new RTCPeerConnection({ certificates: [expiredCert] });
+ ok(false, 'Constructing peer connection with an expired cert is not allowed');
+ } catch(e) {
+ is(e.name, 'InvalidAccessError',
+ 'Constructing peer connection with an expired certs is not allowed');
+ }
+ })
+
+ .then(() => Promise.all([
+ RTCPeerConnection.generateCertificate({
+ name: "ECDSA",
+ namedCurve: "P-256"
+ }),
+ RTCPeerConnection.generateCertificate({
+ name: "RSASSA-PKCS1-v1_5",
+ hash: "SHA-256",
+ modulusLength: 2048,
+ publicExponent: new Uint8Array([1, 0, 1])
+ })
+ ]))
+
+ // A round trip through indexedDB should not do anything.
+ .then(storeAndRetrieve)
+ .then(certs => {
+ try {
+ new RTCPeerConnection({ certificates: certs });
+ ok(false, 'Constructing peer connection with multiple certs is not allowed');
+ } catch(e) {
+ is(e.name, 'NotSupportedError',
+ 'Constructing peer connection with multiple certs is not allowed');
+ }
+ return certs;
+ })
+ .then(certs => {
+ test = new PeerConnectionTest({
+ config_local: {
+ certificates: [certs[0]]
+ },
+ config_remote: {
+ certificates: [certs[1]]
+ }
+ });
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ return test.run();
+ })
+ .catch(e => {
+ console.log('test failure', e);
+ ok(false, 'test failed: ' + e);
+ });
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_checkPacketDumpHook.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_checkPacketDumpHook.html
new file mode 100644
index 0000000000..248e102dd2
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_checkPacketDumpHook.html
@@ -0,0 +1,107 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1377299",
+ title: "Check that packet dump hooks generate callbacks"
+ });
+
+ function waitForPacket(pc, checkFunction) {
+ return new Promise(resolve => {
+ function onPacket(level, type, sending, packet) {
+ if (checkFunction(level, type, sending, packet)) {
+ SpecialPowers.wrap(pc).mozSetPacketCallback(() => {});
+ resolve();
+ }
+ }
+
+ SpecialPowers.wrap(pc).mozSetPacketCallback(onPacket);
+ }
+ );
+ }
+
+ async function waitForSendPacket(pc, type, level) {
+ await SpecialPowers.wrap(pc).mozEnablePacketDump(level, type, true);
+ await timeout(
+ waitForPacket(pc, (obsLevel, obsType, sending) => {
+ is(obsLevel, level, "Level for packet is " + level);
+ is(obsType, type, "Type for packet is " + type);
+ ok(sending, "This is a send packet");
+ return true;
+ }),
+ 10000, "Timeout waiting for " + type + " send packet on level " + level);
+ await SpecialPowers.wrap(pc).mozDisablePacketDump(level, type, true);
+ }
+
+ async function waitForRecvPacket(pc, type, level) {
+ await SpecialPowers.wrap(pc).mozEnablePacketDump(level, type, false);
+ await timeout(
+ waitForPacket(pc, (obsLevel, obsType, sending) => {
+ is(obsLevel, level, "Level for packet is " + level);
+ is(obsType, type, "Type for packet is " + type);
+ ok(!sending, "This is a recv packet");
+ return true;
+ }),
+ 10000, "Timeout waiting for " + type + " recv packet on level " + level);
+ await SpecialPowers.wrap(pc).mozDisablePacketDump(level, type, false);
+ }
+
+ var test;
+ runNetworkTest(function (options) {
+ test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{audio: true, video: true}],
+ [{audio: true, video: true}]);
+ // pc.js uses video elements by default, we want to test audio elements here
+ test.pcLocal.audioElementsOnly = true;
+
+ test.chain.insertBefore('PC_LOCAL_WAIT_FOR_MEDIA_FLOW',[
+ async function PC_LOCAL_CHECK_PACKET_DUMP_HOOKS() {
+ await waitForRecvPacket(test.pcLocal._pc, "rtp", 0);
+ await waitForRecvPacket(test.pcLocal._pc, "rtcp", 0);
+ await waitForRecvPacket(test.pcLocal._pc, "srtp", 0);
+ await waitForRecvPacket(test.pcLocal._pc, "srtcp", 0);
+ await waitForSendPacket(test.pcLocal._pc, "rtp", 0);
+ await waitForSendPacket(test.pcLocal._pc, "rtcp", 0);
+ await waitForSendPacket(test.pcLocal._pc, "srtp", 0);
+ await waitForSendPacket(test.pcLocal._pc, "srtcp", 0);
+
+ await waitForRecvPacket(test.pcLocal._pc, "rtp", 1);
+ await waitForRecvPacket(test.pcLocal._pc, "rtcp", 1);
+ await waitForRecvPacket(test.pcLocal._pc, "srtp", 1);
+ await waitForRecvPacket(test.pcLocal._pc, "srtcp", 1);
+ await waitForSendPacket(test.pcLocal._pc, "rtp", 1);
+ await waitForSendPacket(test.pcLocal._pc, "rtcp", 1);
+ await waitForSendPacket(test.pcLocal._pc, "srtp", 1);
+ await waitForSendPacket(test.pcLocal._pc, "srtcp", 1);
+ },
+ async function PC_REMOTE_CHECK_PACKET_DUMP_HOOKS() {
+ await waitForRecvPacket(test.pcRemote._pc, "rtp", 0);
+ await waitForRecvPacket(test.pcRemote._pc, "rtcp", 0);
+ await waitForRecvPacket(test.pcRemote._pc, "srtp", 0);
+ await waitForRecvPacket(test.pcRemote._pc, "srtcp", 0);
+ await waitForSendPacket(test.pcRemote._pc, "rtp", 0);
+ await waitForSendPacket(test.pcRemote._pc, "rtcp", 0);
+ await waitForSendPacket(test.pcRemote._pc, "srtp", 0);
+ await waitForSendPacket(test.pcRemote._pc, "srtcp", 0);
+
+ await waitForRecvPacket(test.pcRemote._pc, "rtp", 1);
+ await waitForRecvPacket(test.pcRemote._pc, "rtcp", 1);
+ await waitForRecvPacket(test.pcRemote._pc, "srtp", 1);
+ await waitForRecvPacket(test.pcRemote._pc, "srtcp", 1);
+ await waitForSendPacket(test.pcRemote._pc, "rtp", 1);
+ await waitForSendPacket(test.pcRemote._pc, "rtcp", 1);
+ await waitForSendPacket(test.pcRemote._pc, "srtp", 1);
+ await waitForSendPacket(test.pcRemote._pc, "srtcp", 1);
+ }
+ ]);
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_close.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_close.html
new file mode 100644
index 0000000000..3edf677203
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_close.html
@@ -0,0 +1,134 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "991877",
+ title: "Basic RTCPeerConnection.close() tests"
+ });
+
+ runNetworkTest(function () {
+ var pc = new RTCPeerConnection();
+ var sender = pc.addTrack(getSilentTrack(), new MediaStream());
+ var exception = null;
+ var eTimeout = null;
+
+ // everything should be in initial state
+ is(pc.signalingState, "stable", "Initial signalingState is 'stable'");
+ is(pc.iceConnectionState, "new", "Initial iceConnectionState is 'new'");
+ is(pc.iceGatheringState, "new", "Initial iceGatheringState is 'new'");
+
+ var finish;
+ var finished = new Promise(resolve => finish = resolve);
+
+ var mustNotSettle = (p, ms, msg) => Promise.race([
+ p.then(() => ok(false, msg + " must not settle"),
+ e => ok(false, msg + " must not settle. Got " + e.name)),
+ wait(ms).then(() => ok(true, msg + " must not settle"))
+ ]);
+
+ var silence = mustNotSettle(pc.createOffer(), 1000,
+ "createOffer immediately followed by close");
+ try {
+ pc.close();
+ } catch (e) {
+ exception = e;
+ }
+ is(exception, null, "closing the connection raises no exception");
+ is(pc.signalingState, "closed", "Final signalingState is 'closed'");
+ is(pc.iceConnectionState, "closed", "Final iceConnectionState is 'closed'");
+
+ // test that pc is really closed (and doesn't crash, bug 1259728)
+ try {
+ pc.getLocalStreams();
+ } catch (e) {
+ exception = e;
+ }
+ is(exception && exception.name, "InvalidStateError",
+ "pc.getLocalStreams should throw when closed");
+ exception = null;
+
+ try {
+ pc.close();
+ } catch (e) {
+ exception = e;
+ }
+ is(exception, null, "A second close() should not raise an exception");
+ is(pc.signalingState, "closed", "Final signalingState stays at 'closed'");
+ is(pc.iceConnectionState, "closed", "Final iceConnectionState stays at 'closed'");
+
+ // Due to a limitation in our WebIDL compiler that prevents overloads with
+ // both Promise and non-Promise return types, legacy APIs with callbacks
+ // are unable to continue to throw exceptions. Luckily the spec uses
+ // exceptions solely for "programming errors" so this should not hinder
+ // working code from working, which is the point of the legacy API. All
+ // new code should use the promise API.
+ //
+ // The legacy methods that no longer throw on programming errors like
+ // "invalid-on-close" are:
+ // - createOffer
+ // - createAnswer
+ // - setLocalDescription
+ // - setRemoteDescription
+ // - addIceCandidate
+ // - getStats
+ //
+ // These legacy methods fire the error callback instead. This is not
+ // entirely to spec but is better than ignoring programming errors.
+
+ var offer = new RTCSessionDescription({ sdp: "sdp", type: "offer" });
+ var answer = new RTCSessionDescription({ sdp: "sdp", type: "answer" });
+ var candidate = new RTCIceCandidate({ candidate: "dummy",
+ sdpMid: "test",
+ sdpMLineIndex: 3 });
+
+ var doesFail = (p, msg) => p.then(generateErrorCallback(msg),
+ r => is(r.name, "InvalidStateError", msg));
+ Promise.all([
+ [pc.createOffer(), "createOffer"],
+ [pc.createOffer({offerToReceiveAudio: true}), "createOffer({offerToReceiveAudio: true})"],
+ [pc.createOffer({offerToReceiveAudio: false}), "createOffer({offerToReceiveAudio: false})"],
+ [pc.createOffer({offerToReceiveVideo: true}), "createOffer({offerToReceiveVideo: true})"],
+ [pc.createOffer({offerToReceiveVideo: false}), "createOffer({offerToReceiveVideo: false})"],
+ [pc.createAnswer(), "createAnswer"],
+ [pc.setLocalDescription(offer), "setLocalDescription"],
+ [pc.setRemoteDescription(answer), "setRemoteDescription"],
+ [pc.addIceCandidate(candidate), "addIceCandidate"],
+ [new Promise((y, n) => pc.createOffer(y, n)), "Legacy createOffer"],
+ [new Promise((y, n) => pc.createAnswer(y, n)), "Legacy createAnswer"],
+ [new Promise((y, n) => pc.setLocalDescription(offer, y, n)), "Legacy setLocalDescription"],
+ [new Promise((y, n) => pc.setRemoteDescription(answer, y, n)), "Legacy setRemoteDescription"],
+ [new Promise((y, n) => pc.addIceCandidate(candidate, y, n)), "Legacy addIceCandidate"],
+ [sender.replaceTrack(getSilentTrack()), "replaceTrack"],
+ ].map(([p, name]) => doesFail(p, name + " fails on close")))
+ .catch(reason => ok(false, "unexpected failure: " + reason))
+ .then(finish);
+
+ // Other methods are unaffected.
+
+ SimpleTest.doesThrow(function() {
+ pc.updateIce("Invalid RTC Configuration")},
+ "updateIce() on closed PC raised expected exception");
+
+ SimpleTest.doesThrow(function() {
+ pc.addStream("Invalid Media Stream")},
+ "addStream() on closed PC raised expected exception");
+
+ SimpleTest.doesThrow(function() {
+ pc.createDataChannel({})},
+ "createDataChannel() on closed PC raised expected exception");
+
+ SimpleTest.doesThrow(function() {
+ pc.setIdentityProvider("Invalid Provider")},
+ "setIdentityProvider() on closed PC raised expected exception");
+
+ return Promise.all([finished, silence]);
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_closeDuringIce.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_closeDuringIce.html
new file mode 100644
index 0000000000..db3a2922d5
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_closeDuringIce.html
@@ -0,0 +1,79 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1087629",
+ title: "Close PCs during ICE connectivity check"
+ });
+
+// Test closeDuringIce to simulate problems during peer connections
+
+
+function PC_LOCAL_SETUP_NULL_ICE_HANDLER(test) {
+ test.pcLocal.setupIceCandidateHandler(test, function() {}, function () {});
+}
+function PC_REMOTE_SETUP_NULL_ICE_HANDLER(test) {
+ test.pcRemote.setupIceCandidateHandler(test, function() {}, function () {});
+}
+function PC_REMOTE_ADD_FAKE_ICE_CANDIDATE(test) {
+ var cand = {"candidate":"candidate:0 1 UDP 2130379007 192.0.2.1 12345 typ host","sdpMid":"","sdpMLineIndex":0};
+ test.pcRemote.storeOrAddIceCandidate(cand);
+ info(test.pcRemote + " Stored fake candidate: " + JSON.stringify(cand));
+}
+function PC_LOCAL_ADD_FAKE_ICE_CANDIDATE(test) {
+ var cand = {"candidate":"candidate:0 1 UDP 2130379007 192.0.2.2 56789 typ host","sdpMid":"","sdpMLineIndex":0};
+ test.pcLocal.storeOrAddIceCandidate(cand);
+ info(test.pcLocal + " Stored fake candidate: " + JSON.stringify(cand));
+}
+function PC_LOCAL_CLOSE_DURING_ICE(test) {
+ return test.pcLocal.iceChecking.then(() => {
+ test.pcLocal.onsignalingstatechange = function () {};
+ test.pcLocal.close();
+ });
+}
+function PC_REMOTE_CLOSE_DURING_ICE(test) {
+ return test.pcRemote.iceChecking.then(() => {
+ test.pcRemote.onsignalingstatechange = function () {};
+ test.pcRemote.close();
+ });
+}
+function PC_LOCAL_WAIT_FOR_ICE_CHECKING(test) {
+ var resolveIceChecking;
+ test.pcLocal.iceChecking = new Promise(r => resolveIceChecking = r);
+ test.pcLocal.ice_connection_callbacks.checkIceStatus = () => {
+ if (test.pcLocal._pc.iceConnectionState === "checking") {
+ resolveIceChecking();
+ }
+ }
+}
+function PC_REMOTE_WAIT_FOR_ICE_CHECKING(test) {
+ var resolveIceChecking;
+ test.pcRemote.iceChecking = new Promise(r => resolveIceChecking = r);
+ test.pcRemote.ice_connection_callbacks.checkIceStatus = () => {
+ if (test.pcRemote._pc.iceConnectionState === "checking") {
+ resolveIceChecking();
+ }
+ }
+}
+
+runNetworkTest(() => {
+ var test = new PeerConnectionTest();
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ test.chain.replace("PC_LOCAL_SETUP_ICE_HANDLER", PC_LOCAL_SETUP_NULL_ICE_HANDLER);
+ test.chain.replace("PC_REMOTE_SETUP_ICE_HANDLER", PC_REMOTE_SETUP_NULL_ICE_HANDLER);
+ test.chain.insertAfter("PC_REMOTE_SETUP_NULL_ICE_HANDLER", PC_LOCAL_WAIT_FOR_ICE_CHECKING);
+ test.chain.insertAfter("PC_LOCAL_WAIT_FOR_ICE_CHECKING", PC_REMOTE_WAIT_FOR_ICE_CHECKING);
+ test.chain.removeAfter("PC_LOCAL_SET_REMOTE_DESCRIPTION");
+ test.chain.append([PC_REMOTE_ADD_FAKE_ICE_CANDIDATE, PC_LOCAL_ADD_FAKE_ICE_CANDIDATE,
+ PC_LOCAL_CLOSE_DURING_ICE, PC_REMOTE_CLOSE_DURING_ICE]);
+ return test.run();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_codecNegotiationFailure.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_codecNegotiationFailure.html
new file mode 100644
index 0000000000..819e13fe1b
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_codecNegotiationFailure.html
@@ -0,0 +1,111 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="iceTestUtils.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1683934",
+ title: "RTCPeerConnection check codec negotiation failure"
+ });
+
+ function makeWeirdCodecs(sdp) {
+ return sdp
+ .replaceAll('VP8', 'VEEEEEEEEP8')
+ .replaceAll('VP9', 'VEEEEEEEEP9')
+ .replaceAll('H264', 'HERP264');
+ }
+
+ const tests = [
+ async function offererWeirdCodecs() {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+
+ const stream = await navigator.mediaDevices.getUserMedia({video: true});
+ pc1.addTrack(stream.getTracks()[0]);
+ pc2.addTrack(stream.getTracks()[0]);
+
+ const offer = await pc1.createOffer();
+ offer.sdp = makeWeirdCodecs(offer.sdp);
+ // It is not an error to receive an offer with no codecs we support
+ await pc2.setRemoteDescription(offer);
+ await pc2.setLocalDescription();
+ await wait(2000);
+ },
+
+ async function answererWeirdCodecs() {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+
+ const stream = await navigator.mediaDevices.getUserMedia({video: true});
+ pc1.addTrack(stream.getTracks()[0]);
+ pc2.addTrack(stream.getTracks()[0]);
+
+ await pc1.setLocalDescription();
+ await pc2.setRemoteDescription(pc1.localDescription);
+ const answer = await pc2.createAnswer();
+ answer.sdp = makeWeirdCodecs(answer.sdp);
+ try {
+ await pc1.setRemoteDescription(answer);
+ ok(false, "Should have thrown");
+ } catch (e) {
+ ok(true, "Should have thrown");
+ }
+ },
+
+ async function reoffererWeirdCodecs() {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+
+ const stream = await navigator.mediaDevices.getUserMedia({video: true});
+ pc1.addTrack(stream.getTracks()[0]);
+ pc2.addTrack(stream.getTracks()[0]);
+
+ await connect(pc1, pc2, 32000, "Initial connection");
+
+ const offer = await pc1.createOffer();
+ offer.sdp = makeWeirdCodecs(offer.sdp);
+ // It is not an error to receive an offer with no codecs we support
+ await pc2.setRemoteDescription(offer);
+ await pc2.setLocalDescription();
+ await wait(2000);
+ },
+
+ async function reanswererWeirdCodecs() {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+
+ const stream = await navigator.mediaDevices.getUserMedia({video: true});
+ pc1.addTrack(stream.getTracks()[0]);
+ pc2.addTrack(stream.getTracks()[0]);
+
+ await connect(pc1, pc2, 32000, "Initial connection");
+ await pc1.setLocalDescription();
+ await pc2.setRemoteDescription(pc1.localDescription);
+ const answer = await pc2.createAnswer();
+ answer.sdp = makeWeirdCodecs(answer.sdp);
+ try {
+ await pc1.setRemoteDescription(answer);
+ ok(false, "Should have thrown");
+ } catch (e) {
+ ok(true, "Should have thrown");
+ }
+ },
+
+ ];
+
+ runNetworkTest(async () => {
+ for (const test of tests) {
+ info(`Running test: ${test.name}`);
+ await test();
+ info(`Done running test: ${test.name}`);
+ }
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_constructedStream.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_constructedStream.html
new file mode 100644
index 0000000000..8431b7534e
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_constructedStream.html
@@ -0,0 +1,67 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+ bug: "1271669",
+ title: "Test that pc.addTrack() accepts any MediaStream",
+ visible: true
+});
+
+runNetworkTest(() => {
+ var test = new PeerConnectionTest();
+ var constructedStream;
+ var dummyStream = new MediaStream();
+ var dummyStreamTracks = [];
+
+ test.setMediaConstraints([ {audio: true, video: true}
+ , {audio: true}
+ , {video: true}
+ ], []);
+ test.chain.replace("PC_LOCAL_GUM", [
+ function PC_LOCAL_GUM_CONSTRUCTED_STREAM(test) {
+ return getUserMedia(test.pcLocal.constraints[0]).then(stream => {
+ constructedStream = new MediaStream(stream.getTracks());
+ test.pcLocal.attachLocalStream(constructedStream);
+ });
+ },
+ function PC_LOCAL_GUM_DUMMY_STREAM(test) {
+ return getUserMedia(test.pcLocal.constraints[1])
+ .then(stream => dummyStreamTracks.push(...stream.getTracks()))
+ .then(() => getUserMedia(test.pcLocal.constraints[2]))
+ .then(stream => dummyStreamTracks.push(...stream.getTracks()))
+ .then(() => dummyStreamTracks.forEach(t =>
+ test.pcLocal.attachLocalTrack(t, dummyStream)));
+ },
+ ]);
+
+ let checkSentTracksReceived = (sentStreamId, sentTracks) => {
+ let receivedStream =
+ test.pcRemote._pc.getRemoteStreams().find(s => s.id == sentStreamId);
+ ok(receivedStream, "We should receive a stream with with the sent stream's id (" + sentStreamId + ")");
+ if (!receivedStream) {
+ return;
+ }
+
+ is(receivedStream.getTracks().length, sentTracks.length,
+ "Should receive same number of tracks as were sent");
+ };
+
+ test.chain.append([
+ function PC_REMOTE_CHECK_RECEIVED_CONSTRUCTED_STREAM() {
+ checkSentTracksReceived(constructedStream.id, constructedStream.getTracks());
+ },
+ function PC_REMOTE_CHECK_RECEIVED_DUMMY_STREAM() {
+ checkSentTracksReceived(dummyStream.id, dummyStreamTracks);
+ },
+ ]);
+ return test.run();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_disabledVideoPreNegotiation.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_disabledVideoPreNegotiation.html
new file mode 100644
index 0000000000..4c06de792e
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_disabledVideoPreNegotiation.html
@@ -0,0 +1,45 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1570673",
+ title: "Sending an initially disabled video track should be playable remotely",
+ visible: true,
+ });
+
+ var test;
+ runNetworkTest(async (options) => {
+ // [TODO] re-enable HW decoder after bug 1526207 is fixed.
+ if (navigator.userAgent.includes("Android")) {
+ await pushPrefs(["media.navigator.mediadatadecoder_vpx_enabled", false],
+ ["media.webrtc.hw.h264.enabled", false]);
+ }
+
+ test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{video: true}], []);
+ test.chain.insertAfter("PC_LOCAL_GUM", function PC_LOCAL_DISABLE_VIDEO() {
+ for (const {track} of test.pcLocal._pc.getSenders()) {
+ if (track.kind == "video") {
+ track.enabled = false;
+ }
+ }
+ });
+ test.chain.append(async function PC_REMOTE_RECEIVING_BLACK() {
+ const v = test.pcRemote.remoteMediaElements[0];
+ is(v.readyState, v.HAVE_ENOUGH_DATA, "video element should be playing");
+ const h = new CaptureStreamTestHelper2D();
+ await h.waitForPixel(test.pcRemote.remoteMediaElements[0],
+ px => h.isPixel(px, h.black, 128));
+ });
+ await test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_encodingsNegotiation.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_encodingsNegotiation.html
new file mode 100644
index 0000000000..f46d7eb0d2
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_encodingsNegotiation.html
@@ -0,0 +1,85 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="simulcast.js"></script>
+ <script type="application/javascript" src="helpers_from_wpt/sdp.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+ bug: "1401592",
+ title: "Simulcast negotiation tests",
+ visible: true
+});
+
+// simulcast negotiation is mostly tested in wpt, but we test a few
+// implementation-specific things here.
+const tests = [
+ async function checkVideoEncodingLimit() {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+
+ const stream = await navigator.mediaDevices.getUserMedia({video: true});
+ const sender = pc1.addTrack(stream.getTracks()[0]);
+ pc2.addTrack(stream.getTracks()[0]);
+
+ await doOfferToRecvSimulcast(pc2, pc1, ["1", "2", "3", "4"]);
+
+ const {encodings} = sender.getParameters();
+ const rids = encodings.map(({rid}) => rid);
+ isDeeply(rids, ["1", "2", "3"]);
+
+ pc1.close();
+ pc2.close();
+ stream.getTracks().forEach(track => track.stop());
+ },
+
+ // wpt currently does not assume support for 3 encodings, which limits the
+ // effectiveness of its powers-of-2 test (since it can test only for 1 and 2)
+ async function checkScaleResolutionDownByAutoFillPowersOf2() {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ const stream = await navigator.mediaDevices.getUserMedia({video: true});
+ const sender = pc1.addTrack(stream.getTracks()[0]);
+ pc2.addTrack(stream.getTracks()[0]);
+
+ await doOfferToRecvSimulcast(pc2, pc1, ["1", "2", "3"]);
+
+ const {encodings} = sender.getParameters();
+ const scaleValues = encodings.map(({scaleResolutionDownBy}) => scaleResolutionDownBy);
+ isDeeply(scaleValues, [4, 2, 1]);
+ },
+
+ async function checkLibwebrtcRidLengthLimit() {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+
+ const stream = await navigator.mediaDevices.getUserMedia({video: true});
+ const sender = pc1.addTrack(stream.getTracks()[0]);
+ pc2.addTrack(stream.getTracks()[0]);
+
+ await doOfferToRecvSimulcast(pc2, pc1, ["foo", "wibblywobblyjeremybearimy"]);
+ const {encodings} = sender.getParameters();
+ const rids = encodings.map(({rid}) => rid);
+ isDeeply(rids, ["foo"]);
+
+ pc1.close();
+ pc2.close();
+ stream.getTracks().forEach(track => track.stop());
+ },
+];
+
+runNetworkTest(async () => {
+ for (const test of tests) {
+ info(`Running test: ${test.name}`);
+ await test();
+ info(`Done running test: ${test.name}`);
+ }
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_errorCallbacks.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_errorCallbacks.html
new file mode 100644
index 0000000000..851a256509
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_errorCallbacks.html
@@ -0,0 +1,55 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "834270",
+ title: "Align PeerConnection error handling with WebRTC specification"
+ });
+
+ function validateReason(reason) {
+ ok(reason.name.length, "Reason name = " + reason.name);
+ ok(reason.message.length, "Reason message = " + reason.message);
+ };
+
+ function testCreateAnswerError() {
+ var pc = new RTCPeerConnection();
+ info ("Testing createAnswer error");
+ return pc.createAnswer()
+ .then(generateErrorCallback("createAnswer before offer should fail"),
+ validateReason);
+ };
+
+ function testSetLocalDescriptionError() {
+ var pc = new RTCPeerConnection();
+ info ("Testing setLocalDescription error");
+ return pc.setLocalDescription({ sdp: "Picklechips!", type: "offer" })
+ .then(generateErrorCallback("setLocalDescription with nonsense SDP should fail"),
+ validateReason);
+ };
+
+ function testSetRemoteDescriptionError() {
+ var pc = new RTCPeerConnection();
+ info ("Testing setRemoteDescription error");
+ return pc.setRemoteDescription({ sdp: "Who?", type: "offer" })
+ .then(generateErrorCallback("setRemoteDescription with nonsense SDP should fail"),
+ validateReason);
+ };
+
+ // No test for createOffer errors -- there's nothing we can do at this
+ // level to evoke an error in createOffer.
+
+ runNetworkTest(function () {
+ return testCreateAnswerError()
+ .then(testSetLocalDescriptionError)
+ .then(testSetRemoteDescriptionError);
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_extmapRenegotiation.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_extmapRenegotiation.html
new file mode 100644
index 0000000000..78c6bb986c
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_extmapRenegotiation.html
@@ -0,0 +1,325 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="iceTestUtils.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1799932",
+ title: "RTCPeerConnection check renegotiation of extmap"
+ });
+
+ function setExtmap(sdp, uri, id) {
+ const regex = new RegExp(`a=extmap:[0-9]+(\/[a-z]+)? ${uri}`, 'g');
+ if (id) {
+ return sdp.replaceAll(regex, `a=extmap:${id}$1 ${uri}`);
+ } else {
+ return sdp.replaceAll(regex, `a=unknownattr`);
+ }
+ }
+
+ function getExtmap(sdp, uri) {
+ const regex = new RegExp(`a=extmap:([0-9]+)(\/[a-z]+)? ${uri}`);
+ return sdp.match(regex)[1];
+ }
+
+ function replaceExtUri(sdp, oldUri, newUri) {
+ const regex = new RegExp(`(a=extmap:[0-9]+\/[a-z]+)? ${oldUri}`, 'g');
+ return sdp.replaceAll(regex, `$1 ${newUri}`);
+ }
+
+ const tests = [
+ async function checkAudioMidChange() {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+
+ const stream = await navigator.mediaDevices.getUserMedia({audio: true});
+ pc1.addTrack(stream.getTracks()[0]);
+ pc2.addTrack(stream.getTracks()[0]);
+
+ await connect(pc1, pc2, 32000, "Initial connection");
+
+ // Sadly, there's no way to tell the offerer to change the extmap. Other
+ // types of endpoint could conceivably do this, so we at least don't want
+ // to crash.
+ // TODO: Would be nice to be able to test this with an endpoint that
+ // actually changes the ids it uses.
+ const reoffer = await pc1.createOffer();
+ reoffer.sdp = setExtmap(reoffer.sdp, "urn:ietf:params:rtp-hdrext:sdes:mid", 14);
+ info(`New reoffer: ${reoffer.sdp}`);
+ await pc2.setRemoteDescription(reoffer);
+ await pc2.setLocalDescription();
+ await wait(2000);
+ },
+
+ async function checkVideoMidChange() {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+
+ const stream = await navigator.mediaDevices.getUserMedia({video: true});
+ pc1.addTrack(stream.getTracks()[0]);
+ pc2.addTrack(stream.getTracks()[0]);
+
+ await connect(pc1, pc2, 32000, "Initial connection");
+
+ // Sadly, there's no way to tell the offerer to change the extmap. Other
+ // types of endpoint could conceivably do this, so we at least don't want
+ // to crash.
+ // TODO: Would be nice to be able to test this with an endpoint that
+ // actually changes the ids it uses.
+ const reoffer = await pc1.createOffer();
+ reoffer.sdp = setExtmap(reoffer.sdp, "urn:ietf:params:rtp-hdrext:sdes:mid", 14);
+ info(`New reoffer: ${reoffer.sdp}`);
+ await pc2.setRemoteDescription(reoffer);
+ await pc2.setLocalDescription();
+ await wait(2000);
+ },
+
+ async function checkAudioMidSwap() {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+
+ const stream = await navigator.mediaDevices.getUserMedia({audio: true});
+ pc1.addTrack(stream.getTracks()[0]);
+ pc2.addTrack(stream.getTracks()[0]);
+
+ await connect(pc1, pc2, 32000, "Initial connection");
+
+ // Sadly, there's no way to tell the offerer to change the extmap. Other
+ // types of endpoint could conceivably do this, so we at least don't want
+ // to crash.
+ // TODO: Would be nice to be able to test this with an endpoint that
+ // actually changes the ids it uses.
+ const reoffer = await pc1.createOffer();
+ const midId = getExtmap(reoffer.sdp, "urn:ietf:params:rtp-hdrext:sdes:mid");
+ const ssrcLevelId = getExtmap(reoffer.sdp, "urn:ietf:params:rtp-hdrext:ssrc-audio-level");
+ reoffer.sdp = setExtmap(reoffer.sdp, "urn:ietf:params:rtp-hdrext:sdes:mid", ssrcLevelId);
+ reoffer.sdp = setExtmap(reoffer.sdp, "urn:ietf:params:rtp-hdrext:ssrc-audio-level", midId);
+ info(`New reoffer: ${reoffer.sdp}`);
+ try {
+ await pc2.setRemoteDescription(reoffer);
+ ok(false, "sRD should fail when it attempts extension id remapping");
+ } catch (e) {
+ ok(true, "sRD should fail when it attempts extension id remapping");
+ }
+ },
+
+ async function checkVideoMidSwap() {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+
+ const stream = await navigator.mediaDevices.getUserMedia({video: true});
+ pc1.addTrack(stream.getTracks()[0]);
+ pc2.addTrack(stream.getTracks()[0]);
+
+ await connect(pc1, pc2, 32000, "Initial connection");
+
+ // Sadly, there's no way to tell the offerer to change the extmap. Other
+ // types of endpoint could conceivably do this, so we at least don't want
+ // to crash.
+ // TODO: Would be nice to be able to test this with an endpoint that
+ // actually changes the ids it uses.
+ const reoffer = await pc1.createOffer();
+ const midId = getExtmap(reoffer.sdp, "urn:ietf:params:rtp-hdrext:sdes:mid");
+ const toffsetId = getExtmap(reoffer.sdp, "urn:ietf:params:rtp-hdrext:toffset");
+ reoffer.sdp = setExtmap(reoffer.sdp, "urn:ietf:params:rtp-hdrext:sdes:mid", toffsetId);
+ reoffer.sdp = setExtmap(reoffer.sdp, "urn:ietf:params:rtp-hdrext:toffset", midId);
+ info(`New reoffer: ${reoffer.sdp}`);
+ try {
+ await pc2.setRemoteDescription(reoffer);
+ ok(false, "sRD should fail when it attempts extension id remapping");
+ } catch (e) {
+ ok(true, "sRD should fail when it attempts extension id remapping");
+ }
+ },
+
+ async function checkAudioIdReuse() {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+
+ const stream = await navigator.mediaDevices.getUserMedia({audio: true});
+ pc1.addTrack(stream.getTracks()[0]);
+ pc2.addTrack(stream.getTracks()[0]);
+
+ await connect(pc1, pc2, 32000, "Initial connection");
+
+ // Sadly, there's no way to tell the offerer to change the extmap. Other
+ // types of endpoint could conceivably do this, so we at least don't want
+ // to crash.
+ // TODO: Would be nice to be able to test this with an endpoint that
+ // actually changes the ids it uses.
+ const reoffer = await pc1.createOffer();
+ // Change uri, but not the id, so the id now refers to foo.
+ reoffer.sdp = replaceExtUri(reoffer.sdp, "urn:ietf:params:rtp-hdrext:ssrc-audio-level", "foo");
+ info(`New reoffer: ${reoffer.sdp}`);
+ try {
+ await pc2.setRemoteDescription(reoffer);
+ ok(false, "sRD should fail when it attempts extension id remapping");
+ } catch (e) {
+ ok(true, "sRD should fail when it attempts extension id remapping");
+ }
+ },
+
+ async function checkVideoIdReuse() {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+
+ const stream = await navigator.mediaDevices.getUserMedia({video: true});
+ pc1.addTrack(stream.getTracks()[0]);
+ pc2.addTrack(stream.getTracks()[0]);
+
+ await connect(pc1, pc2, 32000, "Initial connection");
+
+ // Sadly, there's no way to tell the offerer to change the extmap. Other
+ // types of endpoint could conceivably do this, so we at least don't want
+ // to crash.
+ // TODO: Would be nice to be able to test this with an endpoint that
+ // actually changes the ids it uses.
+ const reoffer = await pc1.createOffer();
+ // Change uri, but not the id, so the id now refers to foo.
+ reoffer.sdp = replaceExtUri(reoffer.sdp, "urn:ietf:params:rtp-hdrext:toffset", "foo");
+ info(`New reoffer: ${reoffer.sdp}`);
+ try {
+ await pc2.setRemoteDescription(reoffer);
+ ok(false, "sRD should fail when it attempts extension id remapping");
+ } catch (e) {
+ ok(true, "sRD should fail when it attempts extension id remapping");
+ }
+ },
+
+ // What happens when remote answer uses an extmap id, and then a remote
+ // reoffer tries to use the same id for something else?
+ async function checkAudioIdReuseOffererThenAnswerer() {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+
+ const stream = await navigator.mediaDevices.getUserMedia({audio: true});
+ pc1.addTrack(stream.getTracks()[0]);
+ pc2.addTrack(stream.getTracks()[0]);
+
+ await connect(pc1, pc2, 32000, "Initial connection");
+
+ const reoffer = await pc2.createOffer();
+ // Change uri, but not the id, so the id now refers to foo.
+ reoffer.sdp = replaceExtUri(reoffer.sdp, "urn:ietf:params:rtp-hdrext:ssrc-audio-level", "foo");
+ info(`New reoffer: ${reoffer.sdp}`);
+ try {
+ await pc1.setRemoteDescription(reoffer);
+ ok(false, "sRD should fail when it attempts extension id remapping");
+ } catch (e) {
+ ok(true, "sRD should fail when it attempts extension id remapping");
+ }
+ },
+
+ // What happens when a remote offer uses a different extmap id than the
+ // default? Does the answerer remember the new id in reoffers?
+ async function checkAudioIdReuseOffererThenAnswerer() {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+
+ const stream = await navigator.mediaDevices.getUserMedia({audio: true});
+ pc1.addTrack(stream.getTracks()[0]);
+ pc2.addTrack(stream.getTracks()[0]);
+
+ // Negotiate, but change id for ssrc-audio-level to something pc2 would
+ // not typically use.
+ await pc1.setLocalDescription();
+ const mungedOffer = setExtmap(pc1.localDescription.sdp, "urn:ietf:params:rtp-hdrext:ssrc-audio-level", 12);
+ await pc2.setRemoteDescription({sdp: mungedOffer, type: "offer"});
+ await pc2.setLocalDescription();
+
+ const reoffer = await pc2.createOffer();
+ is(getExtmap(reoffer.sdp, "urn:ietf:params:rtp-hdrext:ssrc-audio-level"), "12");
+ },
+
+ async function checkAudioUnnegotiatedIdReuse1() {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+
+ const stream = await navigator.mediaDevices.getUserMedia({audio: true});
+ pc1.addTrack(stream.getTracks()[0]);
+ pc2.addTrack(stream.getTracks()[0]);
+
+ // Negotiate, but remove ssrc-audio-level from answer
+ await pc1.setLocalDescription();
+ const levelId = getExtmap(pc1.localDescription.sdp, "urn:ietf:params:rtp-hdrext:ssrc-audio-level");
+ await pc2.setRemoteDescription(pc1.localDescription);
+ await pc2.setLocalDescription();
+ const answerNoExt = setExtmap(pc2.localDescription.sdp, "urn:ietf:params:rtp-hdrext:ssrc-audio-level", undefined);
+ await pc1.setRemoteDescription({sdp: answerNoExt, type: "answer"});
+
+ // Renegotiate, and use the id that offerer used for ssrc-audio-level for
+ // something different (while making sure we don't use it twice)
+ await pc2.setLocalDescription();
+ const mungedReoffer = setExtmap(pc2.localDescription.sdp, "urn:ietf:params:rtp-hdrext:sdes:mid", levelId);
+ const twiceMungedReoffer = setExtmap(mungedReoffer, "urn:ietf:params:rtp-hdrext:ssrc-audio-level", undefined);
+ await pc1.setRemoteDescription({sdp: twiceMungedReoffer, type: "offer"});
+ },
+
+ async function checkAudioUnnegotiatedIdReuse2() {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+
+ const stream = await navigator.mediaDevices.getUserMedia({audio: true});
+ pc1.addTrack(stream.getTracks()[0]);
+ pc2.addTrack(stream.getTracks()[0]);
+
+ // Negotiate, but remove ssrc-audio-level from offer. pc2 has never seen
+ // |levelId| in extmap yet, but internally probably wants to use that for
+ // ssrc-audio-level
+ await pc1.setLocalDescription();
+ const levelId = getExtmap(pc1.localDescription.sdp, "urn:ietf:params:rtp-hdrext:ssrc-audio-level");
+ const offerNoExt = setExtmap(pc1.localDescription.sdp, "urn:ietf:params:rtp-hdrext:ssrc-audio-level", undefined);
+ await pc2.setRemoteDescription({sdp: offerNoExt, type: "offer"});
+ await pc2.setLocalDescription();
+ await pc1.setRemoteDescription(pc2.localDescription);
+
+ // Renegotiate, but use |levelId| for something other than
+ // ssrc-audio-level. pc2 should not throw.
+ await pc1.setLocalDescription();
+ const mungedReoffer = setExtmap(pc1.localDescription.sdp, "urn:ietf:params:rtp-hdrext:sdes:mid", levelId);
+ const twiceMungedReoffer = setExtmap(mungedReoffer, "urn:ietf:params:rtp-hdrext:ssrc-audio-level", undefined);
+ await pc2.setRemoteDescription({sdp: twiceMungedReoffer, type: "offer"});
+ },
+
+ async function checkAudioUnnegotiatedIdReuse3() {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+
+ const stream = await navigator.mediaDevices.getUserMedia({audio: true});
+ pc1.addTrack(stream.getTracks()[0]);
+ pc2.addTrack(stream.getTracks()[0]);
+
+ // Negotiate, but replace ssrc-audio-level with something pc2 won't
+ // support in offer.
+ await pc1.setLocalDescription();
+ const levelId = getExtmap(pc1.localDescription.sdp, "urn:ietf:params:rtp-hdrext:ssrc-audio-level");
+ const mungedOffer = replaceExtUri(pc1.localDescription.sdp, "urn:ietf:params:rtp-hdrext:ssrc-audio-level", "fooba");
+ await pc2.setRemoteDescription({sdp: mungedOffer, type: "offer"});
+ await pc2.setLocalDescription();
+ await pc1.setRemoteDescription(pc2.localDescription);
+
+ // Renegotiate, and use levelId for something pc2 _will_ support.
+ await pc1.setLocalDescription();
+ const mungedReoffer = setExtmap(pc1.localDescription.sdp, "urn:ietf:params:rtp-hdrext:sdes:mid", levelId);
+ const twiceMungedReoffer = setExtmap(mungedReoffer, "urn:ietf:params:rtp-hdrext:ssrc-audio-level", undefined);
+ await pc2.setRemoteDescription({sdp: twiceMungedReoffer, type: "offer"});
+ },
+
+ ];
+
+ runNetworkTest(async () => {
+ for (const test of tests) {
+ info(`Running test: ${test.name}`);
+ await test();
+ info(`Done running test: ${test.name}`);
+ }
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_forwarding_basicAudioVideoCombined.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_forwarding_basicAudioVideoCombined.html
new file mode 100644
index 0000000000..84b53a123b
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_forwarding_basicAudioVideoCombined.html
@@ -0,0 +1,41 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "931903",
+ title: "Forwarding a stream from a combined audio/video peerconnection to another"
+ });
+
+runNetworkTest(function() {
+ var gumTest = new PeerConnectionTest();
+
+ var forwardingOptions = { config_local: { label_suffix: "forwarded" },
+ config_remote: { label_suffix: "forwarded" } };
+ var forwardingTest = new PeerConnectionTest(forwardingOptions);
+
+ gumTest.setMediaConstraints([{audio: true, video: true}], []);
+ forwardingTest.setMediaConstraints([{audio: true, video: true}], []);
+ forwardingTest.chain.replace("PC_LOCAL_GUM", [
+ function PC_FORWARDING_CAPTUREVIDEO(test) {
+ var streams = gumTest.pcRemote._pc.getRemoteStreams();
+ is(streams.length, 1, "One stream to forward");
+ is(streams[0].getTracks().length, 2, "Forwarded stream has 2 tracks");
+ forwardingTest.pcLocal.attachLocalStream(streams[0]);
+ return Promise.resolve();
+ }
+ ]);
+ gumTest.chain.removeAfter("PC_REMOTE_WAIT_FOR_MEDIA_FLOW");
+ return gumTest.chain.execute()
+ .then(() => forwardingTest.chain.execute())
+ .then(() => gumTest.close())
+ .then(() => forwardingTest.close());
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_gatherWithSetConfiguration.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_gatherWithSetConfiguration.html
new file mode 100644
index 0000000000..6710e628aa
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_gatherWithSetConfiguration.html
@@ -0,0 +1,450 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="iceTestUtils.js"></script>
+ <script type="application/javascript" src="helpers_from_wpt/sdp.js"></script></head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1253706",
+ title: "Test ICE gathering when setConfiguration is used to change the ICE config"
+ });
+
+ const tests = [
+ async function baselineV4Cases() {
+ await checkSrflx([{urls:[`stun:${turnAddressV4}`]}]);
+ await checkRelayUdp([{urls:[`turn:${turnAddressV4}`], username, credential}]);
+ await checkRelayTcp([{urls:[`turn:${turnAddressV4}?transport=tcp`], username, credential}]);
+ await checkRelayUdpTcp([{urls:[`turn:${turnAddressV4}`, `turn:${turnAddressV4}?transport=tcp`], username, credential}]);
+ await checkNoSrflx();
+ await checkNoRelay();
+ },
+
+ async function addStunServerBeforeOffer() {
+ const pc = new RTCPeerConnection();
+ try {
+ pc.setConfiguration({iceServers: [{urls:[`stun:${turnAddressV4}`]}]});
+ const candidates = await gatherWithTimeout(pc, 32000, `just a stun server`);
+ ok(candidates.some(c => c.candidate.includes("srflx")), "Should get at least one srflx candidate");
+ ok(!candidates.some(c => c.candidate.includes("relay")), "Should not get any relay candidates");
+ } finally {
+ pc.close();
+ }
+ },
+
+ async function addTurnServerBeforeOffer() {
+ const pc = new RTCPeerConnection();
+ try {
+ pc.setConfiguration({iceServers: [{urls:[`turn:${turnAddressV4}`], username, credential}]});
+ const candidates = await gatherWithTimeout(pc, 32000, `a turn (udp) server`);
+ ok(candidates.some(c => c.candidate.includes("srflx")), "Should get at least one srflx candidate");
+ ok(candidates.some(c => c.candidate.includes("relay")), "Should get at least one relay candidate");
+ } finally {
+ pc.close();
+ }
+ },
+
+ async function addTurnTcpServerBeforeOffer() {
+ const pc = new RTCPeerConnection();
+ try {
+ pc.setConfiguration({iceServers: [{urls:[`turn:${turnAddressV4}?transport=tcp`], username, credential}]});
+ const candidates = await gatherWithTimeout(pc, 32000, `a turn (tcp) server`);
+ ok(!candidates.some(c => c.candidate.includes("srflx")), "Should not get any srflx candidates");
+ ok(candidates.some(c => c.candidate.includes("relay")), "Should get at least one relay candidate");
+ } finally {
+ pc.close();
+ }
+ },
+
+ async function addStunServerAfterOffer() {
+ const pc = new RTCPeerConnection();
+ try {
+ const candidates1 = await gatherWithTimeout(pc, 32000, `no ICE servers`);
+ ok(!candidates1.some(c => c.candidate.includes("srflx")), "Should not get any srflx candidates");
+ ok(!candidates1.some(c => c.candidate.includes("relay")), "Should not get any relay candidates");
+ await pc.setLocalDescription({type: "rollback"});
+
+ pc.setConfiguration({iceServers: [{urls:[`stun:${turnAddressV4}`]}]});
+ const candidates2 = await gatherWithTimeout(pc, 32000, `just a stun server`);
+ ok(candidates2.some(c => c.candidate.includes("srflx")), "Should get at least one srflx candidate");
+ ok(!candidates2.some(c => c.candidate.includes("relay")), "Should not get any relay candidates");
+ } finally {
+ pc.close();
+ }
+ },
+
+ async function addTurnServerAfterOffer() {
+ const pc = new RTCPeerConnection();
+ try {
+ const candidates1 = await gatherWithTimeout(pc, 32000, `no ICE servers`);
+ ok(!candidates1.some(c => c.candidate.includes("srflx")), "Should not get any srflx candidates");
+ ok(!candidates1.some(c => c.candidate.includes("relay")), "Should not get any relay candidates");
+ await pc.setLocalDescription({type: "rollback"});
+
+ pc.setConfiguration({iceServers: [{urls:[`turn:${turnAddressV4}`], username, credential}]});
+ const candidates2 = await gatherWithTimeout(pc, 32000, `a turn (udp) server`);
+ ok(candidates2.some(c => c.candidate.includes("srflx")), "Should get at least one srflx candidate");
+ ok(candidates2.some(c => c.candidate.includes("relay")), "Should get at least one relay candidate");
+ } finally {
+ pc.close();
+ }
+ },
+
+ async function addTurnTcpServerAfterOffer() {
+ const pc = new RTCPeerConnection();
+ try {
+ const candidates1 = await gatherWithTimeout(pc, 32000, `no ICE servers`);
+ ok(!candidates1.some(c => c.candidate.includes("srflx")), "Should not get any srflx candidates");
+ ok(!candidates1.some(c => c.candidate.includes("relay")), "Should not get any relay candidates");
+ await pc.setLocalDescription({type: "rollback"});
+
+ pc.setConfiguration({iceServers: [{urls:[`turn:${turnAddressV4}?transport=tcp`], username, credential}]});
+ const candidates2 = await gatherWithTimeout(pc, 32000, `a turn (tcp) server`);
+ ok(!candidates2.some(c => c.candidate.includes("srflx")), "Should get no srflx candidates");
+ ok(candidates2.some(c => c.candidate.includes("relay")), "Should get at least one relay candidate");
+ } finally {
+ pc.close();
+ }
+ },
+
+ async function removeStunServerBeforeOffer() {
+ const pc = new RTCPeerConnection({iceServers: [{urls:[`stun:${turnAddressV4}`]}]});
+ try {
+ pc.setConfiguration({});
+ const candidates = await gatherWithTimeout(pc, 32000, `no ICE servers`);
+ ok(!candidates.some(c => c.candidate.includes("srflx")), "Should not get any srflx candidates");
+ ok(!candidates.some(c => c.candidate.includes("relay")), "Should not get any relay candidates");
+ } finally {
+ pc.close();
+ }
+ },
+
+ async function removeTurnServerBeforeOffer() {
+ const pc = new RTCPeerConnection({iceServers: [{urls:[`turn:${turnAddressV4}`], username, credential}]});
+ try {
+ pc.setConfiguration({});
+ const candidates = await gatherWithTimeout(pc, 32000, `no ICE servers`);
+ ok(!candidates.some(c => c.candidate.includes("srflx")), "Should not get any srflx candidates");
+ ok(!candidates.some(c => c.candidate.includes("relay")), "Should not get any relay candidates");
+ } finally {
+ pc.close();
+ }
+ },
+
+ async function removeTurnTcpServerBeforeOffer() {
+ const pc = new RTCPeerConnection({iceServers: [{urls:[`turn:${turnAddressV4}?transport=tcp`], username, credential}]});
+ try {
+ pc.setConfiguration({});
+ const candidates = await gatherWithTimeout(pc, 32000, `no ICE servers`);
+ ok(!candidates.some(c => c.candidate.includes("srflx")), "Should not get any srflx candidates");
+ ok(!candidates.some(c => c.candidate.includes("relay")), "Should not get any relay candidates");
+ } finally {
+ pc.close();
+ }
+ },
+
+ async function removeStunServerAfterOffer() {
+ const pc = new RTCPeerConnection({iceServers: [{urls:[`stun:${turnAddressV4}`]}]});
+ try {
+ const candidates1 = await gatherWithTimeout(pc, 32000, `just a stun server`);
+ ok(candidates1.some(c => c.candidate.includes("srflx")), "Should get at least one srflx candidate");
+ ok(!candidates1.some(c => c.candidate.includes("relay")), "Should not get any relay candidates");
+ await pc.setLocalDescription({type: "rollback"});
+
+ pc.setConfiguration({});
+ const candidates2 = await gatherWithTimeout(pc, 32000, `no ICE servers`);
+ ok(!candidates2.some(c => c.candidate.includes("srflx")), "Should not get any srflx candidates");
+ ok(!candidates2.some(c => c.candidate.includes("relay")), "Should not get any relay candidates");
+ } finally {
+ pc.close();
+ }
+ },
+
+ async function removeTurnServerAfterOffer() {
+ const pc = new RTCPeerConnection({iceServers: [{urls:[`turn:${turnAddressV4}`], username, credential}]});
+ try {
+ const candidates1 = await gatherWithTimeout(pc, 32000, `a turn (udp) server`);
+ ok(candidates1.some(c => c.candidate.includes("srflx")), "Should get at least one srflx candidate");
+ ok(candidates1.some(c => c.candidate.includes("relay")), "Should get at least one relay candidate");
+ await pc.setLocalDescription({type: "rollback"});
+
+ pc.setConfiguration({});
+ const candidates2 = await gatherWithTimeout(pc, 32000, `no ICE servers`);
+ ok(!candidates2.some(c => c.candidate.includes("srflx")), "Should not get any srflx candidates");
+ ok(!candidates2.some(c => c.candidate.includes("relay")), "Should not get any relay candidates");
+ } finally {
+ pc.close();
+ }
+ },
+
+ async function removeTurnTcpServerAfterOffer() {
+ const pc = new RTCPeerConnection({iceServers: [{urls:[`turn:${turnAddressV4}?transport=tcp`], username, credential}]});
+ try {
+ const candidates1 = await gatherWithTimeout(pc, 32000, `a turn (tcp) server`);
+ ok(!candidates1.some(c => c.candidate.includes("srflx")), "Should get no srflx candidates");
+ ok(candidates1.some(c => c.candidate.includes("relay")), "Should get at least one relay candidate");
+ await pc.setLocalDescription({type: "rollback"});
+
+ pc.setConfiguration({});
+ const candidates2 = await gatherWithTimeout(pc, 32000, `no ICE servers`);
+ ok(!candidates2.some(c => c.candidate.includes("srflx")), "Should not get any srflx candidates");
+ ok(!candidates2.some(c => c.candidate.includes("relay")), "Should not get any relay candidates");
+ } finally {
+ pc.close();
+ }
+ },
+
+ async function addStunServerAfterNegotiation() {
+ const offerer = new RTCPeerConnection();
+ const answerer = new RTCPeerConnection({iceServers: [{urls:[`stun:${turnAddressV4}`]}]});
+ try {
+ const candidatePromise = trickleIce(offerer);
+ await connect(offerer, answerer, 32000, `no ICE servers`);
+ const candidates = await candidatePromise;
+ const ufrags = Array.from(new Set(candidates.map(c => c.usernameFragment)));
+ is(ufrags.length, 1, "Should have one ufrag in candidate set");
+
+ offerer.setConfiguration({iceServers: [{urls:[`stun:${turnAddressV4}`]}]});
+ const candidates2 = await gatherWithTimeout(offerer, 32000, `just a stun server`);
+ ok(candidates2.some(c => c.candidate.includes("srflx")), "Should get at least one srflx candidate");
+ ok(!candidates2.some(c => c.candidate.includes("relay")), "Should not get any relay candidates");
+ const ufrags2 = Array.from(new Set(candidates2.map(c => c.usernameFragment)));
+ is(ufrags2.length, 1, "Should have one ufrag in candidate set");
+ isnot(ufrags[0], ufrags2[0], "ufrag should change, because setConfiguration should have triggered an ICE restart");
+ } finally {
+ offerer.close();
+ answerer.close();
+ }
+ },
+
+ async function addTurnServerAfterNegotiation() {
+ const offerer = new RTCPeerConnection();
+ const answerer = new RTCPeerConnection({iceServers: [{urls:[`stun:${turnAddressV4}`]}]});
+ try {
+ const candidatePromise = trickleIce(offerer);
+ await connect(offerer, answerer, 32000, `no ICE servers`);
+ const candidates = await candidatePromise;
+ const ufrags = Array.from(new Set(candidates.map(c => c.usernameFragment)));
+ is(ufrags.length, 1, "Should have one ufrag in candidate set");
+
+ offerer.setConfiguration({iceServers: [{urls:[`turn:${turnAddressV4}`], username, credential}]});
+ const candidates2 = await gatherWithTimeout(offerer, 32000, `a turn (udp) server`);
+ ok(candidates2.some(c => c.candidate.includes("srflx")), "Should get at least one srflx candidate");
+ ok(candidates2.some(c => c.candidate.includes("relay")), "Should get at least one relay candidate");
+ const ufrags2 = Array.from(new Set(candidates2.map(c => c.usernameFragment)));
+ is(ufrags2.length, 1, "Should have one ufrag in candidate set");
+ isnot(ufrags[0], ufrags2[0], "ufrag should change, because setConfiguration should have triggered an ICE restart");
+ } finally {
+ offerer.close();
+ answerer.close();
+ }
+ },
+
+ async function addTurnTcpServerAfterNegotiation() {
+ const offerer = new RTCPeerConnection();
+ const answerer = new RTCPeerConnection({iceServers: [{urls:[`stun:${turnAddressV4}`]}]});
+ try {
+ const candidatePromise = trickleIce(offerer);
+ await connect(offerer, answerer, 32000, `no ICE servers`);
+ const candidates = await candidatePromise;
+ const ufrags = Array.from(new Set(candidates.map(c => c.usernameFragment)));
+ is(ufrags.length, 1, "Should have one ufrag in candidate set");
+
+ offerer.setConfiguration({iceServers: [{urls:[`turn:${turnAddressV4}?transport=tcp`], username, credential}]});
+ const candidates2 = await gatherWithTimeout(offerer, 32000, `a turn (tcp) server`);
+ ok(!candidates2.some(c => c.candidate.includes("srflx")), "Should not get any srflx candidates");
+ ok(candidates2.some(c => c.candidate.includes("relay")), "Should get at least one relay candidate");
+ const ufrags2 = Array.from(new Set(candidates2.map(c => c.usernameFragment)));
+ is(ufrags2.length, 1, "Should have one ufrag in candidate set");
+ isnot(ufrags[0], ufrags2[0], "ufrag should change, because setConfiguration should have triggered an ICE restart");
+ } finally {
+ offerer.close();
+ answerer.close();
+ }
+ },
+
+ async function addStunServerBeforeCreateAnswer() {
+ const offerer = new RTCPeerConnection();
+ const answerer = new RTCPeerConnection();
+
+ try {
+ await answerer.setRemoteDescription(await offerer.createOffer({offerToReceiveAudio: true}));
+
+ answerer.setConfiguration({iceServers: [{urls:[`stun:${turnAddressV4}`]}]});
+ const candidates = await gatherWithTimeout(answerer, 32000, `just a stun server`);
+ ok(candidates.some(c => c.candidate.includes("srflx")), "Should get at least one srflx candidate");
+ ok(!candidates.some(c => c.candidate.includes("relay")), "Should not get any relay candidates");
+ } finally {
+ offerer.close();
+ answerer.close();
+ }
+ },
+
+ async function addTurnServerBeforeCreateAnswer() {
+ const offerer = new RTCPeerConnection();
+ const answerer = new RTCPeerConnection();
+
+ try {
+ await answerer.setRemoteDescription(await offerer.createOffer({offerToReceiveAudio: true}));
+
+ answerer.setConfiguration({iceServers: [{urls:[`turn:${turnAddressV4}`], username, credential}]});
+ const candidates = await gatherWithTimeout(answerer, 32000, `a turn (udp) server`);
+ ok(candidates.some(c => c.candidate.includes("srflx")), "Should get at least one srflx candidate");
+ ok(candidates.some(c => c.candidate.includes("relay")), "Should get at least one relay candidate");
+ } finally {
+ offerer.close();
+ answerer.close();
+ }
+ },
+
+ async function addTurnTcpServerBeforeCreateAnswer() {
+ const offerer = new RTCPeerConnection();
+ const answerer = new RTCPeerConnection();
+
+ try {
+ await answerer.setRemoteDescription(await offerer.createOffer({offerToReceiveAudio: true}));
+
+ answerer.setConfiguration({iceServers: [{urls:[`turn:${turnAddressV4}?transport=tcp`], username, credential}]});
+ const candidates = await gatherWithTimeout(answerer, 32000, `a turn (tcp) server`);
+ ok(!candidates.some(c => c.candidate.includes("srflx")), "Should not get any srflx candidates");
+ ok(candidates.some(c => c.candidate.includes("relay")), "Should get at least one relay candidate");
+ } finally {
+ offerer.close();
+ answerer.close();
+ }
+ },
+
+ async function relayPolicyPreventsSrflx() {
+ const pc = new RTCPeerConnection();
+ try {
+ pc.setConfiguration({iceServers: [{urls:[`turn:${turnAddressV4}`], username, credential}], iceTransportPolicy: "relay"});
+ const candidates = await gatherWithTimeout(pc, 32000, `a turn (udp) server`);
+ ok(!candidates.some(c => c.candidate.includes("srflx")), "Should not get a srflx candidate");
+ ok(candidates.some(c => c.candidate.includes("relay")), "Should get at least one relay candidate");
+ } finally {
+ pc.close();
+ }
+ },
+
+ async function addOffererStunServerAllowsIceToConnect() {
+ const offerer = new RTCPeerConnection();
+ const answerer = new RTCPeerConnection();
+
+ try {
+ try {
+ // Both ends are behind a simulated endpoint-independent NAT, which
+ // requires at least one side to have a srflx candidate to work.
+ await connect(offerer, answerer, 2000, `no ICE servers`);
+ ok(false, "ICE should either have failed, or timed out!");
+ } catch (e) {
+ if (!(e instanceof Error)) throw e;
+ ok(true, "ICE should either have failed, or timed out!");
+ }
+
+ offerer.setConfiguration({iceServers: [{urls:[`stun:${turnAddressV4}`]}]});
+ await connect(offerer, answerer, 32000, `just a STUN server`);
+ } finally {
+ offerer.close();
+ answerer.close();
+ }
+ },
+
+ async function addAnswererStunServerDoesNotAllowIceToConnect() {
+ const offerer = new RTCPeerConnection();
+ const answerer = new RTCPeerConnection();
+
+ try {
+ try {
+ // Both ends are behind a simulated endpoint-independent NAT, which
+ // requires at least one side to have a srflx candidate to work.
+ await connect(offerer, answerer, 2000, `no ICE servers`);
+ ok(false, "ICE should either have failed, or timed out!");
+ } catch (e) {
+ if (!(e instanceof Error)) throw e;
+ ok(true, "ICE should either have failed, or timed out!");
+ }
+
+ // This _won't_ help, because the answerer does not get to decide to
+ // trigger an ICE restart.
+ answerer.setConfiguration({iceServers: [{urls:[`stun:${turnAddressV4}`]}]});
+ try {
+ await connectNoTrickleWait(offerer, answerer, 2000, `no ICE servers`);
+ ok(false, "ICE should either have failed, or timed out!");
+ } catch (e) {
+ if (!(e instanceof Error)) throw e;
+ ok(true, "ICE should either have failed, or timed out!");
+ }
+ } finally {
+ offerer.close();
+ answerer.close();
+ }
+ },
+
+ async function addOffererTurnServerAllowsIceToConnect() {
+ await pushPrefs(
+ ['media.peerconnection.nat_simulator.filtering_type', 'PORT_DEPENDENT'],
+ ['media.peerconnection.nat_simulator.mapping_type', 'PORT_DEPENDENT']);
+
+ const offerer = new RTCPeerConnection({iceServers: [{urls:[`stun:${turnAddressV4}`]}]});
+ const answerer = new RTCPeerConnection({iceServers: [{urls:[`stun:${turnAddressV4}`]}]});
+
+ try {
+ try {
+ // Both ends are behind a simulated port-dependent NAT, which
+ // requires at least one side to have a relay candidate to work.
+ await connect(offerer, answerer, 2000, `just a STUN server`);
+ ok(false, "ICE should either have failed, or timed out!");
+ } catch (e) {
+ if (!(e instanceof Error)) throw e;
+ ok(true, "ICE should either have failed, or timed out!");
+ }
+
+ offerer.setConfiguration({iceServers: [{urls:[`turn:${turnAddressV4}`], username, credential}]});
+ await connect(offerer, answerer, 32000, `a TURN (udp) server`);
+ } finally {
+ offerer.close();
+ answerer.close();
+ await SpecialPowers.popPrefEnv();
+ }
+ },
+
+ ];
+
+ runNetworkTest(async () => {
+ const turnServer = iceServersArray.find(server => "username" in server);
+ username = turnServer.username;
+ credential = turnServer.credential;
+ // Just use the first url. It might make sense to look for TURNS first,
+ // since that will always use a hostname, but on CI we don't have TURNS
+ // support anyway (see bug 1323439).
+ const turnHostname = getTurnHostname(turnServer.urls[0]);
+ turnAddressV4 = await dnsLookupV4(turnHostname);
+
+ await pushPrefs(
+ ['media.peerconnection.ice.obfuscate_host_addresses', false],
+ ['media.peerconnection.nat_simulator.filtering_type', 'ENDPOINT_INDEPENDENT'],
+ ['media.peerconnection.nat_simulator.mapping_type', 'ENDPOINT_INDEPENDENT'],
+ ['media.peerconnection.ice.loopback', true],
+ ['media.getusermedia.insecure.enabled', true]);
+
+ for (const test of tests) {
+ info(`Running test: ${test.name}`);
+ try {
+ await test();
+ } catch (e) {
+ ok(false, `Caught ${e.name}: ${e.message} ${e.stack}`);
+ }
+ info(`Done running test: ${test.name}`);
+ // Make sure we don't build up a pile of GC work, and also get PCImpl to
+ // print their timecards.
+ await new Promise(r => SpecialPowers.exactGC(r));
+ }
+
+ await SpecialPowers.popPrefEnv();
+ }, { useIceServer: true });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_gatherWithStun300.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_gatherWithStun300.html
new file mode 100644
index 0000000000..50bc4a6553
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_gatherWithStun300.html
@@ -0,0 +1,269 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="iceTestUtils.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "857668",
+ title: "RTCPeerConnection check STUN gathering with STUN/300 responses"
+ });
+
+ /* This is pretty hairy, so some background:
+ * Spec is here: https://datatracker.ietf.org/doc/html/rfc8489#section-10
+ * STUN/300 responses allow a server to redirect STUN requests to one or
+ more other servers, as ALTERNATE-SERVER attributes.
+ * The server specifies the IP address, IP version, and port for each
+ ALTERNATE-SERVER.
+ * The spec allows multiple rounds of redirects, and requires the client to
+ remember the servers it has already tried to avoid redirect loops.
+ * For TURNS, the TURN server can also supply an ALTERNATE-DOMAIN attribute,
+ which the client MUST use for the TLS handshake on the new target. The
+ client does _not_ use this as an FQDN; it always uses the address in the
+ ALTERNATE-SERVER. ALTERNATE-DOMAIN is meaningless in the non-TLS case.
+ * STUN/300 with ALTERNATE-SERVER is only defined for the TURN Allocate
+ message type (at least in the context of ICE). Clients are supposed to
+ treat STUN/300 as an unrecoverable error in all other cases. The TURN spec
+ does _not_ spell out how a client should handle multiple ALTERNATE-SERVERs.
+ We just take the first one that we have not already tried, and that is the
+ same IP version that we started with. This is because switching the IP
+ version is problematic for ICE.
+ * The test TURN server opens extra ports that will respond with redirects to
+ the _real_ ports, but the address remains the same. This is because we
+ cannot know ahead of time whether the machine we're running on has more
+ than one IP address of each version. This means the test TURN server is not
+ useful for testing cases where the address changes. Also, the test TURN
+ server does not currently know how to respond with multiple
+ ALTERNATE-SERVERs.
+ * To test cases where the _address_ changes, we instead use a feature in the
+ NAT simulator to respond with fake redirects when the destination address
+ matches an address that we configure with a pref. This feature can add
+ multiple ALTERNATE-SERVERs.
+ * The test TURN server's STUN/300 responses have a proper MESSAGE-INTEGRITY,
+ but the NAT simulator's do _not_. For now, we want both cases to work,
+ because some servers respond with STUN/300 without including
+ MESSAGE-INTEGRITY. This is a spec violation, even though the spec
+ contradicts itself in non-normative language elsewhere.
+ * Right now, neither the NAT simulator nor the test TURN server support
+ ALTERNATE-DOMAIN.
+ */
+
+ // These are the ports the test TURN server will respond with redirects on.
+ // The test TURN server tells us what these are in the JSON it spits out when
+ // we start it.
+ let turnRedirectPort;
+ let turnsRedirectPort;
+
+ // These are the addresses that we will configure the NAT simulator to
+ // redirect to. We do DNS lookups of the host in iceServersArray (provided
+ // by the test TURN server), and put the results here. On some platforms this
+ // will be 127.0.0.1 and ::1, but on others we may use a real address.
+ let redirectTargetV4;
+
+ // Test TURN server tells us these in the JSON it spits out when we start it
+ let username;
+ let credential;
+
+ // This is the address we will configure the NAT simulator to respond with
+ // redirects for. We use an address from TEST-NET since it is really unlikely
+ // we'll see that on a real machine, and also because we do not have
+ // special-case code in nICEr for TEST-NET (like we do for link-local, for
+ // example).
+ const redirectAddressV4 = '198.51.100.1';
+
+ const tests = [
+ async function baselineV4Cases() {
+ await checkSrflx([{urls:[`stun:${redirectTargetV4}`]}]);
+ await checkRelayUdp([{urls:[`turn:${redirectTargetV4}`], username, credential}]);
+ await checkRelayTcp([{urls:[`turn:${redirectTargetV4}?transport=tcp`], username, credential}]);
+ await checkRelayUdpTcp([{urls:[`turn:${redirectTargetV4}`, `turn:${redirectTargetV4}?transport=tcp`], username, credential}]);
+ },
+
+ async function stunV4Redirect() {
+ // This test uses the test TURN server, because nICEr drops responses
+ // without MESSAGE-INTEGRITY on the floor _unless_ they are a STUN/300 to
+ // an Allocate request. If we tried to use the NAT simulator for this, we
+ // would have to wait for nICEr to time out, since the NAT simulator does
+ // not know how to do MESSAGE-INTEGRITY.
+ await checkNoSrflx(
+ [{urls:[`stun:${redirectTargetV4}:${turnRedirectPort}`]}]);
+ },
+
+ async function turnV4UdpPortRedirect() {
+ await checkRelayUdp([{urls:[`turn:${redirectTargetV4}:${turnRedirectPort}`], username, credential}]);
+ },
+
+ async function turnV4TcpPortRedirect() {
+ await checkRelayTcp([{urls:[`turn:${redirectTargetV4}:${turnRedirectPort}?transport=tcp`], username, credential}]);
+ },
+
+ async function turnV4UdpTcpPortRedirect() {
+ await checkRelayUdpTcp([{urls:[`turn:${redirectTargetV4}:${turnRedirectPort}`, `turn:${redirectTargetV4}:${turnRedirectPort}?transport=tcp`], username, credential}]);
+ },
+
+ async function turnV4UdpAddressRedirect() {
+ await pushPrefs(
+ ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV4}`],
+ ['media.peerconnection.nat_simulator.redirect_targets', `${redirectTargetV4}`]);
+ await checkRelayUdp([{urls:[`turn:${redirectAddressV4}`], username, credential}]);
+ await SpecialPowers.popPrefEnv();
+ },
+
+ async function turnV4TcpAddressRedirect() {
+ await pushPrefs(
+ ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV4}`],
+ ['media.peerconnection.nat_simulator.redirect_targets', `${redirectTargetV4}`]);
+ await checkRelayTcp([{urls:[`turn:${redirectAddressV4}?transport=tcp`], username, credential}]);
+ await SpecialPowers.popPrefEnv();
+ },
+
+ async function turnV4UdpTcpAddressRedirect() {
+ await pushPrefs(
+ ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV4}`],
+ ['media.peerconnection.nat_simulator.redirect_targets', `${redirectTargetV4}`]);
+ await checkRelayUdpTcp([{urls:[`turn:${redirectAddressV4}`, `turn:${redirectAddressV4}?transport=tcp`], username, credential}]);
+ await SpecialPowers.popPrefEnv();
+ },
+
+ async function turnV4UdpEmptyRedirect() {
+ await pushPrefs(
+ ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV4}`],
+ ['media.peerconnection.nat_simulator.redirect_targets', '']);
+ await checkNoRelay([{urls:[`turn:${redirectAddressV4}`], username, credential}]);
+ await SpecialPowers.popPrefEnv();
+ },
+
+ async function turnV4TcpEmptyRedirect() {
+ await pushPrefs(
+ ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV4}`],
+ ['media.peerconnection.nat_simulator.redirect_targets', '']);
+ await checkNoRelay([{urls:[`turn:${redirectAddressV4}?transport=tcp`], username, credential}]);
+ await SpecialPowers.popPrefEnv();
+ },
+
+ async function turnV4UdpTcpEmptyRedirect() {
+ await pushPrefs(
+ ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV4}`],
+ ['media.peerconnection.nat_simulator.redirect_targets', '']);
+ await checkNoRelay([{urls:[`turn:${redirectAddressV4}`, `turn:${redirectAddressV4}?transport=tcp`], username, credential}]);
+ await SpecialPowers.popPrefEnv();
+ },
+
+ async function turnV4UdpAddressAndPortRedirect() {
+ // This should result in two rounds of redirection; the first is by
+ // address, the second is by port.
+ await pushPrefs(
+ ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV4}`],
+ ['media.peerconnection.nat_simulator.redirect_targets', `${redirectTargetV4}`]);
+ await checkRelayUdp([{urls:[`turn:${redirectAddressV4}:${turnRedirectPort}`], username, credential}]);
+ await SpecialPowers.popPrefEnv();
+ },
+
+ async function turnV4TcpAddressAndPortRedirect() {
+ // This should result in two rounds of redirection; the first is by
+ // address, the second is by port.
+ await pushPrefs(
+ ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV4}`],
+ ['media.peerconnection.nat_simulator.redirect_targets', `${redirectTargetV4}`]);
+ await checkRelayTcp([{urls:[`turn:${redirectAddressV4}:${turnRedirectPort}?transport=tcp`], username, credential}]);
+ await SpecialPowers.popPrefEnv();
+ },
+
+ async function turnV4UdpTcpAddressAndPortRedirect() {
+ // This should result in two rounds of redirection; the first is by
+ // address, the second is by port.
+ await pushPrefs(
+ ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV4}`],
+ ['media.peerconnection.nat_simulator.redirect_targets', `${redirectTargetV4}`]);
+ await checkRelayUdpTcp([{urls:[`turn:${redirectAddressV4}:${turnRedirectPort}`, `turn:${redirectAddressV4}:${turnRedirectPort}?transport=tcp`], username, credential}]);
+ await SpecialPowers.popPrefEnv();
+ },
+
+ async function turnV4UdpRedirectLoop() {
+ await pushPrefs(
+ ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV4}`],
+ ['media.peerconnection.nat_simulator.redirect_targets', `${redirectAddressV4}`]);
+ // If we don't detect the loop, gathering will not finish
+ await checkNoRelay([{urls:[`turn:${redirectAddressV4}`], username, credential}]);
+ await SpecialPowers.popPrefEnv();
+ },
+
+ async function turnV4TcpRedirectLoop() {
+ await pushPrefs(
+ ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV4}`],
+ ['media.peerconnection.nat_simulator.redirect_targets', `${redirectAddressV4}`]);
+ // If we don't detect the loop, gathering will not finish
+ await checkNoRelay([{urls:[`turn:${redirectAddressV4}?transport=tcp`], username, credential}]);
+ await SpecialPowers.popPrefEnv();
+ },
+
+ async function turnV4UdpTcpRedirectLoop() {
+ await pushPrefs(
+ ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV4}`],
+ ['media.peerconnection.nat_simulator.redirect_targets', `${redirectAddressV4}`]);
+ // If we don't detect the loop, gathering will not finish
+ await checkNoRelay([{urls:[`turn:${redirectAddressV4}`, `turn:${redirectAddressV4}?transport=tcp`], username, credential}]);
+ await SpecialPowers.popPrefEnv();
+ },
+
+ async function turnV4UdpMultipleAddressRedirect() {
+ await pushPrefs(
+ ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV4}`],
+ ['media.peerconnection.nat_simulator.redirect_targets', `${redirectAddressV4},${redirectTargetV4}`]);
+ await checkRelayUdp([{urls:[`turn:${redirectAddressV4}`], username, credential}]);
+ await SpecialPowers.popPrefEnv();
+ },
+
+ async function turnV4TcpMultipleAddressRedirect() {
+ await pushPrefs(
+ ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV4}`],
+ ['media.peerconnection.nat_simulator.redirect_targets', `${redirectAddressV4},${redirectTargetV4}`]);
+ await checkRelayTcp([{urls:[`turn:${redirectAddressV4}?transport=tcp`], username, credential}]);
+ await SpecialPowers.popPrefEnv();
+ },
+
+ async function turnV4UdpTcpMultipleAddressRedirect() {
+ await pushPrefs(
+ ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV4}`],
+ ['media.peerconnection.nat_simulator.redirect_targets', `${redirectAddressV4},${redirectTargetV4}`]);
+ await checkRelayUdpTcp([{urls:[`turn:${redirectAddressV4}`, `turn:${redirectAddressV4}?transport=tcp`], username, credential}]);
+ await SpecialPowers.popPrefEnv();
+ },
+ ];
+
+ runNetworkTest(async () => {
+ const turnServer = iceServersArray.find(server => "username" in server);
+ username = turnServer.username;
+ credential = turnServer.credential;
+ // Special props, non-standard
+ turnRedirectPort = turnServer.turn_redirect_port;
+ turnsRedirectPort = turnServer.turns_redirect_port;
+ // Just use the first url. It might make sense to look for TURNS first,
+ // since that will always use a hostname, but on CI we don't have TURNS
+ // support anyway (see bug 1323439).
+ const turnHostname = getTurnHostname(turnServer.urls[0]);
+ redirectTargetV4 = await dnsLookupV4(turnHostname);
+
+ await pushPrefs(
+ ['media.peerconnection.ice.obfuscate_host_addresses', false],
+ ['media.peerconnection.nat_simulator.filtering_type', 'ENDPOINT_INDEPENDENT'],
+ ['media.peerconnection.nat_simulator.mapping_type', 'ENDPOINT_INDEPENDENT'],
+ ['media.peerconnection.ice.loopback', true],
+ ['media.getusermedia.insecure.enabled', true]);
+
+ for (const test of tests) {
+ info(`Running test: ${test.name}`);
+ await test();
+ info(`Done running test: ${test.name}`);
+ }
+
+ await SpecialPowers.popPrefEnv();
+ }, { useIceServer: true });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_gatherWithStun300IPv6.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_gatherWithStun300IPv6.html
new file mode 100644
index 0000000000..16f8f39978
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_gatherWithStun300IPv6.html
@@ -0,0 +1,283 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="iceTestUtils.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "857668",
+ title: "RTCPeerConnection check STUN gathering with STUN/300 responses (IPv6)"
+ });
+
+ /* This is pretty hairy, so some background:
+ * Spec is here: https://datatracker.ietf.org/doc/html/rfc8489#section-10
+ * STUN/300 responses allow a server to redirect STUN requests to one or
+ more other servers, as ALTERNATE-SERVER attributes.
+ * The server specifies the IP address, IP version, and port for each
+ ALTERNATE-SERVER.
+ * The spec allows multiple rounds of redirects, and requires the client to
+ remember the servers it has already tried to avoid redirect loops.
+ * For TURNS, the TURN server can also supply an ALTERNATE-DOMAIN attribute,
+ which the client MUST use for the TLS handshake on the new target. The
+ client does _not_ use this as an FQDN; it always uses the address in the
+ ALTERNATE-SERVER. ALTERNATE-DOMAIN is meaningless in the non-TLS case.
+ * STUN/300 with ALTERNATE-SERVER is only defined for the TURN Allocate
+ message type (at least in the context of ICE). Clients are supposed to
+ treat STUN/300 as an unrecoverable error in all other cases. The TURN spec
+ does _not_ spell out how a client should handle multiple ALTERNATE-SERVERs.
+ We just take the first one that we have not already tried, and that is the
+ same IP version that we started with. This is because switching the IP
+ version is problematic for ICE.
+ * The test TURN server opens extra ports that will respond with redirects to
+ the _real_ ports, but the address remains the same. This is because we
+ cannot know ahead of time whether the machine we're running on has more
+ than one IP address of each version. This means the test TURN server is not
+ useful for testing cases where the address changes. Also, the test TURN
+ server does not currently know how to respond with multiple
+ ALTERNATE-SERVERs.
+ * To test cases where the _address_ changes, we instead use a feature in the
+ NAT simulator to respond with fake redirects when the destination address
+ matches an address that we configure with a pref. This feature can add
+ multiple ALTERNATE-SERVERs.
+ * The test TURN server's STUN/300 responses have a proper MESSAGE-INTEGRITY,
+ but the NAT simulator's do _not_. For now, we want both cases to work,
+ because some servers respond with STUN/300 without including
+ MESSAGE-INTEGRITY. This is a spec violation, even though the spec
+ contradicts itself in non-normative language elsewhere.
+ * Right now, neither the NAT simulator nor the test TURN server support
+ ALTERNATE-DOMAIN.
+ */
+
+ // These are the ports the test TURN server will respond with redirects on.
+ // The test TURN server tells us what these are in the JSON it spits out when
+ // we start it.
+ let turnRedirectPort;
+ let turnsRedirectPort;
+
+ // These are the addresses that we will configure the NAT simulator to
+ // redirect to. We do DNS lookups of the host in iceServersArray (provided
+ // by the test TURN server), and put the results here. On some platforms this
+ // will be 127.0.0.1 and ::1, but on others we may use a real address.
+ let redirectTargetV6;
+
+ // Test TURN server tells us these in the JSON it spits out when we start it
+ let username;
+ let credential;
+
+ // This is the address we will configure the NAT simulator to respond with
+ // redirects for. We use an address from TEST-NET since it is really unlikely
+ // we'll see that on a real machine, and also because we do not have
+ // special-case code in nICEr for TEST-NET (like we do for link-local, for
+ // example).
+ const redirectAddressV6 = '::ffff:198.51.100.1';
+
+ const tests = [
+ async function baselineV6Cases() {
+ await checkSrflx([{urls:[`stun:[${redirectTargetV6}]`]}]);
+ await checkRelayUdp([{urls:[`turn:[${redirectTargetV6}]`], username, credential}]);
+ await checkRelayTcp([{urls:[`turn:[${redirectTargetV6}]?transport=tcp`], username, credential}]);
+ await checkRelayUdpTcp([{urls:[`turn:[${redirectTargetV6}]`, `turn:[${redirectTargetV6}]?transport=tcp`], username, credential}]);
+ },
+
+ async function stunV6Redirect() {
+ // This test uses the test TURN server, because nICEr drops responses
+ // without MESSAGE-INTEGRITY on the floor _unless_ they are a STUN/300 to
+ // an Allocate request. If we tried to use the NAT simulator for this, we
+ // would have to wait for nICEr to time out, since the NAT simulator does
+ // not know how to do MESSAGE-INTEGRITY.
+ await checkNoSrflx(
+ [{urls:[`stun:[${redirectTargetV6}]:${turnRedirectPort}`]}]);
+ },
+
+ async function turnV6UdpPortRedirect() {
+ await checkRelayUdp([{urls:[`turn:[${redirectTargetV6}]:${turnRedirectPort}`], username, credential}]);
+ },
+
+ async function turnV6TcpPortRedirect() {
+ await checkRelayTcp([{urls:[`turn:[${redirectTargetV6}]:${turnRedirectPort}?transport=tcp`], username, credential}]);
+ },
+
+ async function turnV6UdpTcpPortRedirect() {
+ await checkRelayUdpTcp([{urls:[`turn:[${redirectTargetV6}]:${turnRedirectPort}`, `turn:[${redirectTargetV6}]:${turnRedirectPort}?transport=tcp`], username, credential}]);
+ },
+
+ async function turnV6UdpAddressRedirect() {
+ await pushPrefs(
+ ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV6}`],
+ ['media.peerconnection.nat_simulator.redirect_targets', `${redirectTargetV6}`]);
+ await checkRelayUdp([{urls:[`turn:[${redirectAddressV6}]`], username, credential}]);
+ await SpecialPowers.popPrefEnv();
+ },
+
+ async function turnV6TcpAddressRedirect() {
+ await pushPrefs(
+ ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV6}`],
+ ['media.peerconnection.nat_simulator.redirect_targets', `${redirectTargetV6}`]);
+ await checkRelayTcp([{urls:[`turn:[${redirectAddressV6}]?transport=tcp`], username, credential}]);
+ await SpecialPowers.popPrefEnv();
+ },
+
+ async function turnV6UdpTcpAddressRedirect() {
+ await pushPrefs(
+ ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV6}`],
+ ['media.peerconnection.nat_simulator.redirect_targets', `${redirectTargetV6}`]);
+ await checkRelayUdpTcp([{urls:[`turn:[${redirectAddressV6}]`, `turn:[${redirectAddressV6}]?transport=tcp`], username, credential}]);
+ await SpecialPowers.popPrefEnv();
+ },
+
+ async function turnV6UdpEmptyRedirect() {
+ await pushPrefs(
+ ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV6}`],
+ ['media.peerconnection.nat_simulator.redirect_targets', '']);
+ await checkNoRelay([{urls:[`turn:[${redirectAddressV6}]`], username, credential}]);
+ await SpecialPowers.popPrefEnv();
+ },
+
+ async function turnV6TcpEmptyRedirect() {
+ await pushPrefs(
+ ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV6}`],
+ ['media.peerconnection.nat_simulator.redirect_targets', '']);
+ await checkNoRelay([{urls:[`turn:[${redirectAddressV6}]?transport=tcp`], username, credential}]);
+ await SpecialPowers.popPrefEnv();
+ },
+
+ async function turnV6UdpTcpEmptyRedirect() {
+ await pushPrefs(
+ ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV6}`],
+ ['media.peerconnection.nat_simulator.redirect_targets', '']);
+ await checkNoRelay([{urls:[`turn:[${redirectAddressV6}]`, `turn:[${redirectAddressV6}]?transport=tcp`], username, credential}]);
+ await SpecialPowers.popPrefEnv();
+ },
+
+ async function turnV6UdpAddressAndPortRedirect() {
+ // This should result in two rounds of redirection; the first is by
+ // address, the second is by port.
+ await pushPrefs(
+ ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV6}`],
+ ['media.peerconnection.nat_simulator.redirect_targets', `${redirectTargetV6}`]);
+ await checkRelayUdp([{urls:[`turn:[${redirectAddressV6}]:${turnRedirectPort}`], username, credential}]);
+ await SpecialPowers.popPrefEnv();
+ },
+
+ async function turnV6TcpAddressAndPortRedirect() {
+ // This should result in two rounds of redirection; the first is by
+ // address, the second is by port.
+ await pushPrefs(
+ ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV6}`],
+ ['media.peerconnection.nat_simulator.redirect_targets', `${redirectTargetV6}`]);
+ await checkRelayTcp([{urls:[`turn:[${redirectAddressV6}]:${turnRedirectPort}?transport=tcp`], username, credential}]);
+ await SpecialPowers.popPrefEnv();
+ },
+
+ async function turnV6UdpTcpAddressAndPortRedirect() {
+ // This should result in two rounds of redirection; the first is by
+ // address, the second is by port.
+ await pushPrefs(
+ ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV6}`],
+ ['media.peerconnection.nat_simulator.redirect_targets', `${redirectTargetV6}`]);
+ await checkRelayUdpTcp([{urls:[`turn:[${redirectAddressV6}]:${turnRedirectPort}`, `turn:[${redirectAddressV6}]:${turnRedirectPort}?transport=tcp`], username, credential}]);
+ await SpecialPowers.popPrefEnv();
+ },
+
+ async function turnV6UdpRedirectLoop() {
+ await pushPrefs(
+ ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV6}`],
+ ['media.peerconnection.nat_simulator.redirect_targets', `${redirectAddressV6}`]);
+ // If we don't detect the loop, gathering will not finish
+ await checkNoRelay([{urls:[`turn:[${redirectAddressV6}]`], username, credential}]);
+ await SpecialPowers.popPrefEnv();
+ },
+
+ async function turnV6TcpRedirectLoop() {
+ await pushPrefs(
+ ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV6}`],
+ ['media.peerconnection.nat_simulator.redirect_targets', `${redirectAddressV6}`]);
+ // If we don't detect the loop, gathering will not finish
+ await checkNoRelay([{urls:[`turn:[${redirectAddressV6}]?transport=tcp`], username, credential}]);
+ await SpecialPowers.popPrefEnv();
+ },
+
+ async function turnV6UdpTcpRedirectLoop() {
+ await pushPrefs(
+ ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV6}`],
+ ['media.peerconnection.nat_simulator.redirect_targets', `${redirectAddressV6}`]);
+ // If we don't detect the loop, gathering will not finish
+ await checkNoRelay([{urls:[`turn:[${redirectAddressV6}]`, `turn:[${redirectAddressV6}]?transport=tcp`], username, credential}]);
+ await SpecialPowers.popPrefEnv();
+ },
+
+ async function turnV6UdpMultipleAddressRedirect() {
+ await pushPrefs(
+ ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV6}`],
+ ['media.peerconnection.nat_simulator.redirect_targets', `${redirectAddressV6},${redirectTargetV6}`]);
+ await checkRelayUdp([{urls:[`turn:[${redirectAddressV6}]`], username, credential}]);
+ await SpecialPowers.popPrefEnv();
+ },
+
+ async function turnV6TcpMultipleAddressRedirect() {
+ await pushPrefs(
+ ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV6}`],
+ ['media.peerconnection.nat_simulator.redirect_targets', `${redirectAddressV6},${redirectTargetV6}`]);
+ await checkRelayTcp([{urls:[`turn:[${redirectAddressV6}]?transport=tcp`], username, credential}]);
+ await SpecialPowers.popPrefEnv();
+ },
+
+ async function turnV6UdpTcpMultipleAddressRedirect() {
+ await pushPrefs(
+ ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV6}`],
+ ['media.peerconnection.nat_simulator.redirect_targets', `${redirectAddressV6},${redirectTargetV6}`]);
+ await checkRelayUdpTcp([{urls:[`turn:[${redirectAddressV6}]`, `turn:[${redirectAddressV6}]?transport=tcp`], username, credential}]);
+ await SpecialPowers.popPrefEnv();
+ },
+ ];
+
+ runNetworkTest(async () => {
+ const turnServer = iceServersArray.find(server => "username" in server);
+ username = turnServer.username;
+ credential = turnServer.credential;
+ // Special props, non-standard
+ turnRedirectPort = turnServer.turn_redirect_port;
+ turnsRedirectPort = turnServer.turns_redirect_port;
+ // Just use the first url. It might make sense to look for TURNS first,
+ // since that will always use a hostname, but on CI we don't have TURNS
+ // support anyway (see bug 1323439).
+ const turnHostname = getTurnHostname(turnServer.urls[0]);
+
+ await pushPrefs(
+ ['media.peerconnection.ice.obfuscate_host_addresses', false],
+ ['media.peerconnection.nat_simulator.filtering_type', 'ENDPOINT_INDEPENDENT'],
+ ['media.peerconnection.nat_simulator.mapping_type', 'ENDPOINT_INDEPENDENT'],
+ ['media.peerconnection.ice.loopback', true],
+ ['media.getusermedia.insecure.enabled', true]);
+
+ if (await ipv6Supported()) {
+ redirectTargetV6 = await dnsLookupV6(turnHostname);
+ if (redirectTargetV6 == '' && turnHostname == 'localhost') {
+ // Our testers don't seem to have IPv6 DNS resolution for localhost
+ // set up...
+ redirectTargetV6 = '::1';
+ }
+
+ if (redirectTargetV6 != '') {
+ for (const test of tests) {
+ info(`Running test: ${test.name}`);
+ await test();
+ info(`Done running test: ${test.name}`);
+ }
+ } else {
+ ok(false, `This machine has an IPv6 address, but ${turnHostname} does not resolve to an IPv6 address`);
+ }
+ } else {
+ ok(false, 'This machine appears to not have an IPv6 address');
+ }
+
+ await SpecialPowers.popPrefEnv();
+ }, { useIceServer: true });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_glean.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_glean.html
new file mode 100644
index 0000000000..d5d6026ee5
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_glean.html
@@ -0,0 +1,488 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+ bug: "1401592",
+ title: "Test that glean is recording stats as expected",
+ visible: true
+});
+
+const { AppConstants } = SpecialPowers.ChromeUtils.import(
+ "resource://gre/modules/AppConstants.jsm"
+);
+
+async function getAllWarningRates() {
+ return {
+ warnNoGetparameters:
+ await GleanTest.rtcrtpsenderSetparameters.warnNoGetparameters.testGetValue(),
+ warnLengthChanged:
+ await GleanTest.rtcrtpsenderSetparameters.warnLengthChanged.testGetValue(),
+ warnRidChanged:
+ await GleanTest.rtcrtpsenderSetparameters.warnRidChanged.testGetValue(),
+ warnNoTransactionid:
+ await GleanTest.rtcrtpsenderSetparameters.warnNoTransactionid.testGetValue(),
+ warnStaleTransactionid:
+ await GleanTest.rtcrtpsenderSetparameters.warnStaleTransactionid.testGetValue(),
+ };
+}
+
+const tests = [
+ async function checkRTCRtpSenderCount() {
+ const pc = new RTCPeerConnection();
+ const oldCount = await GleanTest.rtcrtpsender.count.testGetValue() ?? 0;
+ const {sender} = pc.addTransceiver('video', {
+ sendEncodings: [{rid: "0"},{rid: "1"},{rid: "2"}]
+ });
+ const countDiff = await GleanTest.rtcrtpsender.count.testGetValue() - oldCount;
+ is(countDiff, 1, "Glean should have recorded the creation of a single RTCRtpSender");
+ },
+
+ async function checkRTCRtpSenderSetParametersCompatCount() {
+ await pushPrefs(
+ ["media.peerconnection.allow_old_setParameters", true]);
+ const pc = new RTCPeerConnection();
+ const oldCount = await GleanTest.rtcrtpsender.countSetparametersCompat.testGetValue() ?? 0;
+ const {sender} = pc.addTransceiver('video', {
+ sendEncodings: [{rid: "0"},{rid: "1"},{rid: "2"}]
+ });
+ const countDiff = await GleanTest.rtcrtpsender.countSetparametersCompat.testGetValue() - oldCount;
+ is(countDiff, 1, "Glean should have recorded the creation of a single RTCRtpSender that uses the setParameters compat mode");
+ },
+
+ async function checkSendEncodings() {
+ const pc = new RTCPeerConnection();
+ const oldRate = await GleanTest.rtcrtpsender.usedSendencodings.testGetValue();
+ const {sender} = pc.addTransceiver('video', {
+ sendEncodings: [{rid: "0"},{rid: "1"},{rid: "2"}]
+ });
+ const newRate = await GleanTest.rtcrtpsender.usedSendencodings.testGetValue();
+ is(newRate.denominator, oldRate.denominator + 1, "Glean should have recorded the creation of a single RTCRtpSender");
+ is(newRate.numerator, oldRate.numerator + 1, "Glean should have recorded the use of sendEncodings");
+ },
+
+ async function checkAddTransceiverNoSendEncodings() {
+ const pc = new RTCPeerConnection();
+ const oldRate = await GleanTest.rtcrtpsender.usedSendencodings.testGetValue();
+ const {sender} = pc.addTransceiver('video');
+ const newRate = await GleanTest.rtcrtpsender.usedSendencodings.testGetValue();
+ is(newRate.denominator, oldRate.denominator + 1, "Glean should have recorded the creation of a single RTCRtpSender");
+ is(newRate.numerator, oldRate.numerator, "Glean should not have recorded a use of sendEncodings");
+ },
+
+ async function checkAddTrack() {
+ const pc = new RTCPeerConnection();
+ const oldRate = await GleanTest.rtcrtpsender.usedSendencodings.testGetValue();
+ const stream = await navigator.mediaDevices.getUserMedia({video: true});
+ const sender = pc.addTrack(stream.getTracks()[0]);
+ const newRate = await GleanTest.rtcrtpsender.usedSendencodings.testGetValue();
+ is(newRate.denominator, oldRate.denominator + 1, "Glean should have recorded the creation of a single RTCRtpSender");
+ is(newRate.numerator, oldRate.numerator, "Glean should not have recorded a use of sendEncodings");
+ },
+
+ async function checkGoodSetParametersCompatMode() {
+ await pushPrefs(
+ ["media.peerconnection.allow_old_setParameters", true]);
+ const pc = new RTCPeerConnection();
+ const {sender} = pc.addTransceiver('video', {
+ sendEncodings: [{rid: "0"},{rid: "1"},{rid: "2"}]
+ });
+ const oldWarningRates = await getAllWarningRates();
+ await sender.setParameters(sender.getParameters());
+ const newWarningRates = await getAllWarningRates();
+ isDeeply(oldWarningRates, newWarningRates);
+ },
+
+ async function checkBadSetParametersNoGetParametersWarning() {
+ await pushPrefs(
+ ["media.peerconnection.allow_old_setParameters", true]);
+ const pc = new RTCPeerConnection();
+ const {sender} = pc.addTransceiver('video', {
+ sendEncodings: [{rid: "0"},{rid: "1"},{rid: "2"}]
+ });
+
+ let oldRate = await GleanTest.rtcrtpsenderSetparameters.warnNoGetparameters.testGetValue();
+ let oldBlameCount = await GleanTest.rtcrtpsenderSetparameters.blameNoGetparameters["example.com"].testGetValue() || 0;
+
+ await sender.setParameters({encodings: [{rid: "0"},{rid: "1"},{rid: "2"}]});
+
+ let newRate = await GleanTest.rtcrtpsenderSetparameters.warnNoGetparameters.testGetValue();
+ let newBlameCount = await GleanTest.rtcrtpsenderSetparameters.blameNoGetparameters["example.com"].testGetValue() || 0;
+
+ is(newRate.numerator, oldRate.numerator + 1, "Glean should have recorded a warning in setParameters due to lack of a getParameters call");
+
+ if (AppConstants.EARLY_BETA_OR_EARLIER) {
+ is(newBlameCount, oldBlameCount + 1, "Glean should have recorded that example.com encountered this warning");
+ } else {
+ is(newBlameCount, oldBlameCount, "Glean should not have recorded that example.com encountered this warning, because we're running this test on a stable channel");
+ }
+
+ oldRate = newRate;
+ oldBlameCount = newBlameCount;
+
+ // Glean should only record the warning once per sender!
+ await sender.setParameters({encodings: [{rid: "0"},{rid: "1"},{rid: "2"}]});
+
+ newRate = await GleanTest.rtcrtpsenderSetparameters.warnNoGetparameters.testGetValue();
+ newBlameCount = await GleanTest.rtcrtpsenderSetparameters.blameNoGetparameters["example.com"].testGetValue() || 0;
+
+ is(newRate.numerator, oldRate.numerator, "Glean should not have recorded another warning in setParameters due to lack of a getParameters call");
+ is(newBlameCount, oldBlameCount, "Glean should not have recorded that example.com encountered this warning a second time, since this is the same sender");
+ },
+
+ async function checkBadSetParametersLengthChangedWarning() {
+ await pushPrefs(
+ ["media.peerconnection.allow_old_setParameters", true]);
+ const pc = new RTCPeerConnection();
+ const {sender} = pc.addTransceiver('video', {
+ sendEncodings: [{rid: "0"},{rid: "1"},{rid: "2"}]
+ });
+
+ let oldRate = await GleanTest.rtcrtpsenderSetparameters.warnLengthChanged.testGetValue();
+ let oldBlameCount = await GleanTest.rtcrtpsenderSetparameters.blameLengthChanged["example.com"].testGetValue() || 0;
+
+ let params = sender.getParameters();
+ params.encodings.pop();
+ await sender.setParameters(params);
+
+ let newRate = await GleanTest.rtcrtpsenderSetparameters.warnLengthChanged.testGetValue();
+ let newBlameCount = await GleanTest.rtcrtpsenderSetparameters.blameLengthChanged["example.com"].testGetValue() || 0;
+
+ is(newRate.numerator, oldRate.numerator + 1, "Glean should have recorded a warning due to a length change in encodings");
+
+ if (AppConstants.EARLY_BETA_OR_EARLIER) {
+ is(newBlameCount, oldBlameCount + 1, "Glean should have recorded that example.com encountered this warning");
+ } else {
+ is(newBlameCount, oldBlameCount, "Glean should not have recorded that example.com encountered this warning, because we're running this test on a stable channel");
+ }
+
+ oldRate = newRate;
+ oldBlameCount = newBlameCount;
+
+ // Glean should only record the warning once per sender!
+ params = sender.getParameters();
+ params.encodings.pop();
+ await sender.setParameters(params);
+
+ newRate = await GleanTest.rtcrtpsenderSetparameters.warnLengthChanged.testGetValue();
+ newBlameCount = await GleanTest.rtcrtpsenderSetparameters.blameLengthChanged["example.com"].testGetValue() || 0;
+
+ is(newRate.numerator, oldRate.numerator, "Glean should not have recorded another warning due to a length change in encodings");
+ is(newBlameCount, oldBlameCount, "Glean should not have recorded that example.com encountered this warning a second time, since this is the same sender");
+ },
+
+ async function checkBadSetParametersRidChangedWarning() {
+ // This pref does not let rid change errors slide anymore
+ await pushPrefs(
+ ["media.peerconnection.allow_old_setParameters", true]);
+ const pc = new RTCPeerConnection();
+ const {sender} = pc.addTransceiver('video', {
+ sendEncodings: [{rid: "0"},{rid: "1"},{rid: "2"}]
+ });
+
+ let oldRate = await GleanTest.rtcrtpsenderSetparameters.failRidChanged.testGetValue();
+ let oldWarnRate = await GleanTest.rtcrtpsenderSetparameters.warnRidChanged.testGetValue();
+
+ let params = sender.getParameters();
+ params.encodings[1].rid = "foo";
+ try {
+ await sender.setParameters(params);
+ } catch (e) {
+ }
+ let newRate = await GleanTest.rtcrtpsenderSetparameters.failRidChanged.testGetValue();
+ let newWarnRate = await GleanTest.rtcrtpsenderSetparameters.warnRidChanged.testGetValue();
+ is(newRate.numerator, oldRate.numerator + 1, "Glean should have recorded an error due to a rid change in encodings");
+ is(newWarnRate.numerator, oldWarnRate.numerator, "Glean should not have recorded a warning due to a rid change in encodings");
+
+ // Glean should only record the error once per sender!
+ params = sender.getParameters();
+ params.encodings[1].rid = "bar";
+ oldRate = newRate;
+ try {
+ await sender.setParameters(params);
+ } catch (e) {
+ }
+ newRate = await GleanTest.rtcrtpsenderSetparameters.failRidChanged.testGetValue();
+ newWarnRate = await GleanTest.rtcrtpsenderSetparameters.warnRidChanged.testGetValue();
+ is(newRate.numerator, oldRate.numerator, "Glean should not have recorded another error due to a rid change in encodings");
+ is(newWarnRate.numerator, oldWarnRate.numerator, "Glean should not have recorded a warning due to a rid change in encodings");
+ },
+
+ async function checkBadSetParametersNoTransactionIdWarning() {
+ await pushPrefs(
+ ["media.peerconnection.allow_old_setParameters", true]);
+ const pc = new RTCPeerConnection();
+ const {sender} = pc.addTransceiver('video', {
+ sendEncodings: [{rid: "0"},{rid: "1"},{rid: "2"}]
+ });
+
+ let oldRate = await GleanTest.rtcrtpsenderSetparameters.warnNoTransactionid.testGetValue();
+ let oldBlameCount = await GleanTest.rtcrtpsenderSetparameters.blameNoTransactionid["example.com"].testGetValue() || 0;
+
+ await sender.setParameters({encodings: [{rid: "0"},{rid: "1"},{rid: "2"}]});
+
+ let newRate = await GleanTest.rtcrtpsenderSetparameters.warnNoTransactionid.testGetValue();
+ let newBlameCount = await GleanTest.rtcrtpsenderSetparameters.blameNoTransactionid["example.com"].testGetValue() || 0;
+
+ is(newRate.numerator, oldRate.numerator + 1, "Glean should have recorded a warning due to missing transactionId in setParameters");
+
+ if (AppConstants.EARLY_BETA_OR_EARLIER) {
+ is(newBlameCount, oldBlameCount + 1, "Glean should have recorded that example.com encountered this warning");
+ } else {
+ is(newBlameCount, oldBlameCount, "Glean should not have recorded that example.com encountered this warning, because we're running this test on a stable channel");
+ }
+
+ oldRate = newRate;
+ oldBlameCount = newBlameCount;
+
+ // Glean should only record the warning once per sender!
+ await sender.setParameters({encodings: [{rid: "0"},{rid: "1"},{rid: "2"}]});
+
+ newRate = await GleanTest.rtcrtpsenderSetparameters.warnNoTransactionid.testGetValue();
+ newBlameCount = await GleanTest.rtcrtpsenderSetparameters.blameNoTransactionid["example.com"].testGetValue() || 0;
+
+ is(newRate.numerator, oldRate.numerator, "Glean should not have recorded another warning due to missing transactionId in setParameters");
+ is(newBlameCount, oldBlameCount, "Glean should not have recorded that example.com encountered this warning a second time, since this is the same sender");
+ },
+
+ async function checkBadSetParametersStaleTransactionIdWarning() {
+ await pushPrefs(
+ ["media.peerconnection.allow_old_setParameters", true]);
+ const pc = new RTCPeerConnection();
+ const {sender} = pc.addTransceiver('video', {
+ sendEncodings: [{rid: "0"},{rid: "1"},{rid: "2"}]
+ });
+
+ let oldRate = await GleanTest.rtcrtpsenderSetparameters.warnStaleTransactionid.testGetValue();
+ let oldBlameCount = await GleanTest.rtcrtpsenderSetparameters.blameStaleTransactionid["example.com"].testGetValue() || 0;
+
+ let params = sender.getParameters();
+ // Cause transactionId to be stale
+ await pc.createOffer();
+ // ...but make sure there is a recent getParameters call
+ sender.getParameters();
+ await sender.setParameters(params);
+
+ let newRate = await GleanTest.rtcrtpsenderSetparameters.warnStaleTransactionid.testGetValue();
+ let newBlameCount = await GleanTest.rtcrtpsenderSetparameters.blameStaleTransactionid["example.com"].testGetValue() || 0;
+
+ is(newRate.numerator, oldRate.numerator + 1, "Glean should have recorded a warning due to stale transactionId in setParameters");
+
+ if (AppConstants.EARLY_BETA_OR_EARLIER) {
+ is(newBlameCount, oldBlameCount + 1, "Glean should have recorded that example.com encountered this warning");
+ } else {
+ is(newBlameCount, oldBlameCount, "Glean should not have recorded that example.com encountered this warning, because we're running this test on a stable channel");
+ }
+
+ oldRate = newRate;
+ oldBlameCount = newBlameCount;
+
+ // Glean should only record the warning once per sender!
+ params = sender.getParameters();
+ // Cause transactionId to be stale
+ await pc.createOffer();
+ // ...but make sure there is a recent getParameters call
+ sender.getParameters();
+ await sender.setParameters(params);
+
+ newRate = await GleanTest.rtcrtpsenderSetparameters.warnStaleTransactionid.testGetValue();
+ newBlameCount = await GleanTest.rtcrtpsenderSetparameters.blameStaleTransactionid["example.com"].testGetValue() || 0;
+
+ is(newRate.numerator, oldRate.numerator, "Glean should not have recorded another warning due to stale transactionId in setParameters");
+ is(newBlameCount, oldBlameCount, "Glean should not have recorded that example.com encountered this warning a second time, since this is the same sender");
+ },
+
+ async function checkBadSetParametersLengthChangedError() {
+ await pushPrefs(
+ ["media.peerconnection.allow_old_setParameters", false]);
+ const pc = new RTCPeerConnection();
+ const {sender} = pc.addTransceiver('video', {
+ sendEncodings: [{rid: "0"},{rid: "1"},{rid: "2"}]
+ });
+ let oldRate = await GleanTest.rtcrtpsenderSetparameters.failLengthChanged.testGetValue();
+ let params = sender.getParameters();
+ params.encodings.pop();
+ try {
+ await sender.setParameters(params);
+ } catch(e) {
+ }
+ let newRate = await GleanTest.rtcrtpsenderSetparameters.failLengthChanged.testGetValue();
+ is(newRate.numerator, oldRate.numerator + 1, "Glean should have recorded an error due to a length change in encodings");
+
+ // Glean should only record the error once per sender!
+ params = sender.getParameters();
+ params.encodings.pop();
+ oldRate = newRate;
+ try {
+ await sender.setParameters(params);
+ } catch (e) {
+ }
+ newRate = await GleanTest.rtcrtpsenderSetparameters.failLengthChanged.testGetValue();
+ is(newRate.numerator, oldRate.numerator, "Glean should not have recorded another error due to a length change in encodings");
+ },
+
+ async function checkBadSetParametersRidChangedError() {
+ await pushPrefs(
+ ["media.peerconnection.allow_old_setParameters", false]);
+ const pc = new RTCPeerConnection();
+ const {sender} = pc.addTransceiver('video', {
+ sendEncodings: [{rid: "0"},{rid: "1"},{rid: "2"}]
+ });
+ let oldRate = await GleanTest.rtcrtpsenderSetparameters.failRidChanged.testGetValue();
+ let params = sender.getParameters();
+ params.encodings[1].rid = "foo";
+ try {
+ await sender.setParameters(params);
+ } catch (e) {
+ }
+ let newRate = await GleanTest.rtcrtpsenderSetparameters.failRidChanged.testGetValue();
+ is(newRate.numerator, oldRate.numerator + 1, "Glean should have recorded an error due to a rid change in encodings");
+
+ // Glean should only record the error once per sender!
+ params = sender.getParameters();
+ params.encodings[1].rid = "bar";
+ oldRate = newRate;
+ try {
+ await sender.setParameters(params);
+ } catch (e) {
+ }
+ newRate = await GleanTest.rtcrtpsenderSetparameters.failRidChanged.testGetValue();
+ is(newRate.numerator, oldRate.numerator, "Glean should not have recorded another error due to a rid change in encodings");
+ },
+
+ async function checkBadSetParametersNoGetParametersError() {
+ await pushPrefs(
+ ["media.peerconnection.allow_old_setParameters", false]);
+ const pc = new RTCPeerConnection();
+ const {sender} = pc.addTransceiver('video', {
+ sendEncodings: [{rid: "0"},{rid: "1"},{rid: "2"}]
+ });
+ let oldRate = await GleanTest.rtcrtpsenderSetparameters.failNoGetparameters.testGetValue();
+ try {
+ await sender.setParameters({encodings: [{rid: "0"},{rid: "1"},{rid: "2"}]});
+ } catch (e) {
+ }
+ let newRate = await GleanTest.rtcrtpsenderSetparameters.failNoGetparameters.testGetValue();
+ is(newRate.numerator, oldRate.numerator + 1, "Glean should have recorded an error in setParameters due to lack of a getParameters call");
+
+ // Glean should only record the error once per sender!
+ oldRate = newRate;
+ try {
+ await sender.setParameters({encodings: [{rid: "0"},{rid: "1"},{rid: "2"}]});
+ } catch (e) {
+ }
+ newRate = await GleanTest.rtcrtpsenderSetparameters.failNoGetparameters.testGetValue();
+ is(newRate.numerator, oldRate.numerator, "Glean should not have recorded another error in setParameters due to lack of a getParameters call");
+ },
+
+ async function checkBadSetParametersStaleTransactionIdError() {
+ await pushPrefs(
+ ["media.peerconnection.allow_old_setParameters", false]);
+ const pc = new RTCPeerConnection();
+ const {sender} = pc.addTransceiver('video', {
+ sendEncodings: [{rid: "0"},{rid: "1"},{rid: "2"}]
+ });
+ let oldRate = await GleanTest.rtcrtpsenderSetparameters.failStaleTransactionid.testGetValue();
+ let params = sender.getParameters();
+ // Cause transactionId to be stale
+ await pc.createOffer();
+ // ...but make sure there is a recent getParameters call
+ sender.getParameters();
+ try {
+ await sender.setParameters(params);
+ } catch (e) {
+ }
+ let newRate = await GleanTest.rtcrtpsenderSetparameters.failStaleTransactionid.testGetValue();
+ is(newRate.numerator, oldRate.numerator + 1, "Glean should have recorded an error due to stale transactionId in setParameters");
+
+ // Glean should only record the error once per sender!
+ oldRate = newRate;
+ params = sender.getParameters();
+ // Cause transactionId to be stale
+ await pc.createOffer();
+ // ...but make sure there is a recent getParameters call
+ sender.getParameters();
+ try {
+ await sender.setParameters(params);
+ } catch (e) {
+ }
+ newRate = await GleanTest.rtcrtpsenderSetparameters.failStaleTransactionid.testGetValue();
+ is(newRate.numerator, oldRate.numerator, "Glean should not have recorded another error due to stale transactionId in setParameters");
+ },
+
+ async function checkBadSetParametersNoEncodingsError() {
+ // If we do not allow the old setParameters, this will fail the length check
+ // instead.
+ await pushPrefs(
+ ["media.peerconnection.allow_old_setParameters", true]);
+ const pc = new RTCPeerConnection();
+ const {sender} = pc.addTransceiver('video', {
+ sendEncodings: [{rid: "0"},{rid: "1"},{rid: "2"}]
+ });
+ let oldRate = await GleanTest.rtcrtpsenderSetparameters.failNoEncodings.testGetValue();
+ let params = sender.getParameters();
+ params.encodings = [];
+ try {
+ await sender.setParameters(params);
+ } catch (e) {
+ }
+ let newRate = await GleanTest.rtcrtpsenderSetparameters.failNoEncodings.testGetValue();
+ is(newRate.numerator, oldRate.numerator, "Glean should not have recorded an error due to empty encodings in setParameters");
+
+ // Glean should only record the error once per sender!
+ oldRate = newRate;
+ params = sender.getParameters();
+ params.encodings = [];
+ try {
+ await sender.setParameters(params);
+ } catch (e) {
+ }
+ newRate = await GleanTest.rtcrtpsenderSetparameters.failNoEncodings.testGetValue();
+ is(newRate.numerator, oldRate.numerator, "Glean should not have recorded an error due empty encodings in setParameters");
+ },
+
+ async function checkBadSetParametersOtherError() {
+ const pc = new RTCPeerConnection();
+ const {sender} = pc.addTransceiver('video', {
+ sendEncodings: [{rid: "0"},{rid: "1"},{rid: "2"}]
+ });
+ let oldRate = await GleanTest.rtcrtpsenderSetparameters.failOther.testGetValue();
+ let params = sender.getParameters();
+ params.encodings[0].scaleResolutionDownBy = 0.5;
+ try {
+ await sender.setParameters(params);
+ } catch (e) {
+ }
+ let newRate = await GleanTest.rtcrtpsenderSetparameters.failOther.testGetValue();
+ is(newRate.numerator, oldRate.numerator + 1, "Glean should have recorded an error due to some other failure");
+
+ // Glean should only record the error once per sender!
+ oldRate = newRate;
+ params = sender.getParameters();
+ params.encodings[0].scaleResolutionDownBy = 0.5;
+ try {
+ await sender.setParameters(params);
+ } catch (e) {
+ }
+ newRate = await GleanTest.rtcrtpsenderSetparameters.failOther.testGetValue();
+ is(newRate.numerator, oldRate.numerator, "Glean should not have recorded another error due to some other failure");
+ },
+
+];
+
+runNetworkTest(async () => {
+ for (const test of tests) {
+ info(`Running test: ${test.name}`);
+ await test();
+ info(`Done running test: ${test.name}`);
+ }
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_iceFailure.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_iceFailure.html
new file mode 100644
index 0000000000..1b82473997
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_iceFailure.html
@@ -0,0 +1,84 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1087629",
+ title: "Wait for ICE failure"
+ });
+
+// Test iceFailure
+
+function PC_LOCAL_SETUP_NULL_ICE_HANDLER(test) {
+ test.pcLocal.setupIceCandidateHandler(test, function() {}, function () {});
+}
+function PC_REMOTE_SETUP_NULL_ICE_HANDLER(test) {
+ test.pcRemote.setupIceCandidateHandler(test, function() {}, function () {});
+}
+function PC_REMOTE_ADD_FAKE_ICE_CANDIDATE(test) {
+ var cand = {"candidate":"candidate:0 1 UDP 2130379007 192.0.2.1 12345 typ host","sdpMid":"","sdpMLineIndex":0};
+ test.pcRemote.storeOrAddIceCandidate(cand);
+ info(test.pcRemote + " Stored fake candidate: " + JSON.stringify(cand));
+}
+function PC_LOCAL_ADD_FAKE_ICE_CANDIDATE(test) {
+ var cand = {"candidate":"candidate:0 1 UDP 2130379007 192.0.2.2 56789 typ host","sdpMid":"","sdpMLineIndex":0};
+ test.pcLocal.storeOrAddIceCandidate(cand);
+ info(test.pcLocal + " Stored fake candidate: " + JSON.stringify(cand));
+}
+function PC_LOCAL_WAIT_FOR_ICE_FAILURE(test) {
+ return test.pcLocal.iceFailed.then(() => {
+ ok(true, this.pcLocal + " Ice Failure Reached.");
+ });
+}
+function PC_REMOTE_WAIT_FOR_ICE_FAILURE(test) {
+ return test.pcRemote.iceFailed.then(() => {
+ ok(true, this.pcRemote + " Ice Failure Reached.");
+ });
+}
+function PC_LOCAL_WAIT_FOR_ICE_FAILED(test) {
+ var resolveIceFailed;
+ test.pcLocal.iceFailed = new Promise(r => resolveIceFailed = r);
+ test.pcLocal.ice_connection_callbacks.checkIceStatus = () => {
+ if (test.pcLocal._pc.iceConnectionState === "failed") {
+ resolveIceFailed();
+ }
+ }
+}
+function PC_REMOTE_WAIT_FOR_ICE_FAILED(test) {
+ var resolveIceFailed;
+ test.pcRemote.iceFailed = new Promise(r => resolveIceFailed = r);
+ test.pcRemote.ice_connection_callbacks.checkIceStatus = () => {
+ if (test.pcRemote._pc.iceConnectionState === "failed") {
+ resolveIceFailed();
+ }
+ }
+}
+
+runNetworkTest(async () => {
+ await pushPrefs(
+ ['media.peerconnection.ice.stun_client_maximum_transmits', 3],
+ ['media.peerconnection.ice.trickle_grace_period', 3000],
+ );
+ var test = new PeerConnectionTest();
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ test.chain.replace("PC_LOCAL_SETUP_ICE_HANDLER", PC_LOCAL_SETUP_NULL_ICE_HANDLER);
+ test.chain.replace("PC_REMOTE_SETUP_ICE_HANDLER", PC_REMOTE_SETUP_NULL_ICE_HANDLER);
+ test.chain.insertAfter("PC_REMOTE_SETUP_NULL_ICE_HANDLER", PC_LOCAL_WAIT_FOR_ICE_FAILED);
+ test.chain.insertAfter("PC_LOCAL_WAIT_FOR_ICE_FAILED", PC_REMOTE_WAIT_FOR_ICE_FAILED);
+ test.chain.removeAfter("PC_LOCAL_SET_REMOTE_DESCRIPTION");
+ test.chain.append([
+ PC_REMOTE_ADD_FAKE_ICE_CANDIDATE,
+ PC_LOCAL_ADD_FAKE_ICE_CANDIDATE,
+ PC_LOCAL_WAIT_FOR_ICE_FAILURE,
+ PC_REMOTE_WAIT_FOR_ICE_FAILURE
+ ]);
+ await test.run();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_insertDTMF.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_insertDTMF.html
new file mode 100644
index 0000000000..ca8b866a6d
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_insertDTMF.html
@@ -0,0 +1,76 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+ bug: "1291715",
+ title: "Test insertDTMF on sender",
+ visible: true
+});
+
+function insertdtmftest(pc) {
+ ok(pc.getSenders().length, "have senders");
+ var sender = pc.getSenders()[0];
+ ok(sender.dtmf, "sender has dtmf object");
+
+ ok(sender.dtmf.toneBuffer === "", "sender should start with empty tonebuffer");
+
+ // These will trigger assertions on debug builds if we do not enforce the
+ // specified minimums and maximums for duration and interToneGap.
+ sender.dtmf.insertDTMF("A", 10);
+ sender.dtmf.insertDTMF("A", 10000);
+ sender.dtmf.insertDTMF("A", 70, 10);
+
+ var threw = false;
+ try {
+ sender.dtmf.insertDTMF("bad tones");
+ } catch (ex) {
+ threw = true;
+ is(ex.code, DOMException.INVALID_CHARACTER_ERR, "Expected InvalidCharacterError");
+ }
+ ok(threw, "Expected exception");
+
+ sender.dtmf.insertDTMF("A");
+ sender.dtmf.insertDTMF("B");
+ ok(!sender.dtmf.toneBuffer.includes("A"), "calling insertDTMF should replace current characters");
+
+ sender.dtmf.insertDTMF("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
+ ok(sender.dtmf.toneBuffer.includes("A"), "lowercase characters should be normalized");
+
+ pc.removeTrack(sender);
+ threw = false;
+ try {
+ sender.dtmf.insertDTMF("AAA");
+ } catch (ex) {
+ threw = true;
+ is(ex.code, DOMException.INVALID_STATE_ERR, "Expected InvalidStateError");
+ }
+ ok(threw, "Expected exception");
+}
+
+runNetworkTest(() => {
+ test = new PeerConnectionTest();
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ test.chain.removeAfter("PC_REMOTE_WAIT_FOR_MEDIA_FLOW");
+
+ // Test sender dtmf.
+ test.chain.append([
+ function PC_LOCAL_INSERT_DTMF(test) {
+ // We want to call removeTrack
+ test.pcLocal.expectNegotiationNeeded();
+ return insertdtmftest(test.pcLocal._pc);
+ }
+ ]);
+
+ return pushPrefs(['media.peerconnection.dtmf.enabled', true])
+ .then(() => test.run());
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_localReofferRollback.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_localReofferRollback.html
new file mode 100644
index 0000000000..16406ece6e
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_localReofferRollback.html
@@ -0,0 +1,44 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "952145",
+ title: "Rollback local reoffer"
+ });
+
+ runNetworkTest(function (options) {
+ const test = new PeerConnectionTest(options);
+ addRenegotiation(test.chain, [
+ function PC_LOCAL_ADD_SECOND_STREAM(test) {
+ test.setMediaConstraints([{audio: true}, {audio: true}],
+ [{audio: true}]);
+ return test.pcLocal.getAllUserMediaAndAddStreams([{audio: true}]);
+ },
+
+ function PC_LOCAL_CREATE_AND_SET_OFFER(test) {
+ return test.createOffer(test.pcLocal).then(offer => {
+ return test.setLocalDescription(test.pcLocal, offer, HAVE_LOCAL_OFFER);
+ });
+ },
+
+ function PC_LOCAL_ROLLBACK(test) {
+ // the negotiationNeeded slot should have been true both before and
+ // after this SLD, so the event should fire again.
+ test.pcLocal.expectNegotiationNeeded();
+ return test.setLocalDescription(test.pcLocal,
+ { type: "rollback", sdp: "" },
+ STABLE);
+ },
+ ]);
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_localRollback.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_localRollback.html
new file mode 100644
index 0000000000..5bdc8cc029
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_localRollback.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "952145",
+ title: "Rollback local offer"
+ });
+
+ runNetworkTest(function (options) {
+ const test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ test.chain.insertBefore('PC_LOCAL_CREATE_OFFER', [
+ function PC_REMOTE_CREATE_AND_SET_OFFER(test) {
+ return test.createOffer(test.pcRemote).then(offer => {
+ return test.setLocalDescription(test.pcRemote, offer, HAVE_LOCAL_OFFER);
+ });
+ },
+
+ function PC_REMOTE_ROLLBACK(test) {
+ // the negotiationNeeded slot should have been true both before and
+ // after this SLD, so the event should fire again.
+ test.pcRemote.expectNegotiationNeeded();
+ return test.setLocalDescription(test.pcRemote,
+ { type: "rollback", sdp: "" },
+ STABLE);
+ },
+
+ // Rolling back should shut down gathering
+ function PC_REMOTE_WAIT_FOR_END_OF_TRICKLE(test) {
+ return test.pcRemote.endOfTrickleIce;
+ },
+
+ function PC_REMOTE_SETUP_ICE_HANDLER(test) {
+ test.pcRemote.setupIceCandidateHandler(test);
+ },
+ ]);
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_maxFsConstraint.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_maxFsConstraint.html
new file mode 100644
index 0000000000..a2f2555020
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_maxFsConstraint.html
@@ -0,0 +1,112 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1393687",
+ title: "Enforce max-fs constraint on a PeerConnection",
+ visible: true
+ });
+
+ var mustRejectWith = (msg, reason, f) =>
+ f().then(() => ok(false, msg),
+ e => is(e.name, reason, msg));
+
+ var removeAllButCodec = (d, codec) =>
+ (d.sdp = d.sdp.replace(/m=video (\w) UDP\/TLS\/RTP\/SAVPF \w.*\r\n/,
+ "m=video $1 UDP/TLS/RTP/SAVPF " + codec + "\r\n"), d);
+
+ var mungeSDP = (d, forceH264) => {
+ if (forceH264) {
+ removeAllButCodec(d, 126);
+ d.sdp = d.sdp.replace(/a=fmtp:126 (.*);packetization-mode=1/, "a=fmtp:126 $1;packetization-mode=1;max-fs=100");
+ } else {
+ d.sdp = d.sdp.replace(/max-fs=\d+/, "max-fs=100");
+ }
+ return d;
+ };
+
+ const checkForH264Support = async () => {
+ const pc = new RTCPeerConnection();
+ const offer = await pc.createOffer({offerToReceiveVideo: true});
+ return offer.sdp.match(/a=rtpmap:[1-9][0-9]* H264/);
+ };
+
+ let resolutionAlignment = 1;
+
+ function testScale(codec) {
+ var v1 = createMediaElement('video', 'v1');
+ var v2 = createMediaElement('video', 'v2');
+
+ var pc1 = new RTCPeerConnection();
+ var pc2 = new RTCPeerConnection();
+
+ var add = (pc, can, failed) => can && pc.addIceCandidate(can).catch(failed);
+ pc1.onicecandidate = e => add(pc2, e.candidate, generateErrorCallback());
+ pc2.onicecandidate = e => add(pc1, e.candidate, generateErrorCallback());
+
+ info("testing max-fs with" + codec);
+
+ pc1.onnegotiationneeded = e =>
+ pc1.createOffer()
+ .then(d => pc1.setLocalDescription(mungeSDP(d, codec == "H264")))
+ .then(() => pc2.setRemoteDescription(pc1.localDescription))
+ .then(() => pc2.createAnswer()).then(d => pc2.setLocalDescription(mungeSDP(d, codec =="H264")))
+ .then(() => pc1.setRemoteDescription(pc2.localDescription))
+ .catch(generateErrorCallback());
+
+ pc2.ontrack = e => {
+ v2.srcObject = e.streams[0];
+ };
+
+ var stream;
+
+ return navigator.mediaDevices.getUserMedia({ video: true })
+ .then(s => {
+ stream = s;
+ v1.srcObject = stream;
+ let track = stream.getVideoTracks()[0];
+ let sender = pc1.addTrack(track, stream);
+ is(v2.currentTime, 0, "v2.currentTime is zero at outset");
+ })
+ .then(() => wait(5000))
+ .then(() => {
+ if (v2.videoWidth == 0 && v2.videoHeight == 0) {
+ info("Skipping test, insufficient time for video to start.");
+ } else {
+ const expectedWidth = 184 - 184 % resolutionAlignment;
+ const expectedHeight = 138 - 138 % resolutionAlignment;
+ is(v2.videoWidth, expectedWidth,
+ `sink width should be ${expectedWidth} for ${codec}`);
+ is(v2.videoHeight, expectedHeight,
+ `sink height should be ${expectedHeight} for ${codec}`);
+ }})
+ .then(() => {
+ stream.getTracks().forEach(track => track.stop());
+ v1.srcObject = v2.srcObject = null;
+ }).catch(generateErrorCallback());
+ }
+
+ runNetworkTest(async () => {
+ await pushPrefs(['media.peerconnection.video.lock_scaling', true]);
+ if (await checkForH264Support()) {
+ if (navigator.userAgent.includes("Android")) {
+ // Android only has a hw encoder for h264
+ resolutionAlignment = 16;
+ }
+ await matchPlatformH264CodecPrefs();
+ await testScale("H264");
+ }
+
+ // Disable h264 hardware support, to ensure it is not prioritized over VP8
+ await pushPrefs(["media.webrtc.hw.h264.enabled", false]);
+ await testScale("VP8");
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_multiple_captureStream_canvas_2d.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_multiple_captureStream_canvas_2d.html
new file mode 100644
index 0000000000..9ad25e7852
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_multiple_captureStream_canvas_2d.html
@@ -0,0 +1,115 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+ bug: "1166832",
+ title: "Canvas(2D)::Multiple CaptureStream as video-only input to peerconnection",
+ visible: true
+});
+
+/**
+ * Test to verify using multiple capture streams concurrently.
+ */
+runNetworkTest(async () => {
+ // [TODO] re-enable HW decoder after bug 1526207 is fixed.
+ if (navigator.userAgent.includes("Android")) {
+ await pushPrefs(["media.navigator.mediadatadecoder_vpx_enabled", false]);
+ await pushPrefs(["media.webrtc.hw.h264.enabled", false]);
+ }
+
+ var test = new PeerConnectionTest();
+ var h = new CaptureStreamTestHelper2D(50, 50);
+
+ var vremote1;
+ var stream1;
+ var canvas1 = h.createAndAppendElement('canvas', 'source_canvas1');
+
+ var vremote2;
+ var stream2;
+ var canvas2 = h.createAndAppendElement('canvas', 'source_canvas2');
+
+ const threshold = 128;
+
+ test.setMediaConstraints([{video: true}, {video: true}], []);
+ test.chain.replace("PC_LOCAL_GUM", [
+ function PC_LOCAL_CANVAS_CAPTURESTREAM(test) {
+ h.drawColor(canvas1, h.green);
+ h.drawColor(canvas2, h.blue);
+ stream1 = canvas1.captureStream(0); // fps = 0 to capture single frame
+ test.pcLocal.attachLocalStream(stream1);
+ stream2 = canvas2.captureStream(0); // fps = 0 to capture single frame
+ test.pcLocal.attachLocalStream(stream2);
+ var i = 0;
+ return setInterval(function() {
+ try {
+ info("draw " + i ? "green" : "red/blue");
+ h.drawColor(canvas1, i ? h.green : h.red);
+ h.drawColor(canvas2, i ? h.green : h.blue);
+ i = 1 - i;
+ stream1.requestFrame();
+ stream2.requestFrame();
+ } catch (e) {
+ // ignore; stream might have shut down, and we don't bother clearing
+ // the setInterval.
+ }
+ }, 500);
+ }
+ ]);
+
+ test.chain.append([
+ function CHECK_REMOTE_VIDEO() {
+ is(test.pcRemote.remoteMediaElements.length, 2, "pcRemote Should have 2 remote media elements");
+ vremote1 = test.pcRemote.remoteMediaElements[0];
+ vremote2 = test.pcRemote.remoteMediaElements[1];
+
+ // since we don't know which remote video is created first, we don't know
+ // which should be blue or red, but this will make sure that one is
+ // green and one is blue
+ return Promise.race([
+ Promise.all([
+ h.pixelMustBecome(vremote1, h.red, {
+ threshold,
+ infoString: "pcRemote's remote1 should become red",
+ }),
+ h.pixelMustBecome(vremote2, h.blue, {
+ threshold,
+ infoString: "pcRemote's remote2 should become blue",
+ }),
+ ]),
+ Promise.all([
+ h.pixelMustBecome(vremote2, h.red, {
+ threshold,
+ infoString: "pcRemote's remote2 should become red",
+ }),
+ h.pixelMustBecome(vremote1, h.blue, {
+ threshold,
+ infoString: "pcRemote's remote1 should become blue",
+ }),
+ ])
+ ]);
+ },
+ function WAIT_FOR_REMOTE_BOTH_GREEN() {
+ return Promise.all([
+ h.pixelMustBecome(vremote1, h.green, {
+ threshold,
+ infoString: "pcRemote's remote1 should become green",
+ }),
+ h.pixelMustBecome(vremote2, h.green, {
+ threshold,
+ infoString: "pcRemote's remote2 should become green",
+ }),
+ ])
+ },
+ ]);
+ await test.run();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_noTrickleAnswer.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_noTrickleAnswer.html
new file mode 100644
index 0000000000..7e3fd78430
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_noTrickleAnswer.html
@@ -0,0 +1,25 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="nonTrickleIce.js"></script>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1060102",
+ title: "Basic audio only SDP answer without trickle ICE"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ test = new PeerConnectionTest(options);
+ makeAnswererNonTrickle(test.chain);
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_noTrickleOffer.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_noTrickleOffer.html
new file mode 100644
index 0000000000..12b2a95596
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_noTrickleOffer.html
@@ -0,0 +1,25 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="nonTrickleIce.js"></script>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1060102",
+ title: "Basic audio only SDP offer without trickle ICE"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ test = new PeerConnectionTest(options);
+ makeOffererNonTrickle(test.chain);
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_noTrickleOfferAnswer.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_noTrickleOfferAnswer.html
new file mode 100644
index 0000000000..554750e975
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_noTrickleOfferAnswer.html
@@ -0,0 +1,26 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="nonTrickleIce.js"></script>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1060102",
+ title: "Basic audio only SDP offer and answer without trickle ICE"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ test = new PeerConnectionTest(options);
+ makeOffererNonTrickle(test.chain);
+ makeAnswererNonTrickle(test.chain);
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_nonDefaultRate.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_nonDefaultRate.html
new file mode 100644
index 0000000000..ad9414cef2
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_nonDefaultRate.html
@@ -0,0 +1,200 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({ title: "PeerConnection feed to a graph with non default rate", bug: "1387454" });
+ /**
+ * Run a test to verify that when we use the streem with nonDefault rate to/from a PC
+ * the connection fails. (PC is always on default rate).
+ */
+
+ let pc1;
+ let pc2;
+
+ const offerOptions = {
+ offerToReceiveAudio: 1,
+ };
+
+ function getName(pc) {
+ return (pc === pc1) ? 'pc1' : 'pc2';
+ }
+
+ function getOtherPc(pc) {
+ return (pc === pc1) ? pc2 : pc1;
+ }
+
+ function onAddIceCandidateSuccess(pc) {
+ ok(true, getName(pc) + ' addIceCandidate success');
+ }
+
+ function onAddIceCandidateError(pc, error) {
+ ok(false, getName(pc) + ' failed to add ICE Candidate: ' + error.toString());
+ }
+
+ function onIceCandidate(pc, event, done) {
+ if (!event.candidate) {
+ ok(pc.iceGatheringState === 'complete', getName(pc) + " ICE gathering state has reached complete");
+ done();
+ return;
+ }
+ getOtherPc(pc).addIceCandidate(event.candidate)
+ .then(() => {
+ onAddIceCandidateSuccess(pc);
+ },
+ (err) => {
+ onAddIceCandidateError(pc, err);
+ });
+ info(getName(pc) + ' ICE candidate: ' + event.candidate.candidate);
+ }
+
+ function onIceStateChange(pc, event) {
+ if (pc) {
+ info(getName(pc) + ' ICE state: ' + pc.iceConnectionState);
+ info('ICE state change event: ', event);
+ }
+ }
+
+ function onCreateOfferSuccess(desc) {
+ info('Offer from pc1\n' + desc.sdp);
+ info('pc1 setLocalDescription start');
+
+ pc1.setLocalDescription(desc)
+ .then(() => {
+ onSetLocalSuccess(pc1);
+ },
+ onSetSessionDescriptionError);
+
+ info('pc2 setRemoteDescription start');
+ pc2.setRemoteDescription(desc).then(() => {
+ onSetRemoteSuccess(pc2);
+ },
+ onSetSessionDescriptionError);
+
+ info('pc2 createAnswer start');
+
+ // Since the 'remote' side has no media stream we need
+ // to pass in the right constraints in order for it to
+ // accept the incoming offer of audio and video.
+ pc2.createAnswer()
+ .then(onCreateAnswerSuccess, onCreateSessionDescriptionError);
+ }
+
+ function onSetSessionDescriptionError(error) {
+ ok(false, 'Failed to set session description: ' + error.toString());
+ }
+
+ function onSetLocalSuccess(pc) {
+ ok(true, getName(pc) + ' setLocalDescription complete');
+ }
+
+ function onCreateSessionDescriptionError(error) {
+ ok(false, 'Failed to create session description: ' + error.toString());
+ }
+
+ function onSetRemoteSuccess(pc) {
+ ok(true, getName(pc) + ' setRemoteDescription complete');
+ }
+
+ function onCreateAnswerSuccess(desc) {
+ info('Answer from pc2:\n' + desc.sdp);
+ info('pc2 setLocalDescription start');
+ pc2.setLocalDescription(desc).then(() => {
+ onSetLocalSuccess(pc2);
+ },
+ onSetSessionDescriptionError);
+ info('pc1 setRemoteDescription start');
+ pc1.setRemoteDescription(desc).then(() => {
+ onSetRemoteSuccess(pc1);
+ },
+ onSetSessionDescriptionError);
+ }
+
+ async function getRemoteStream(localStream) {
+ info("got local stream")
+ const audioTracks = localStream.getAudioTracks();
+
+ const servers = null;
+
+ pc1 = new RTCPeerConnection(servers);
+ info('Created local peer connection object pc1');
+ const iceComplete1 = new Promise((resolve, reject) => {
+ pc1.onicecandidate = (e) => {
+ onIceCandidate(pc1, e, resolve);
+ };
+ });
+
+ pc2 = new RTCPeerConnection(servers);
+ info('Created remote peer connection object pc2');
+ const iceComplete2 = new Promise((resolve, reject) => {
+ pc2.onicecandidate = (e) => {
+ onIceCandidate(pc2, e, resolve);
+ };
+ });
+
+ pc1.oniceconnectionstatechange = (e) => {
+ onIceStateChange(pc1, e);
+ };
+ pc2.oniceconnectionstatechange = (e) => {
+ onIceStateChange(pc2, e);
+ };
+
+ const remoteStreamPromise = new Promise((resolve, reject) => {
+ pc2.ontrack = (e) => {
+ info('pc2 received remote stream ' + e.streams[0]);
+ resolve(e.streams[0]);
+ };
+ });
+
+ localStream.getTracks().forEach((track) => {
+ pc1.addTrack(track, localStream);
+ });
+ info('Added local stream to pc1');
+
+ info('pc1 createOffer start');
+ pc1.createOffer(offerOptions)
+ .then(onCreateOfferSuccess,onCreateSessionDescriptionError);
+
+ let promise_arr = await Promise.all([remoteStreamPromise, iceComplete1, iceComplete2]);
+ return promise_arr[0];
+ }
+
+ runTest(async () => {
+ // Local stream operates at non default rate (32000)
+ const nonDefaultRate = 32000;
+ const nonDefault_ctx = new AudioContext({sampleRate: nonDefaultRate});
+ oscillator = nonDefault_ctx.createOscillator();
+ const dest = nonDefault_ctx.createMediaStreamDestination();
+ oscillator.connect(dest);
+ oscillator.start();
+
+ // Wait for remote stream
+ const remoteStream = await getRemoteStream(dest.stream)
+ ok(true, 'Got remote stream ' + remoteStream);
+
+ // remoteStream now comes from PC so operates at default
+ // rates. Verify that by adding to a default context
+ const ac = new AudioContext;
+ const source_default_rate = ac.createMediaStreamSource(remoteStream);
+
+ // Now try to add the remoteStream on a non default context
+ mustThrowWith(
+ "Connect stream with graph of different sample rate",
+ "NotSupportedError", () => {
+ nonDefault_ctx.createMediaStreamSource(remoteStream);
+ }
+ );
+
+ // Close peer connections to make sure we don't get error:
+ // "logged result after SimpleTest.finish(): pc1 addIceCandidate success"
+ // See Bug 1626814.
+ pc1.close();
+ pc2.close();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_offerRequiresReceiveAudio.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_offerRequiresReceiveAudio.html
new file mode 100644
index 0000000000..1f936714f1
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_offerRequiresReceiveAudio.html
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "850275",
+ title: "Simple offer media constraint test with audio"
+ });
+
+ runNetworkTest(function() {
+ var test = new PeerConnectionTest();
+ test.setMediaConstraints([], [{audio: true}]);
+ test.setOfferOptions({ offerToReceiveAudio: true });
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_offerRequiresReceiveVideo.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_offerRequiresReceiveVideo.html
new file mode 100644
index 0000000000..c5afbb5c1f
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_offerRequiresReceiveVideo.html
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "850275",
+ title: "Simple offer media constraint test with video"
+ });
+
+ runNetworkTest(function() {
+ var test = new PeerConnectionTest();
+ test.setMediaConstraints([], [{video: true}]);
+ test.setOfferOptions({ offerToReceiveVideo: true });
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_offerRequiresReceiveVideoAudio.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_offerRequiresReceiveVideoAudio.html
new file mode 100644
index 0000000000..d7bc29c6d3
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_offerRequiresReceiveVideoAudio.html
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "850275",
+ title: "Simple offer media constraint test with video/audio"
+ });
+
+ runNetworkTest(function() {
+ var test = new PeerConnectionTest();
+ test.setMediaConstraints([], [{audio: true, video: true}]);
+ test.setOfferOptions({ offerToReceiveVideo: true, offerToReceiveAudio: true });
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_portRestrictions.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_portRestrictions.html
new file mode 100644
index 0000000000..7cd695ff54
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_portRestrictions.html
@@ -0,0 +1,63 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1677046",
+ title: "RTCPeerConnection check restricted ports"
+ });
+
+var makePC = (config, expected_error) => {
+ var exception;
+ try {
+ new RTCPeerConnection(config).close();
+ } catch (e) {
+ exception = e;
+ }
+ is((exception? exception.name : "success"), expected_error || "success",
+ "RTCPeerConnection(" + JSON.stringify(config) + ")");
+};
+
+// This is a test of the iceServers parsing code + readable errors
+runNetworkTest(() => {
+ var exception = null;
+
+ // check various ports on the blocklist
+ makePC({ iceServers: [
+ { urls:"turn:[::1]:6666", username:"p", credential:"p" }] }, "NS_ERROR_UNEXPECTED");
+ makePC({ iceServers: [
+ { urls:"turns:localhost:6667?transport=udp", username:"p", credential:"p" }] },
+ "NS_ERROR_UNEXPECTED");
+ makePC({ iceServers: [
+ { urls:"stun:localhost:21", foo:"" }] }, "NS_ERROR_UNEXPECTED");
+ makePC({ iceServers: [
+ { urls:"stun:[::1]:22", foo:"" }] }, "NS_ERROR_UNEXPECTED");
+ makePC({ iceServers: [
+ { urls:"turn:localhost:5060", username:"p", credential:"p" }] },
+ "NS_ERROR_UNEXPECTED");
+
+ // check various ports on the good list for webrtc (or default port)
+ makePC({ iceServers: [
+ { urls:"turn:[::1]:53", username:"p", credential:"p" },
+ { urls:"turn:[::1]:5349", username:"p", credential:"p" },
+ { urls:"turn:[::1]:3478", username:"p", credential:"p" },
+ { urls:"turn:[::1]", username:"p", credential:"p" },
+ { urls:"turn:localhost:53?transport=udp", username:"p", credential:"p" },
+ { urls:"turn:localhost:3478?transport=udp", username:"p", credential:"p" },
+ { urls:"turn:localhost:53?transport=tcp", username:"p", credential:"p" },
+ { urls:"turn:localhost:3478?transport=tcp", username:"p", credential:"p" },
+ { urls:"turns:localhost:3478?transport=udp", username:"p", credential:"p" },
+ { urls:"stun:localhost", foo:"" }
+ ]});
+
+ // not in the known good ports and not on the generic block list
+ makePC({ iceServers: [{ urls:"turn:localhost:6664", username:"p", credential:"p" }] });
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_promiseSendOnly.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_promiseSendOnly.html
new file mode 100644
index 0000000000..a3fbb5753c
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_promiseSendOnly.html
@@ -0,0 +1,61 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1091898",
+ title: "PeerConnection with promises (sendonly)",
+ visible: true
+ });
+
+ var pc1 = new RTCPeerConnection();
+ var pc2 = new RTCPeerConnection();
+
+ var add = (pc, can, failed) => can && pc.addIceCandidate(can).catch(failed);
+ pc1.onicecandidate = e => add(pc2, e.candidate, generateErrorCallback());
+ pc2.onicecandidate = e => add(pc1, e.candidate, generateErrorCallback());
+
+ var v1, v2;
+ var delivered = new Promise(resolve => pc2.ontrack = e => {
+ // Test RTCTrackEvent here.
+ ok(e.streams.length, "has streams");
+ ok(e.streams[0].getTrackById(e.track.id), "has track");
+ ok(pc2.getReceivers().some(receiver => receiver == e.receiver), "has receiver");
+ if (e.streams[0].getTracks().length == 2) {
+ // Test RTCTrackEvent required args here.
+ mustThrowWith("RTCTrackEvent wo/required args",
+ "TypeError", () => new RTCTrackEvent("track", {}));
+ v2.srcObject = e.streams[0];
+ resolve();
+ }
+ });
+
+ runNetworkTest(function() {
+ v1 = createMediaElement('video', 'v1');
+ v2 = createMediaElement('video', 'v2');
+ var canPlayThrough = new Promise(resolve => v2.canplaythrough = e => resolve());
+
+ is(v2.currentTime, 0, "v2.currentTime is zero at outset");
+
+ return navigator.mediaDevices.getUserMedia({ video: true, audio: true })
+ .then(stream => (v1.srcObject = stream).getTracks().forEach(t => pc1.addTrack(t, stream)))
+ .then(() => pc1.createOffer({})) // check that createOffer accepts arg.
+ .then(offer => pc1.setLocalDescription(offer))
+ .then(() => pc2.setRemoteDescription(pc1.localDescription))
+ .then(() => pc2.createAnswer({})) // check that createAnswer accepts arg.
+ .then(answer => pc2.setLocalDescription(answer))
+ .then(() => pc1.setRemoteDescription(pc2.localDescription))
+ .then(() => delivered)
+// .then(() => canPlayThrough) // why doesn't this fire?
+ .then(() => waitUntil(() => v2.currentTime > 0))
+ .then(() => ok(v2.currentTime > 0, "v2.currentTime is moving (" + v2.currentTime + ")"))
+ .then(() => ok(true, "Connected."));
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_recordReceiveTrack.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_recordReceiveTrack.html
new file mode 100644
index 0000000000..d5cb91b048
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_recordReceiveTrack.html
@@ -0,0 +1,101 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<script src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script>
+createHTML({
+ bug: "1212237",
+ title: "Recording a fresh receive track should not throw",
+ visible: true,
+});
+
+/**
+ * Called when a fresh track is available, and tests that the track can be
+ * recorded until it ends without any thrown errors or fired error events.
+ */
+let generation = 0;
+async function testTrackAccessible(track) {
+ const id = ++generation;
+ info(`Testing accessibility for ${track.kind} track ${id}`);
+ const recorder = new MediaRecorder(new MediaStream([track]));
+ recorder.start();
+ let haveError = new Promise((_, rej) => recorder.onerror = e => rej(e.error));
+ await Promise.race([
+ new Promise(r => recorder.onstart = r),
+ haveError,
+ ]);
+ info(`Recording of ${track.kind} track ${id} started`);
+
+ const {data} = await Promise.race([
+ new Promise(r => recorder.ondataavailable = r),
+ haveError,
+ ]);
+ info(`Recording of ${track.kind} track ${id} finished at size ${data.size}`);
+
+ await Promise.race([
+ new Promise(r => recorder.onstop = r),
+ haveError,
+ ]);
+ info(`Recording of ${track.kind} track ${id} stopped`);
+
+ const element = createMediaElement(track.kind, `recording_${track.id}`);
+ const url = URL.createObjectURL(data);
+ try {
+ element.src = url;
+ element.preload = "metadata";
+ haveError = new Promise(
+ (_, rej) => element.onerror = e => rej(element.error));
+ await Promise.race([
+ new Promise(r => element.onloadeddata = r),
+ haveError,
+ ]);
+ info(`Playback of recording of ${track.kind} track ${id} loaded data`);
+
+ element.play();
+ await Promise.race([
+ new Promise(r => element.onended = r),
+ haveError,
+ ]);
+ info(`Playback of recording of ${track.kind} track ${id} ended`);
+ } finally {
+ URL.revokeObjectURL(data);
+ }
+}
+
+runNetworkTest(async options => {
+ // [TODO] re-enable HW decoder after bug 1526207 is fixed.
+ if (navigator.userAgent.includes("Android")) {
+ await pushPrefs(["media.navigator.mediadatadecoder_vpx_enabled", false],
+ ["media.webrtc.hw.h264.enabled", false]);
+ }
+ const test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{video: true}], [{audio: true}]);
+ test.setOfferOptions({offerToReceiveAudio: true});
+ const freshVideoTrackIsAccessible = new Promise(
+ r => test.pcRemote._pc.addEventListener("track", r, {once: true})
+ ).then(({track}) => testTrackAccessible(track));
+ const freshAudioTrackIsAccessible = new Promise(
+ r => test.pcLocal._pc.addEventListener("track", r, {once: true})
+ ).then(({track}) => testTrackAccessible(track));
+ test.chain.append([
+ function PC_CLOSE_TO_END_TRACKS() {
+ return test.close();
+ },
+ async function FRESH_VIDEO_TRACK_IS_ACCESSIBLE() {
+ await freshVideoTrackIsAccessible;
+ ok(true, "A freshly received video track is accessible by MediaRecorder");
+ },
+ async function FRESH_AUDIO_TRACK_IS_ACCESSIBLE() {
+ await freshAudioTrackIsAccessible;
+ ok(true, "A freshly received audio track is accessible by MediaRecorder");
+ },
+ ]);
+ await test.run();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_relayOnly.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_relayOnly.html
new file mode 100644
index 0000000000..3b07783c04
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_relayOnly.html
@@ -0,0 +1,60 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+ bug: "1187775",
+ title: "peer connection ICE fails on relay-only without TURN"
+});
+
+function PC_LOCAL_NO_CANDIDATES(test) {
+ var isnt = can => is(can, null, "No candidates: " + JSON.stringify(can));
+ test.pcLocal._pc.addEventListener("icecandidate", e => isnt(e.candidate));
+}
+
+function PC_BOTH_WAIT_FOR_ICE_FAILED(test) {
+ var isFail = (f, reason, msg) =>
+ f().then(() => { throw new Error(msg + " must fail"); },
+ e => is(e.message, reason, msg + " must fail with: " + e.message));
+
+ return Promise.all([
+ isFail(() => test.pcLocal.waitForIceConnected(), "ICE failed", "Local ICE"),
+ isFail(() => test.pcRemote.waitForIceConnected(), "ICE failed", "Remote ICE")
+ ])
+ .then(() => ok(true, "ICE on both sides must fail."));
+}
+
+runNetworkTest(async options => {
+ await pushPrefs(
+ ['media.peerconnection.ice.stun_client_maximum_transmits', 3],
+ ['media.peerconnection.ice.trickle_grace_period', 5000]
+ );
+ options = options || {};
+ options.config_local = options.config_local || {};
+ const servers = options.config_local.iceServers || [];
+ // remove any turn servers
+ options.config_local.iceServers = servers.filter(server =>
+ server.urls.every(u => !u.toLowerCase().startsWith('turn')));
+
+ // Here's the setting we're testing. Comment out and this test should fail:
+ options.config_local.iceTransportPolicy = "relay";
+
+ const test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{audio: true}, {video: true}],
+ [{audio: true}, {video: true}]);
+ test.chain.remove("PC_LOCAL_SETUP_ICE_LOGGER"); // Needed to suppress failing
+ test.chain.remove("PC_REMOTE_SETUP_ICE_LOGGER"); // on ICE-failure.
+ test.chain.insertAfter("PC_LOCAL_SETUP_ICE_HANDLER", PC_LOCAL_NO_CANDIDATES);
+ test.chain.replace("PC_LOCAL_WAIT_FOR_ICE_CONNECTED", PC_BOTH_WAIT_FOR_ICE_FAILED);
+ test.chain.removeAfter("PC_BOTH_WAIT_FOR_ICE_FAILED");
+ await test.run();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_remoteReofferRollback.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_remoteReofferRollback.html
new file mode 100644
index 0000000000..80aa30beaa
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_remoteReofferRollback.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "952145",
+ title: "Rollback remote reoffer"
+ });
+
+ runNetworkTest(function (options) {
+ const test = new PeerConnectionTest(options);
+ addRenegotiation(test.chain,
+ [
+ function PC_LOCAL_ADD_SECOND_STREAM(test) {
+ test.setMediaConstraints([{audio: true}, {audio: true}],
+ [{audio: true}]);
+ return test.pcLocal.getAllUserMediaAndAddStreams([{audio: true}]);
+ },
+ ]
+ );
+ test.chain.replaceAfter('PC_REMOTE_SET_REMOTE_DESCRIPTION',
+ [
+ function PC_REMOTE_ROLLBACK(test) {
+ return test.setRemoteDescription(test.pcRemote, { type: "rollback" },
+ STABLE);
+ },
+
+ function PC_LOCAL_ROLLBACK(test) {
+ // We haven't negotiated the new stream yet.
+ test.pcLocal.expectNegotiationNeeded();
+ return test.setLocalDescription(
+ test.pcLocal,
+ new RTCSessionDescription({ type: "rollback", sdp: ""}),
+ STABLE);
+ },
+ ],
+ 1 // Second PC_REMOTE_SET_REMOTE_DESCRIPTION
+ );
+ test.chain.append(commandsPeerConnectionOfferAnswer);
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_remoteRollback.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_remoteRollback.html
new file mode 100644
index 0000000000..827646b0de
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_remoteRollback.html
@@ -0,0 +1,51 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "952145",
+ title: "Rollback remote offer"
+ });
+
+ runNetworkTest(function (options) {
+ const test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ test.chain.removeAfter('PC_REMOTE_CHECK_CAN_TRICKLE_SYNC');
+ test.chain.append([
+ function PC_REMOTE_ROLLBACK(test) {
+ // We still haven't negotiated the tracks
+ test.pcRemote.expectNegotiationNeeded();
+ return test.setRemoteDescription(test.pcRemote, { type: "rollback" },
+ STABLE);
+ },
+
+ function PC_REMOTE_CHECK_CAN_TRICKLE_REVERT_SYNC(test) {
+ is(test.pcRemote._pc.canTrickleIceCandidates, null,
+ "Remote canTrickleIceCandidates is reverted to null");
+ },
+
+ function PC_LOCAL_ROLLBACK(test) {
+ // We still haven't negotiated the tracks
+ test.pcLocal.expectNegotiationNeeded();
+ return test.setLocalDescription(
+ test.pcLocal,
+ new RTCSessionDescription({ type: "rollback", sdp: ""}),
+ STABLE);
+ },
+
+ // Rolling back should shut down gathering
+ function PC_LOCAL_WAIT_FOR_END_OF_TRICKLE(test) {
+ return test.pcLocal.endOfTrickleIce;
+ },
+ ]);
+ test.chain.append(commandsPeerConnectionOfferAnswer);
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_removeAudioTrack.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_removeAudioTrack.html
new file mode 100644
index 0000000000..e1e99b38c9
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_removeAudioTrack.html
@@ -0,0 +1,57 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1017888",
+ title: "Renegotiation: remove audio track"
+ });
+
+ runNetworkTest(function (options) {
+ const test = new PeerConnectionTest(options);
+ let receivedTrack, analyser, freq;
+ addRenegotiation(test.chain,
+ [
+ function PC_REMOTE_SETUP_ANALYSER(test) {
+ is(test.pcRemote._pc.getReceivers().length, 1,
+ "pcRemote should have one receiver before renegotiation");
+
+ receivedTrack = test.pcRemote._pc.getReceivers()[0].track;
+ is(receivedTrack.readyState, "live",
+ "The received track should be live");
+
+ analyser = new AudioStreamAnalyser(
+ new AudioContext(), new MediaStream([receivedTrack]));
+ freq = analyser.binIndexForFrequency(TEST_AUDIO_FREQ);
+
+ return analyser.waitForAnalysisSuccess(arr => arr[freq] > 200);
+ },
+ function PC_LOCAL_REMOVE_AUDIO_TRACK(test) {
+ test.setOfferOptions({ offerToReceiveAudio: true });
+ return test.pcLocal.removeSender(0);
+ },
+ ],
+ [
+ function PC_REMOTE_CHECK_FLOW_STOPPED(test) {
+ // Simply removing a track is not enough to cause it to be
+ // signaled as ended. Spec may change though.
+ // TODO: One last check of the spec is in order
+ is(receivedTrack.readyState, "live",
+ "The received track should not have ended");
+
+ return analyser.waitForAnalysisSuccess(arr => arr[freq] < 50);
+ },
+ ]
+ );
+
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_removeThenAddAudioTrack.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_removeThenAddAudioTrack.html
new file mode 100644
index 0000000000..28b76e3b43
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_removeThenAddAudioTrack.html
@@ -0,0 +1,87 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1017888",
+ title: "Renegotiation: remove then add audio track"
+ });
+
+ runNetworkTest(function (options) {
+ const test = new PeerConnectionTest(options);
+ let originalTrack;
+ let haveMuteEvent = new Promise(() => {});
+ let haveUnmuteEvent = new Promise(() => {});
+ addRenegotiation(test.chain,
+ [
+ function PC_REMOTE_FIND_RECEIVER(test) {
+ is(test.pcRemote._pc.getReceivers().length, 1,
+ "pcRemote should have one receiver");
+ originalTrack = test.pcRemote._pc.getReceivers()[0].track;
+ },
+ function PC_LOCAL_REMOVE_AUDIO_TRACK(test) {
+ return test.pcLocal.removeSender(0);
+ },
+ function PC_LOCAL_ADD_AUDIO_TRACK(test) {
+ // The new track's pipeline will start with a packet count of
+ // 0, but the remote side will keep its old pipeline and packet
+ // count.
+ test.pcLocal.disableRtpCountChecking = true;
+ return test.pcLocal.getAllUserMediaAndAddStreams([{audio: true}]);
+ },
+ ],
+ [
+ function PC_REMOTE_WAIT_FOR_UNMUTE() {
+ return haveUnmuteEvent;
+ },
+ function PC_REMOTE_CHECK_ADDED_TRACK(test) {
+ is(test.pcRemote._pc.getTransceivers().length, 2,
+ "pcRemote should have two transceivers");
+ const track = test.pcRemote._pc.getTransceivers()[1].receiver.track;
+
+ const analyser = new AudioStreamAnalyser(
+ new AudioContext(), new MediaStream([track]));
+ const freq = analyser.binIndexForFrequency(TEST_AUDIO_FREQ);
+ return analyser.waitForAnalysisSuccess(arr => arr[freq] > 200);
+ },
+ function PC_REMOTE_WAIT_FOR_MUTE() {
+ return haveMuteEvent;
+ },
+ function PC_REMOTE_CHECK_REMOVED_TRACK(test) {
+ is(test.pcRemote._pc.getTransceivers().length, 2,
+ "pcRemote should have two transceivers");
+ const track = test.pcRemote._pc.getTransceivers()[0].receiver.track;
+
+ const analyser = new AudioStreamAnalyser(
+ new AudioContext(), new MediaStream([track]));
+ const freq = analyser.binIndexForFrequency(TEST_AUDIO_FREQ);
+ return analyser.waitForAnalysisSuccess(arr => arr[freq] < 50);
+ }
+ ]
+ );
+
+ // The first track should mute when the connection is closed.
+ test.chain.insertBefore("PC_REMOTE_SET_REMOTE_DESCRIPTION", [
+ function PC_REMOTE_SETUP_ONMUTE(test) {
+ haveMuteEvent = haveEvent(test.pcRemote._pc.getReceivers()[0].track, "mute");
+ }
+ ]);
+
+ // Second negotiation should cause the second track to unmute.
+ test.chain.insertAfter("PC_REMOTE_SET_REMOTE_DESCRIPTION", [
+ function PC_REMOTE_SETUP_ONUNMUTE(test) {
+ haveUnmuteEvent = haveEvent(test.pcRemote._pc.getReceivers()[1].track, "unmute");
+ }
+ ], false, 1);
+
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_removeThenAddAudioTrackNoBundle.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_removeThenAddAudioTrackNoBundle.html
new file mode 100644
index 0000000000..cff424e12c
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_removeThenAddAudioTrackNoBundle.html
@@ -0,0 +1,76 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1017888",
+ title: "Renegotiation: remove then add audio track"
+ });
+
+ runNetworkTest(function (options) {
+ options = options || { };
+ options.bundle = false;
+ const test = new PeerConnectionTest(options);
+ let originalTrack;
+ addRenegotiation(test.chain,
+ [
+ function PC_REMOTE_FIND_RECEIVER(test) {
+ is(test.pcRemote._pc.getReceivers().length, 1,
+ "pcRemote should have one receiver");
+ originalTrack = test.pcRemote._pc.getReceivers()[0].track;
+ },
+ function PC_LOCAL_REMOVE_AUDIO_TRACK(test) {
+ // The new track's pipeline will start with a packet count of
+ // 0, but the remote side will keep its old pipeline and packet
+ // count.
+ test.pcLocal.disableRtpCountChecking = true;
+ return test.pcLocal.removeSender(0);
+ },
+ function PC_LOCAL_ADD_AUDIO_TRACK(test) {
+ return test.pcLocal.getAllUserMediaAndAddStreams([{audio: true}]);
+ },
+ function PC_LOCAL_EXPECT_ICE_CHECKING(test) {
+ test.pcLocal.expectIceChecking();
+ },
+ function PC_REMOTE_EXPECT_ICE_CHECKING(test) {
+ test.pcRemote.expectIceChecking();
+ },
+ ],
+ [
+ function PC_REMOTE_CHECK_ADDED_TRACK(test) {
+ is(test.pcRemote._pc.getTransceivers().length, 2,
+ "pcRemote should have two transceivers");
+ const track = test.pcRemote._pc.getTransceivers()[1].receiver.track;
+
+ const analyser = new AudioStreamAnalyser(
+ new AudioContext(), new MediaStream([track]));
+ const freq = analyser.binIndexForFrequency(TEST_AUDIO_FREQ);
+ return analyser.waitForAnalysisSuccess(arr => arr[freq] > 200);
+ },
+ function PC_REMOTE_CHECK_REMOVED_TRACK(test) {
+ is(test.pcRemote._pc.getTransceivers().length, 2,
+ "pcRemote should have two transceivers");
+ const track = test.pcRemote._pc.getTransceivers()[0].receiver.track;
+
+ const analyser = new AudioStreamAnalyser(
+ new AudioContext(), new MediaStream([track]));
+ const freq = analyser.binIndexForFrequency(TEST_AUDIO_FREQ);
+ return analyser.waitForAnalysisSuccess(arr => arr[freq] < 50);
+ }
+ ]
+ );
+
+ test.chain.insertAfterEach('PC_LOCAL_CREATE_OFFER',
+ PC_LOCAL_REMOVE_BUNDLE_FROM_OFFER);
+
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_removeThenAddVideoTrack.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_removeThenAddVideoTrack.html
new file mode 100644
index 0000000000..b1be690e5b
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_removeThenAddVideoTrack.html
@@ -0,0 +1,98 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1017888",
+ title: "Renegotiation: remove then add video track"
+ });
+
+ runNetworkTest(async function (options) {
+ // Use fake video here since the native fake device on linux doesn't
+ // change color as needed by checkVideoPlaying() below.
+ await pushPrefs(
+ ['media.video_loopback_dev', ''],
+ ['media.navigator.streams.fake', true]);
+ // [TODO] re-enable HW decoder after bug 1526207 is fixed.
+ if (navigator.userAgent.includes("Android")) {
+ await pushPrefs(["media.navigator.mediadatadecoder_vpx_enabled", false],
+ ["media.webrtc.hw.h264.enabled", false]);
+ }
+
+ const test = new PeerConnectionTest(options);
+ const helper = new VideoStreamHelper();
+ var originalTrack;
+ let haveMuteEvent = new Promise(() => {});
+ let haveUnmuteEvent = new Promise(() => {});
+ addRenegotiation(test.chain,
+ [
+ function PC_REMOTE_FIND_RECEIVER(test) {
+ is(test.pcRemote._pc.getReceivers().length, 1,
+ "pcRemote should have one receiver");
+ originalTrack = test.pcRemote._pc.getReceivers()[0].track;
+ },
+ function PC_LOCAL_REMOVE_VIDEO_TRACK(test) {
+ // The new track's pipeline will start with a packet count of
+ // 0, but the remote side will keep its old pipeline and packet
+ // count.
+ test.pcLocal.disableRtpCountChecking = true;
+ return test.pcLocal.removeSender(0);
+ },
+ function PC_LOCAL_ADD_VIDEO_TRACK(test) {
+ return test.pcLocal.getAllUserMediaAndAddStreams([{video: true}]);
+ },
+ ],
+ [
+ function PC_REMOTE_WAIT_FOR_UNMUTE() {
+ return haveUnmuteEvent;
+ },
+ function PC_REMOTE_CHECK_ADDED_TRACK(test) {
+ is(test.pcRemote._pc.getTransceivers().length, 2,
+ "pcRemote should have two transceivers");
+ const track = test.pcRemote._pc.getTransceivers()[1].receiver.track;
+
+ const vAdded = test.pcRemote.remoteMediaElements.find(
+ elem => elem.id.includes(track.id));
+ return helper.checkVideoPlaying(vAdded);
+ },
+ function PC_REMOTE_WAIT_FOR_MUTE() {
+ return haveMuteEvent;
+ },
+ function PC_REMOTE_CHECK_REMOVED_TRACK(test) {
+ is(test.pcRemote._pc.getTransceivers().length, 2,
+ "pcRemote should have two transceivers");
+ const track = test.pcRemote._pc.getTransceivers()[0].receiver.track;
+
+ const vAdded = test.pcRemote.remoteMediaElements.find(
+ elem => elem.id.includes(track.id));
+ return helper.checkVideoPaused(vAdded, 10, 10, 16, 5000);
+ }
+ ]
+ );
+
+ // The first track should mute when the connection is closed.
+ test.chain.insertBefore("PC_REMOTE_SET_REMOTE_DESCRIPTION", [
+ function PC_REMOTE_SETUP_ONMUTE(test) {
+ haveMuteEvent = haveEvent(test.pcRemote._pc.getReceivers()[0].track, "mute");
+ }
+ ]);
+
+ // Second negotiation should cause the second track to unmute.
+ test.chain.insertAfter("PC_REMOTE_SET_REMOTE_DESCRIPTION", [
+ function PC_REMOTE_SETUP_ONUNMUTE(test) {
+ haveUnmuteEvent = haveEvent(test.pcRemote._pc.getReceivers()[1].track, "unmute");
+ }
+ ], false, 1);
+
+ test.setMediaConstraints([{video: true}], [{video: true}]);
+ await test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_removeThenAddVideoTrackNoBundle.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_removeThenAddVideoTrackNoBundle.html
new file mode 100644
index 0000000000..dcaf7943e2
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_removeThenAddVideoTrackNoBundle.html
@@ -0,0 +1,89 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1017888",
+ title: "Renegotiation: remove then add video track, no bundle"
+ });
+
+ runNetworkTest(async function (options) {
+ // Use fake video here since the native fake device on linux doesn't
+ // change color as needed by checkVideoPlaying() below.
+ await pushPrefs(
+ ['media.video_loopback_dev', ''],
+ ['media.navigator.streams.fake', true]);
+ // [TODO] re-enable HW decoder after bug 1526207 is fixed.
+ if (navigator.userAgent.includes("Android")) {
+ await pushPrefs(["media.navigator.mediadatadecoder_vpx_enabled", false],
+ ["media.webrtc.hw.h264.enabled", false]);
+ }
+
+ options = options || { };
+ options.bundle = false;
+ const test = new PeerConnectionTest(options);
+ const helper = new VideoStreamHelper();
+ var originalTrack;
+ addRenegotiation(test.chain,
+ [
+ function PC_REMOTE_FIND_RECEIVER(test) {
+ is(test.pcRemote._pc.getReceivers().length, 1,
+ "pcRemote should have one receiver");
+ originalTrack = test.pcRemote._pc.getReceivers()[0].track;
+ },
+ function PC_LOCAL_REMOVE_VIDEO_TRACK(test) {
+ // The new track's pipeline will start with a packet count of
+ // 0, but the remote side will keep its old pipeline and packet
+ // count.
+ test.pcLocal.disableRtpCountChecking = true;
+ return test.pcLocal.removeSender(0);
+ },
+ function PC_LOCAL_ADD_VIDEO_TRACK(test) {
+ // Use fake:true here since the native fake device on linux doesn't
+ // change color as needed by checkVideoPlaying() below.
+ return test.pcLocal.getAllUserMediaAndAddStreams([{video: true}]);
+ },
+ function PC_LOCAL_EXPECT_ICE_CHECKING(test) {
+ test.pcLocal.expectIceChecking();
+ },
+ function PC_REMOTE_EXPECT_ICE_CHECKING(test) {
+ test.pcRemote.expectIceChecking();
+ },
+ ],
+ [
+ function PC_REMOTE_CHECK_ADDED_TRACK(test) {
+ is(test.pcRemote._pc.getTransceivers().length, 2,
+ "pcRemote should have two transceivers");
+ const track = test.pcRemote._pc.getTransceivers()[1].receiver.track;
+
+ const vAdded = test.pcRemote.remoteMediaElements.find(
+ elem => elem.id.includes(track.id));
+ return helper.checkVideoPlaying(vAdded);
+ },
+ function PC_REMOTE_CHECK_REMOVED_TRACK(test) {
+ is(test.pcRemote._pc.getTransceivers().length, 2,
+ "pcRemote should have two transceivers");
+ const track = test.pcRemote._pc.getTransceivers()[0].receiver.track;
+
+ const vAdded = test.pcRemote.remoteMediaElements.find(
+ elem => elem.id.includes(track.id));
+ return helper.checkVideoPaused(vAdded, 10, 10, 16, 5000);
+ },
+ ]
+ );
+
+ test.chain.insertAfterEach('PC_LOCAL_CREATE_OFFER',
+ PC_LOCAL_REMOVE_BUNDLE_FROM_OFFER);
+
+ test.setMediaConstraints([{video: true}], [{video: true}]);
+ await test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_removeVideoTrack.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_removeVideoTrack.html
new file mode 100644
index 0000000000..4c4e7905e1
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_removeVideoTrack.html
@@ -0,0 +1,64 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1017888",
+ title: "Renegotiation: remove video track"
+ });
+
+ runNetworkTest(async (options) => {
+ // [TODO] re-enable HW decoder after bug 1526207 is fixed.
+ if (navigator.userAgent.includes("Android")) {
+ await pushPrefs(["media.navigator.mediadatadecoder_vpx_enabled", false],
+ ["media.webrtc.hw.h264.enabled", false]);
+ }
+
+ const test = new PeerConnectionTest(options);
+ let receivedTrack, element;
+ addRenegotiation(test.chain,
+ [
+ function PC_REMOTE_SETUP_HELPER(test) {
+ is(test.pcRemote._pc.getReceivers().length, 1,
+ "pcRemote should have one receiver before renegotiation");
+
+ receivedTrack = test.pcRemote._pc.getReceivers()[0].track;
+ is(receivedTrack.readyState, "live",
+ "The received track should be live");
+
+ element = createMediaElement("video", "pcRemoteReceivedVideo");
+ element.srcObject = new MediaStream([receivedTrack]);
+ return haveEvent(element, "loadeddata");
+ },
+ function PC_LOCAL_REMOVE_VIDEO_TRACK(test) {
+ test.setOfferOptions({ offerToReceiveVideo: true });
+ test.setMediaConstraints([], [{video: true}]);
+ return test.pcLocal.removeSender(0);
+ },
+ ],
+ [
+ function PC_REMOTE_CHECK_FLOW_STOPPED(test) {
+ is(test.pcRemote._pc.getTransceivers().length, 1,
+ "pcRemote should have one transceiver");
+ const track = test.pcRemote._pc.getTransceivers()[0].receiver.track;
+
+ const vAdded = test.pcRemote.remoteMediaElements.find(
+ elem => elem.id.includes(track.id));
+ const helper = new VideoStreamHelper();
+ return helper.checkVideoPaused(vAdded, 10, 10, 16, 5000);
+ },
+ ]
+ );
+
+ test.setMediaConstraints([{video: true}], [{video: true}]);
+ await test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_renderAfterRenegotiation.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_renderAfterRenegotiation.html
new file mode 100644
index 0000000000..c8091d7a9e
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_renderAfterRenegotiation.html
@@ -0,0 +1,89 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1273652",
+ title: "Video receiver still renders after renegotiation",
+ visible: true
+ });
+
+ var pc1 = new RTCPeerConnection();
+ var pc2 = new RTCPeerConnection();
+
+ var add = (pc, can, failed) => can && pc.addIceCandidate(can).catch(failed);
+ pc1.onicecandidate = e => add(pc2, e.candidate, generateErrorCallback());
+ pc2.onicecandidate = e => add(pc1, e.candidate, generateErrorCallback());
+
+ var v1, v2;
+ var delivered = new Promise(resolve => pc2.ontrack = e => {
+ // Test RTCTrackEvent here.
+ ok(e.streams.length, "has streams");
+ ok(e.streams[0].getTrackById(e.track.id), "has track");
+ ok(pc2.getReceivers().some(receiver => receiver == e.receiver), "has receiver");
+ if (e.streams[0].getTracks().length == 1) {
+ // Test RTCTrackEvent required args here.
+ mustThrowWith("RTCTrackEvent wo/required args",
+ "TypeError", () => new RTCTrackEvent("track", {}));
+ v2.srcObject = e.streams[0];
+ resolve();
+ }
+ });
+
+ runNetworkTest(async () => {
+ // [TODO] re-enable HW decoder after bug 1526207 is fixed.
+ if (navigator.userAgent.includes("Android")) {
+ await pushPrefs(["media.navigator.mediadatadecoder_vpx_enabled", false]);
+ await pushPrefs(["media.webrtc.hw.h264.enabled", false]);
+ }
+
+ v2 = createMediaElement('video', 'v2');
+ is(v2.currentTime, 0, "v2.currentTime is zero at outset");
+
+ const emitter = new VideoFrameEmitter(CaptureStreamTestHelper.prototype.blue,
+ CaptureStreamTestHelper.prototype.green,
+ 16, 16);
+ emitter.start();
+ emitter.stream().getTracks().forEach(t => pc1.addTrack(t, emitter.stream()));
+ let h = emitter.helper();
+
+ let offer = await pc1.createOffer({});
+ await pc1.setLocalDescription(offer);
+ await pc2.setRemoteDescription(pc1.localDescription);
+ // check that createAnswer accepts arg.
+ let answer = await pc2.createAnswer({});
+ await pc2.setLocalDescription(answer);
+ await pc1.setRemoteDescription(pc2.localDescription);
+
+ // re-negotiate to trigger the race condition in the jitter buffer
+ offer = await pc1.createOffer({}); // check that createOffer accepts arg.
+ await pc1.setLocalDescription(offer);
+ await pc2.setRemoteDescription(pc1.localDescription);
+ answer = await pc2.createAnswer({});
+ await pc2.setLocalDescription(answer);
+ await pc1.setRemoteDescription(pc2.localDescription);
+ await delivered;
+
+ // now verify that actually something gets rendered into the remote video
+ // element.
+ await h.pixelMustBecome(v2, h.blue, {
+ threshold: 128,
+ infoString: "pcRemote's video should become blue",
+ });
+ // This will verify that new changes to the canvas propagate through
+ // the peerconnection
+ emitter.colors(h.red, h.green)
+ await h.pixelMustBecome(v2, h.red, {
+ threshold: 128,
+ infoString: "pcRemote's video should become red",
+ });
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_replaceNullTrackThenRenegotiateAudio.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_replaceNullTrackThenRenegotiateAudio.html
new file mode 100644
index 0000000000..2253b87672
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_replaceNullTrackThenRenegotiateAudio.html
@@ -0,0 +1,53 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1763832",
+ title: "Renegotiation (audio): Start with no track and recvonly, then replace and set direction to sendrecv, then renegotiate"
+ });
+
+ runNetworkTest(async () => {
+ await pushPrefs(
+ ['media.audio_loopback_dev', ''],
+ ['media.navigator.streams.fake', true]);
+ const offerer = new RTCPeerConnection();
+ const answerer = new RTCPeerConnection();
+ const transceiverSend = offerer.addTransceiver('audio', {direction: 'recvonly'});
+
+ const add = (pc, can, failed) => can && pc.addIceCandidate(can).catch(failed);
+ offerer.onicecandidate = e => add(answerer, e.candidate, generateErrorCallback());
+ answerer.onicecandidate = e => add(offerer, e.candidate, generateErrorCallback());
+
+ await offerer.setLocalDescription();
+ await answerer.setRemoteDescription(offerer.localDescription);
+ await answerer.setLocalDescription();
+ await offerer.setRemoteDescription(answerer.localDescription);
+
+ // add audio with replaceTrack, set send bit, and renegotiate
+ const stream = await navigator.mediaDevices.getUserMedia({audio: true});
+ const [track] = stream.getAudioTracks();
+ transceiverSend.sender.replaceTrack(track);
+ transceiverSend.direction = "sendrecv";
+ const remoteStreamAvailable = new Promise(r => {
+ answerer.ontrack = ({track}) => r(new MediaStream([track]));
+ });
+
+ await offerer.setLocalDescription();
+ await answerer.setRemoteDescription(offerer.localDescription);
+ await answerer.setLocalDescription();
+ await offerer.setRemoteDescription(answerer.localDescription);
+
+ const remoteStream = await remoteStreamAvailable;
+ const h = new AudioStreamHelper();
+ await h.checkAudioFlowing(remoteStream);
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_replaceNullTrackThenRenegotiateVideo.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_replaceNullTrackThenRenegotiateVideo.html
new file mode 100644
index 0000000000..d7bd6d8a37
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_replaceNullTrackThenRenegotiateVideo.html
@@ -0,0 +1,63 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script>
+ <script type="application/javascript" src="simulcast.js"></script></head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1763832",
+ title: "Renegotiation (video): Start with no track and recvonly, then replace and set direction to sendrecv, then renegotiate"
+ });
+
+ runNetworkTest(async () => {
+ await pushPrefs(
+ ['media.video_loopback_dev', ''],
+ ['media.navigator.streams.fake', true]);
+ // [TODO] re-enable HW decoder after bug 1526207 is fixed.
+ if (navigator.userAgent.includes("Android")) {
+ await pushPrefs(["media.navigator.mediadatadecoder_vpx_enabled", false],
+ ["media.webrtc.hw.h264.enabled", false]);
+ }
+
+ const offerer = new RTCPeerConnection();
+ const answerer = new RTCPeerConnection();
+ const transceiverSend = offerer.addTransceiver('video', {direction: 'recvonly'});
+
+ const add = (pc, can, failed) => can && pc.addIceCandidate(can).catch(failed);
+ offerer.onicecandidate = e => add(answerer, e.candidate, generateErrorCallback());
+ answerer.onicecandidate = e => add(offerer, e.candidate, generateErrorCallback());
+
+ await offerer.setLocalDescription();
+ await answerer.setRemoteDescription(offerer.localDescription);
+ await answerer.setLocalDescription();
+ await offerer.setRemoteDescription(answerer.localDescription);
+
+ // add video with replaceTrack, set send bit, and renegotiate
+ const stream = await navigator.mediaDevices.getUserMedia({video: true});
+ const [track] = stream.getVideoTracks();
+ transceiverSend.sender.replaceTrack(track);
+ transceiverSend.direction = "sendrecv";
+ const metadataToBeLoaded = [];
+ answerer.ontrack = (e) => {
+ metadataToBeLoaded.push(getPlaybackWithLoadedMetadata(e.track));
+ };
+
+ await offerer.setLocalDescription();
+ await answerer.setRemoteDescription(offerer.localDescription);
+ await answerer.setLocalDescription();
+ await offerer.setRemoteDescription(answerer.localDescription);
+
+ const elems = await Promise.all(metadataToBeLoaded);
+ is(elems.length, 1, "Should have one video element");
+
+ const helper = new VideoStreamHelper();
+ await helper.checkVideoPlaying(elems[0]);
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_replaceTrack.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_replaceTrack.html
new file mode 100644
index 0000000000..9befc5c564
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_replaceTrack.html
@@ -0,0 +1,187 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1032839",
+ title: "Replace video and audio (with WebAudio) tracks",
+ visible: true
+ });
+
+ function allLocalStreamsHaveSender(pc) {
+ return pc.getLocalStreams()
+ .every(s => s.getTracks() // Every local stream,
+ .some(t => pc.getSenders() // should have some track,
+ .some(sn => sn.track == t))) // that's being sent over |pc|.
+ }
+
+ function allRemoteStreamsHaveReceiver(pc) {
+ return pc.getRemoteStreams()
+ .every(s => s.getTracks() // Every remote stream,
+ .some(t => pc.getReceivers() // should have some track,
+ .some(sn => sn.track == t))) // that's being received over |pc|.
+ }
+
+ function replacetest(wrapper) {
+ var pc = wrapper._pc;
+ var oldSenderCount = pc.getSenders().length;
+ var sender = pc.getSenders().find(sn => sn.track.kind == "video");
+ var oldTrack = sender.track;
+ ok(sender, "We have a sender for video");
+ ok(allLocalStreamsHaveSender(pc),
+ "Shouldn't have any local streams without a corresponding sender");
+ ok(allRemoteStreamsHaveReceiver(pc),
+ "Shouldn't have any remote streams without a corresponding receiver");
+
+ var newTrack;
+ var audiotrack;
+ return getUserMedia({video:true, audio:true})
+ .then(newStream => {
+ window.grip = newStream;
+ newTrack = newStream.getVideoTracks()[0];
+ audiotrack = newStream.getAudioTracks()[0];
+ isnot(newTrack, sender.track, "replacing with a different track");
+ ok(!pc.getLocalStreams().some(s => s == newStream),
+ "from a different stream");
+ // Use wrapper function, since it updates expected tracks
+ return wrapper.senderReplaceTrack(sender, newTrack, newStream);
+ })
+ .then(() => {
+ is(pc.getSenders().length, oldSenderCount, "same sender count");
+ is(sender.track, newTrack, "sender.track has been replaced");
+ ok(!pc.getSenders().map(sn => sn.track).some(t => t == oldTrack),
+ "old track not among senders");
+ // Spec does not say we add this new track to any stream
+ ok(!pc.getLocalStreams().some(s => s.getTracks()
+ .some(t => t == sender.track)),
+ "track does not exist among pc's local streams");
+ return sender.replaceTrack(audiotrack)
+ .then(() => ok(false, "replacing with different kind should fail"),
+ e => is(e.name, "TypeError",
+ "replacing with different kind should fail"));
+ });
+ }
+
+ runNetworkTest(function () {
+ test = new PeerConnectionTest();
+ test.audioCtx = new AudioContext();
+ test.setMediaConstraints([{video: true, audio: true}], [{video: true}]);
+ test.chain.removeAfter("PC_REMOTE_WAIT_FOR_MEDIA_FLOW");
+
+ // Test replaceTrack on pcRemote separately since it's video only.
+ test.chain.append([
+ function PC_REMOTE_VIDEOONLY_REPLACE_VIDEOTRACK(test) {
+ return replacetest(test.pcRemote);
+ },
+ function PC_LOCAL_NEW_VIDEOTRACK_WAIT_FOR_MEDIA_FLOW(test) {
+ return test.pcLocal.waitForMediaFlow();
+ }
+ ]);
+
+ // Replace video twice on pcLocal to make sure it still works
+ // (does audio twice too, but hey)
+ test.chain.append([
+ function PC_LOCAL_AUDIOVIDEO_REPLACE_VIDEOTRACK_1(test) {
+ return replacetest(test.pcLocal);
+ },
+ function PC_REMOTE_NEW_VIDEOTRACK_WAIT_FOR_MEDIA_FLOW_1(test) {
+ return test.pcRemote.waitForMediaFlow();
+ },
+ function PC_LOCAL_AUDIOVIDEO_REPLACE_VIDEOTRACK_2(test) {
+ return replacetest(test.pcLocal);
+ },
+ function PC_REMOTE_NEW_VIDEOTRACK_WAIT_FOR_MEDIA_FLOW_2(test) {
+ return test.pcRemote.waitForMediaFlow();
+ }
+ ]);
+
+ test.chain.append([
+ function PC_LOCAL_AUDIOVIDEO_REPLACE_VIDEOTRACK_WITHSAME(test) {
+ var pc = test.pcLocal._pc;
+ var sender = pc.getSenders().find(sn => sn.track.kind == "video");
+ ok(sender, "should still have a sender of video");
+ return sender.replaceTrack(sender.track)
+ .then(() => ok(true, "replacing with itself should succeed"));
+ },
+ function PC_REMOTE_NEW_SAME_VIDEOTRACK_WAIT_FOR_MEDIA_FLOW(test) {
+ return test.pcRemote.waitForMediaFlow();
+ }
+ ]);
+
+ // Replace the gUM audio track on pcLocal with a WebAudio track.
+ test.chain.append([
+ function PC_LOCAL_AUDIOVIDEO_REPLACE_AUDIOTRACK_WEBAUDIO(test) {
+ var pc = test.pcLocal._pc;
+ var sender = pc.getSenders().find(sn => sn.track.kind == "audio");
+ ok(sender, "track has a sender");
+ var oldSenderCount = pc.getSenders().length;
+ var oldTrack = sender.track;
+
+ var sourceNode = test.audioCtx.createOscillator();
+ sourceNode.type = 'sine';
+ // We need a frequency not too close to the fake audio track
+ // (440Hz for loopback devices, 1kHz for fake tracks).
+ sourceNode.frequency.value = 2000;
+ sourceNode.start();
+
+ var destNode = test.audioCtx.createMediaStreamDestination();
+ sourceNode.connect(destNode);
+ var newTrack = destNode.stream.getAudioTracks()[0];
+
+ return test.pcLocal.senderReplaceTrack(
+ sender, newTrack, destNode.stream)
+ .then(() => {
+ is(pc.getSenders().length, oldSenderCount, "same sender count");
+ ok(!pc.getSenders().some(sn => sn.track == oldTrack),
+ "Replaced track should be removed from senders");
+ // TODO: Should PC remove local streams when there are no senders
+ // associated with it? getLocalStreams() isn't in the spec anymore,
+ // so I guess it is pretty arbitrary?
+ is(sender.track, newTrack, "sender.track has been replaced");
+ // Spec does not say we add this new track to any stream
+ ok(!pc.getLocalStreams().some(s => s.getTracks()
+ .some(t => t == sender.track)),
+ "track exists among pc's local streams");
+ });
+ }
+ ]);
+ test.chain.append([
+ function PC_LOCAL_CHECK_WEBAUDIO_FLOW_PRESENT(test) {
+ return test.pcRemote.checkReceivingToneFrom(test.audioCtx, test.pcLocal);
+ }
+ ]);
+ test.chain.append([
+ function PC_LOCAL_INVALID_ADD_VIDEOTRACKS(test) {
+ let videoTransceivers = test.pcLocal._pc.getTransceivers()
+ .filter(transceiver => {
+ return !transceiver.stopped &&
+ transceiver.receiver.track.kind == "video" &&
+ transceiver.sender.track;
+ });
+
+ ok(videoTransceivers.length,
+ "There is at least one non-stopped video transceiver with a track.");
+
+ videoTransceivers.forEach(transceiver => {
+ var stream = test.pcLocal._pc.getLocalStreams()[0];;
+ var track = transceiver.sender.track;
+ try {
+ test.pcLocal._pc.addTrack(track, stream);
+ ok(false, "addTrack existing track should fail");
+ } catch (e) {
+ is(e.name, "InvalidAccessError",
+ "addTrack existing track should fail");
+ }
+ });
+ }
+ ]);
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_replaceTrack_camera.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_replaceTrack_camera.html
new file mode 100644
index 0000000000..356517e79f
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_replaceTrack_camera.html
@@ -0,0 +1,48 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script>
+</head>
+<body>
+<pre id="test">
+
+<video id="video" width="160" height="120" autoplay></video>
+
+<script type="application/javascript">
+ createHTML({
+ bug: "1709481",
+ title: "replaceTrack (null -> camera) test",
+ visible: true
+ });
+
+ runNetworkTest(async () => {
+ // Make sure we use the fake video device, and not loopback
+ await pushPrefs(
+ ['media.video_loopback_dev', ''],
+ ['media.navigator.streams.fake', true]);
+ const pc1 = new RTCPeerConnection(), pc2 = new RTCPeerConnection();
+ pc1.onicecandidate = e => pc2.addIceCandidate(e.candidate);
+ pc2.onicecandidate = e => pc1.addIceCandidate(e.candidate);
+ pc2.ontrack = ({track}) => video.srcObject = new MediaStream([track]);
+ pc1.addTransceiver("audio");
+ const tc1 = pc1.addTransceiver("video");
+ const stream = await navigator.mediaDevices.getUserMedia({video: true});
+ const [track] = stream.getVideoTracks();
+ await pc1.setLocalDescription();
+ await pc2.setRemoteDescription(pc1.localDescription);
+ await pc2.setLocalDescription();
+ await pc1.setRemoteDescription(pc2.localDescription);
+ await wait(100);
+ await tc1.sender.replaceTrack(track);
+ const h = new VideoStreamHelper();
+ await h.checkVideoPlaying(video);
+ pc1.close();
+ pc2.close();
+ await SpecialPowers.popPrefEnv();
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_replaceTrack_disabled.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_replaceTrack_disabled.html
new file mode 100644
index 0000000000..11b2762d96
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_replaceTrack_disabled.html
@@ -0,0 +1,60 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<script src="pc.js"></script>
+<script src="/tests/dom/canvas/test/captureStream_common.js"></script>
+</head>
+<body>
+<pre id="test">
+<script>
+createHTML({
+ bug: "1576771",
+ title: "Replace a disabled video track with an enabled one",
+ visible: true,
+});
+
+runNetworkTest(() => {
+ const helper = new CaptureStreamTestHelper2D(240, 160);
+ const emitter = new VideoFrameEmitter(helper.green, helper.green, 240, 160);
+ const test = new PeerConnectionTest();
+ test.setMediaConstraints([{video: true}], []);
+ test.chain.insertAfter("PC_REMOTE_WAIT_FOR_MEDIA_FLOW", [
+ function PC_LOCAL_DISABLE_SENDTRACK(test) {
+ test.pcLocal._pc.getSenders()[0].track.enabled = false;
+ },
+ function PC_REMOTE_WAIT_FOR_BLACK(test) {
+ return helper.pixelMustBecome(
+ test.pcRemote.remoteMediaElements[0], helper.black, {
+ threshold: 128,
+ infoString: "Remote disabled track becomes black",
+ cancel: wait(10000).then(
+ () => new Error("Timeout waiting for black"))});
+ },
+ function PC_LOCAL_REPLACETRACK_WITH_ENABLED_TRACK(test) {
+ emitter.start();
+ test.pcLocal._pc.getSenders()[0].replaceTrack(
+ emitter.stream().getTracks()[0]);
+ },
+ ]);
+ test.chain.append([
+ function PC_REMOTE_WAIT_FOR_GREEN(test) {
+ return helper.pixelMustBecome(
+ test.pcRemote.remoteMediaElements[0], helper.green, {
+ threshold: 128,
+ infoString: "Remote disabled track becomes green",
+ cancel: wait(10000).then(
+ () => new Error("Timeout waiting for green"))});
+ },
+ function CLEANUP(test) {
+ emitter.stop();
+ for (const track of emitter.stream().getTracks()) {
+ track.stop();
+ }
+ },
+ ]);
+ return test.run();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_replaceTrack_microphone.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_replaceTrack_microphone.html
new file mode 100644
index 0000000000..5886caf6a4
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_replaceTrack_microphone.html
@@ -0,0 +1,46 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script>
+</head>
+<body>
+<pre id="test">
+
+<script type="application/javascript">
+ createHTML({
+ bug: "1709481",
+ title: "replaceTrack (null -> microphone) test",
+ visible: true
+ });
+
+ runNetworkTest(async () => {
+ // Make sure we use the fake audio device, and not loopback
+ await pushPrefs(
+ ['media.audio_loopback_dev', ''],
+ ['media.navigator.streams.fake', true]);
+ const pc1 = new RTCPeerConnection(), pc2 = new RTCPeerConnection();
+ pc1.onicecandidate = e => pc2.addIceCandidate(e.candidate);
+ pc2.onicecandidate = e => pc1.addIceCandidate(e.candidate);
+ let remoteStream;
+ pc2.ontrack = ({track}) => remoteStream = new MediaStream([track]);
+ const tc1 = pc1.addTransceiver("audio");
+ const stream = await navigator.mediaDevices.getUserMedia({audio: true});
+ const [track] = stream.getAudioTracks();
+ await pc1.setLocalDescription();
+ await pc2.setRemoteDescription(pc1.localDescription);
+ await pc2.setLocalDescription();
+ await pc1.setRemoteDescription(pc2.localDescription);
+ await wait(100);
+ await tc1.sender.replaceTrack(track);
+ const h = new AudioStreamHelper();
+ await h.checkAudioFlowing(remoteStream);
+ pc1.close();
+ pc2.close();
+ await SpecialPowers.popPrefEnv();
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_replaceVideoThenRenegotiate.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_replaceVideoThenRenegotiate.html
new file mode 100644
index 0000000000..070cb42fcb
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_replaceVideoThenRenegotiate.html
@@ -0,0 +1,74 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1017888",
+ title: "Renegotiation: replaceTrack followed by adding a second video stream"
+ });
+
+ runNetworkTest(async (options) => {
+ await pushPrefs(['media.peerconnection.video.min_bitrate_estimate', 180*1000]);
+ // [TODO] re-enable HW decoder after bug 1526207 is fixed.
+ if (navigator.userAgent.includes("Android")) {
+ await pushPrefs(["media.navigator.mediadatadecoder_vpx_enabled", false],
+ ["media.webrtc.hw.h264.enabled", false]);
+ }
+
+ const test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{video:true}], [{video:true}]);
+ const helper = new VideoStreamHelper();
+ const emitter1 = new VideoFrameEmitter(CaptureStreamTestHelper.prototype.red,
+ CaptureStreamTestHelper.prototype.green);
+ const emitter2 = new VideoFrameEmitter(CaptureStreamTestHelper.prototype.blue,
+ CaptureStreamTestHelper.prototype.grey);
+ test.chain.replace("PC_LOCAL_GUM", [
+ function PC_LOCAL_ADDTRACK(test) {
+ test.pcLocal.attachLocalStream(emitter1.stream());
+ emitter1.start();
+ },
+ ]);
+ addRenegotiation(test.chain,
+ [
+ function PC_LOCAL_REPLACE_VIDEO_TRACK_THEN_ADD_SECOND_STREAM(test) {
+ emitter1.stop();
+ emitter2.start();
+ const newstream = emitter2.stream();
+ const newtrack = newstream.getVideoTracks()[0];
+ var sender = test.pcLocal._pc.getSenders()[0];
+ return test.pcLocal.senderReplaceTrack(sender, newtrack, newstream)
+ .then(() => {
+ test.setMediaConstraints([{video: true}, {video: true}],
+ [{video: true}]);
+ });
+ },
+ ],
+ [
+ function PC_REMOTE_CHECK_ORIGINAL_TRACK_NOT_ENDED(test) {
+ is(test.pcRemote._pc.getTransceivers().length, 1,
+ "pcRemote should have one transceiver");
+ const track = test.pcRemote._pc.getTransceivers()[0].receiver.track;
+
+ const vremote = test.pcRemote.remoteMediaElements.find(
+ elem => elem.id.includes(track.id));
+ if (!vremote) {
+ return Promise.reject(new Error("Couldn't find video element"));
+ }
+ ok(!vremote.ended, "Original track should not have ended after renegotiation (replaceTrack is not signalled!)");
+ return helper.checkVideoPlaying(vremote);
+ }
+ ]
+ );
+
+ await test.run();
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIce.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIce.html
new file mode 100644
index 0000000000..d94bb084b7
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIce.html
@@ -0,0 +1,41 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "906986",
+ title: "Renegotiation: restart ice"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ test = new PeerConnectionTest(options);
+
+ addRenegotiation(test.chain,
+ [
+ // causes a full, normal ice restart
+ function PC_LOCAL_SET_OFFER_OPTION(test) {
+ test.setOfferOptions({ iceRestart: true });
+ },
+ function PC_LOCAL_EXPECT_ICE_CHECKING(test) {
+ test.pcLocal.expectIceChecking();
+ },
+ function PC_REMOTE_EXPECT_ICE_CHECKING(test) {
+ test.pcRemote.expectIceChecking();
+ }
+ ]
+ );
+
+ test.setMediaConstraints([{audio: true}, {video: true}],
+ [{audio: true}, {video: true}]);
+ return test.run();
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceBadAnswer.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceBadAnswer.html
new file mode 100644
index 0000000000..b71001b0db
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceBadAnswer.html
@@ -0,0 +1,58 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1413709",
+ title: "Renegotiation: bad answer ICE credentials"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ test = new PeerConnectionTest(options);
+
+ addRenegotiation(test.chain,
+ [
+ function PC_LOCAL_ADD_SECOND_STREAM(test) {
+ test.setMediaConstraints([{audio: true}],
+ []);
+ return test.pcLocal.getAllUserMedia([{audio: true}]);
+ },
+ ]
+ );
+
+ // If the offerer hasn't indicated ICE restart, then an answer
+ // arriving during renegotiation that has modified ICE credentials
+ // should cause an error
+ test.chain.replaceAfter("PC_LOCAL_GET_ANSWER",
+ [
+ function PC_LOCAL_REWRITE_REMOTE_SDP_ICE_CREDS(test) {
+ test._remote_answer.sdp =
+ test._remote_answer.sdp.replace(/a=ice-pwd:.*\r\n/g,
+ "a=ice-pwd:bad-pwd\r\n")
+ .replace(/a=ice-ufrag:.*\r\n/g,
+ "a=ice-ufrag:bad-ufrag\r\n");
+ },
+
+ function PC_LOCAL_EXPECT_SET_REMOTE_DESCRIPTION_FAIL(test) {
+ return test.setRemoteDescription(test.pcLocal,
+ test._remote_answer,
+ STABLE)
+ .then(() => ok(false, "setRemoteDescription must fail"),
+ e => is(e.name, "InvalidAccessError",
+ "setRemoteDescription must fail and did"));
+ }
+ ], 1 // replace after the second PC_LOCAL_GET_ANSWER
+ );
+
+ test.setMediaConstraints([{audio: true}], []);
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceLocalAndRemoteRollback.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceLocalAndRemoteRollback.html
new file mode 100644
index 0000000000..6bbf9440fc
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceLocalAndRemoteRollback.html
@@ -0,0 +1,82 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "906986",
+ title: "Renegotiation: restart ice, local and remote rollback"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ test = new PeerConnectionTest(options);
+
+ addRenegotiation(test.chain,
+ [
+ async function PC_LOCAL_SETUP_ICE_HANDLER(test) {
+ await test.pcLocal.endOfTrickleIce;
+ test.pcLocal.setupIceCandidateHandler(test);
+ },
+
+ // causes a full, normal ice restart
+ function PC_LOCAL_SET_OFFER_OPTION(test) {
+ test.setOfferOptions({ iceRestart: true });
+ }
+ ]
+ );
+
+ test.chain.replaceAfter('PC_REMOTE_CREATE_ANSWER',
+ [
+ function PC_LOCAL_EXPECT_ICE_CONNECTED(test) {
+ test.pcLocal.iceCheckingIceRollbackExpected = true;
+ },
+
+ function PC_REMOTE_ROLLBACK(test) {
+ return test.setRemoteDescription(test.pcRemote, { type: "rollback" },
+ STABLE);
+ },
+
+ async function PC_LOCAL_ROLLBACK(test) {
+ await test.pcLocal.endOfTrickleIce;
+ // We haven't negotiated the new stream yet.
+ test.pcLocal.expectNegotiationNeeded();
+ return test.setLocalDescription(
+ test.pcLocal,
+ new RTCSessionDescription({ type: "rollback", sdp: ""}),
+ STABLE);
+ },
+
+ // Rolling back should shut down gathering for the offerer,
+ // but because the answerer never set a local description, no ICE
+ // gathering has happened yet, so there's no changes to ICE gathering
+ // state
+ function PC_LOCAL_WAIT_FOR_END_OF_TRICKLE(test) {
+ return test.pcLocal.endOfTrickleIce;
+ },
+
+ function PC_LOCAL_EXPECT_ICE_CHECKING(test) {
+ test.pcLocal.expectIceChecking();
+ },
+ function PC_REMOTE_EXPECT_ICE_CHECKING(test) {
+ test.pcRemote.expectIceChecking();
+ }
+ ],
+ 1 // Replaces after second PC_REMOTE_CREATE_ANSWER
+ );
+ test.chain.append(commandsPeerConnectionOfferAnswer);
+
+ // for now, only use one stream, because rollback doesn't seem to
+ // like multiple streams. See bug 1259465.
+ test.setMediaConstraints([{audio: true}],
+ [{audio: true}]);
+ return test.run();
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceLocalAndRemoteRollbackNoSubsequentRestart.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceLocalAndRemoteRollbackNoSubsequentRestart.html
new file mode 100644
index 0000000000..37b0fc68fc
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceLocalAndRemoteRollbackNoSubsequentRestart.html
@@ -0,0 +1,77 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "906986",
+ title: "Renegotiation: restart ice, local and remote rollback, without a subsequent ICE restart"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ test = new PeerConnectionTest(options);
+
+ addRenegotiation(test.chain,
+ [
+ function PC_LOCAL_SETUP_ICE_HANDLER(test) {
+ test.pcLocal.setupIceCandidateHandler(test);
+ },
+ function PC_REMOTE_SETUP_ICE_HANDLER(test) {
+ test.pcRemote.setupIceCandidateHandler(test);
+ },
+
+ // causes a full, normal ice restart
+ function PC_LOCAL_SET_OFFER_OPTION(test) {
+ test.setOfferOptions({ iceRestart: true });
+ }
+ ]
+ );
+
+ test.chain.replaceAfter('PC_REMOTE_CREATE_ANSWER',
+ [
+ function PC_REMOTE_ROLLBACK(test) {
+ return test.setRemoteDescription(test.pcRemote, { type: "rollback" },
+ STABLE);
+ },
+
+ async function PC_LOCAL_ROLLBACK(test) {
+ await test.pcLocal.endOfTrickleIce;
+ // We haven't negotiated the new stream yet.
+ test.pcLocal.expectNegotiationNeeded();
+ return test.setLocalDescription(
+ test.pcLocal,
+ new RTCSessionDescription({ type: "rollback", sdp: ""}),
+ STABLE);
+ },
+
+ // Rolling back should shut down gathering for the offerer,
+ // but because the answerer never set a local description, no ICE
+ // gathering has happened yet, so there's no changes to ICE gathering
+ // state
+ function PC_LOCAL_WAIT_FOR_END_OF_TRICKLE(test) {
+ return test.pcLocal.endOfTrickleIce;
+ },
+
+ function PC_LOCAL_SET_OFFER_OPTION(test) {
+ test.setOfferOptions({ iceRestart: false });
+ }
+ ],
+ 1 // Replaces after second PC_REMOTE_CREATE_ANSWER
+ );
+ test.chain.append(commandsPeerConnectionOfferAnswer);
+
+ // for now, only use one stream, because rollback doesn't seem to
+ // like multiple streams. See bug 1259465.
+ test.setMediaConstraints([{audio: true}],
+ [{audio: true}]);
+ return test.run();
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceLocalRollback.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceLocalRollback.html
new file mode 100644
index 0000000000..f5f9a1f220
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceLocalRollback.html
@@ -0,0 +1,76 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "906986",
+ title: "Renegotiation: restart ice, local rollback"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ test = new PeerConnectionTest(options);
+
+ addRenegotiation(test.chain,
+ [
+ // causes a full, normal ice restart
+ function PC_LOCAL_SET_OFFER_OPTION(test) {
+ test.setOfferOptions({ iceRestart: true });
+ },
+ // causes an ice restart and then rolls it back
+ // (does not result in sending an offer)
+ function PC_LOCAL_SETUP_ICE_HANDLER(test) {
+ test.pcLocal.setupIceCandidateHandler(test, () => {});
+ },
+ function PC_LOCAL_CREATE_AND_SET_OFFER(test) {
+ return test.createOffer(test.pcLocal).then(offer => {
+ return test.setLocalDescription(test.pcLocal,
+ offer,
+ HAVE_LOCAL_OFFER);
+ });
+ },
+ function PC_LOCAL_EXPECT_ICE_CONNECTED(test) {
+ test.pcLocal.iceCheckingIceRollbackExpected = true;
+ },
+ function PC_LOCAL_WAIT_FOR_GATHERING(test) {
+ return new Promise(r => {
+ test.pcLocal._pc.addEventListener("icegatheringstatechange", () => {
+ if (test.pcLocal._pc.iceGatheringState == "gathering") {
+ r();
+ }
+ });
+ });
+ },
+ function PC_LOCAL_ROLLBACK(test) {
+ return test.setLocalDescription(test.pcLocal,
+ { type: "rollback", sdp: ""},
+ STABLE);
+ },
+ // Rolling back should shut down gathering
+ function PC_LOCAL_WAIT_FOR_END_OF_TRICKLE(test) {
+ return test.pcLocal.endOfTrickleIce;
+ },
+ function PC_LOCAL_EXPECT_ICE_CHECKING(test) {
+ test.pcLocal.expectIceChecking();
+ },
+ function PC_REMOTE_EXPECT_ICE_CHECKING(test) {
+ test.pcRemote.expectIceChecking();
+ }
+ ]
+ );
+
+ // for now, only use one stream, because rollback doesn't seem to
+ // like multiple streams. See bug 1259465.
+ test.setMediaConstraints([{audio: true}],
+ [{audio: true}]);
+ return test.run();
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceLocalRollbackNoSubsequentRestart.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceLocalRollbackNoSubsequentRestart.html
new file mode 100644
index 0000000000..8e27864aae
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceLocalRollbackNoSubsequentRestart.html
@@ -0,0 +1,60 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "906986",
+ title: "Renegotiation: restart ice, local rollback, then renegotiation without ICE restart"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ test = new PeerConnectionTest(options);
+
+ addRenegotiation(test.chain,
+ [
+ // causes a full, normal ice restart
+ function PC_LOCAL_SET_OFFER_OPTION(test) {
+ test.setOfferOptions({ iceRestart: true });
+ },
+ // causes an ice restart and then rolls it back
+ // (does not result in sending an offer)
+ function PC_LOCAL_SETUP_ICE_HANDLER(test) {
+ test.pcLocal.setupIceCandidateHandler(test, () => {});
+ },
+ function PC_LOCAL_CREATE_AND_SET_OFFER(test) {
+ return test.createOffer(test.pcLocal).then(offer => {
+ return test.setLocalDescription(test.pcLocal,
+ offer,
+ HAVE_LOCAL_OFFER);
+ });
+ },
+ function PC_LOCAL_WAIT_FOR_END_OF_TRICKLE(test) {
+ return test.pcLocal.endOfTrickleIce;
+ },
+ function PC_LOCAL_ROLLBACK(test) {
+ return test.setLocalDescription(test.pcLocal,
+ { type: "rollback", sdp: ""},
+ STABLE);
+ },
+ function PC_LOCAL_SET_OFFER_OPTION(test) {
+ test.setOfferOptions({ iceRestart: false });
+ }
+ ]
+ );
+
+ // for now, only use one stream, because rollback doesn't seem to
+ // like multiple streams. See bug 1259465.
+ test.setMediaConstraints([{audio: true}],
+ [{audio: true}]);
+ return test.run();
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceNoBundle.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceNoBundle.html
new file mode 100644
index 0000000000..134fa97cc0
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceNoBundle.html
@@ -0,0 +1,43 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "906986",
+ title: "Renegotiation: restart ice, no bundle"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ options = options || { };
+ options.bundle = false;
+ test = new PeerConnectionTest(options);
+
+ addRenegotiation(test.chain,
+ [
+ // causes a full, normal ice restart
+ function PC_LOCAL_SET_OFFER_OPTION(test) {
+ test.setOfferOptions({ iceRestart: true });
+ },
+ function PC_LOCAL_EXPECT_ICE_CHECKING(test) {
+ test.pcLocal.expectIceChecking();
+ },
+ function PC_REMOTE_EXPECT_ICE_CHECKING(test) {
+ test.pcRemote.expectIceChecking();
+ }
+ ]
+ );
+
+ test.setMediaConstraints([{audio: true}, {video: true}],
+ [{audio: true}, {video: true}]);
+ return test.run();
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceNoBundleNoRtcpMux.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceNoBundleNoRtcpMux.html
new file mode 100644
index 0000000000..06a3a3c980
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceNoBundleNoRtcpMux.html
@@ -0,0 +1,44 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "906986",
+ title: "Renegotiation: restart ice, no bundle and disabled RTCP-Mux"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ options = options || { };
+ options.bundle = false;
+ options.rtcpmux = false;
+ test = new PeerConnectionTest(options);
+
+ addRenegotiation(test.chain,
+ [
+ // causes a full, normal ice restart
+ function PC_LOCAL_SET_OFFER_OPTION(test) {
+ test.setOfferOptions({ iceRestart: true });
+ },
+ function PC_LOCAL_EXPECT_ICE_CHECKING(test) {
+ test.pcLocal.expectIceChecking();
+ },
+ function PC_REMOTE_EXPECT_ICE_CHECKING(test) {
+ test.pcRemote.expectIceChecking();
+ }
+ ]
+ );
+
+ test.setMediaConstraints([{audio: true}, {video: true}],
+ [{audio: true}, {video: true}]);
+ return test.run();
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceNoRtcpMux.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceNoRtcpMux.html
new file mode 100644
index 0000000000..5d4780211a
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceNoRtcpMux.html
@@ -0,0 +1,43 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "906986",
+ title: "Renegotiation: restart ice, with disabled RTCP-Mux"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ options = options || { };
+ options.rtcpmux = false;
+ test = new PeerConnectionTest(options);
+
+ addRenegotiation(test.chain,
+ [
+ // causes a full, normal ice restart
+ function PC_LOCAL_SET_OFFER_OPTION(test) {
+ test.setOfferOptions({ iceRestart: true });
+ },
+ function PC_LOCAL_EXPECT_ICE_CHECKING(test) {
+ test.pcLocal.expectIceChecking();
+ },
+ function PC_REMOTE_EXPECT_ICE_CHECKING(test) {
+ test.pcRemote.expectIceChecking();
+ }
+ ]
+ );
+
+ test.setMediaConstraints([{audio: true}, {video: true}],
+ [{audio: true}, {video: true}]);
+ return test.run();
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_restrictBandwidthTargetBitrate.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_restrictBandwidthTargetBitrate.html
new file mode 100644
index 0000000000..ff9fb1fc22
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_restrictBandwidthTargetBitrate.html
@@ -0,0 +1,29 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "Bug 1404250",
+ title: "Extremely bitrate restricted video-only peer connection"
+ });
+
+ runNetworkTest(function (options) {
+ const test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{video: true}], [{video: true}]);
+ test.chain.insertAfter('PC_REMOTE_GET_OFFER', [
+ function PC_REMOTE_ADD_TIAS(test) {
+ test._local_offer.sdp = sdputils.addTiasBps(
+ test._local_offer.sdp, 25000);
+ info("Offer with TIAS: " + JSON.stringify(test._local_offer));
+ }
+ ]);
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_restrictBandwidthWithTias.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_restrictBandwidthWithTias.html
new file mode 100644
index 0000000000..85b831e9a8
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_restrictBandwidthWithTias.html
@@ -0,0 +1,30 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1359854",
+ title: "500Kb restricted video-only peer connection"
+ });
+
+ runNetworkTest(function (options) {
+ const test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{video: true}], [{video: true}]);
+ test.chain.insertAfter('PC_REMOTE_GET_OFFER', [
+ function PC_REMOTE_ADD_TIAS(test) {
+ test._local_offer.sdp = sdputils.addTiasBps(
+ test._local_offer.sdp, 250000);
+ info("Offer with TIAS: " + JSON.stringify(test._local_offer));
+ }
+ ]);
+ // TODO it would be nice to verify the used bandwidth
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_rtcp_rsize.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_rtcp_rsize.html
new file mode 100644
index 0000000000..25270984ea
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_rtcp_rsize.html
@@ -0,0 +1,81 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="stats.js"></script>
+ <script type="application/javascript" src="sdpUtils.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1279153",
+ title: "rtcp-rsize",
+ visible: true
+ });
+
+ // 0) Use webrtc-sdp
+ // 1) ADD RTCP-RISZE to all video m-sections
+ // 2) Check for RTCP-RSIZE in ANSWER
+ // 3) Wait for media to flow
+ // 4) Wait for RTCP stats
+
+ runNetworkTest(async function (options) {
+ const test = new PeerConnectionTest(options);
+
+ let mSectionsAltered = 0;
+
+ test.chain.insertAfter("PC_LOCAL_CREATE_OFFER", [
+ function PC_LOCAL_ADD_RTCP_RSIZE(test) {
+ const lines = test.originalOffer.sdp.split("\r\n");
+ info(`SDP before rtcp-rsize: ${lines.join('\n')}`);
+ // Insert an rtcp-rsize for each m section
+ const rsizeAdded = lines.flatMap(line => {
+ if (line.startsWith("m=video")) {
+ mSectionsAltered = mSectionsAltered + 1;
+ return [line, "a=rtcp-rsize"];
+ }
+ return [line];
+ });
+ test.originalOffer.sdp = rsizeAdded.join("\r\n");
+ info(`SDP with rtcp-rsize: ${rsizeAdded.join("\n")}`);
+ is(mSectionsAltered, 1, "We only altered 1 msection")
+ }]);
+
+ // Check that the rtcp-rsize makes into the answer
+ test.chain.insertAfter("PC_LOCAL_SET_REMOTE_DESCRIPTION", [
+ function PC_LOCAL_CHECK_RTCP_RSIZE(test) {
+ const msections = sdputils.getMSections(test.pcLocal._pc.currentRemoteDescription.sdp);
+ var alteredMSectionsFound = 0;
+ for (msection of msections) {
+ if (msection.startsWith("m=video")) {
+ ok(msection.includes("\r\na=rtcp-rsize\r\n"), "video m-section includes RTCP-RSIZE");
+ alteredMSectionsFound = alteredMSectionsFound + 1;
+ } else {
+ ok(!msection.includes("\r\na=rtcp-rsize\r\n"), "audio m-section does not include RTCP-RSIZE");
+ }
+ }
+ is(alteredMSectionsFound, mSectionsAltered, "correct number of msections found");
+ }
+ ]);
+
+ // Make sure that we are still getting RTCP stats
+ test.chain.insertAfter("PC_REMOTE_WAIT_FOR_MEDIA_FLOW",
+ async function PC_LOCAL_AND_REMOTE_CHECK_FOR_RTCP_STATS(test) {
+ await Promise.all([
+ waitForSyncedRtcp(test.pcLocal._pc),
+ waitForSyncedRtcp(test.pcRemote._pc),
+ ]);
+ // The work is done by waitForSyncedRtcp which will throw if
+ // RTCP stats are not received.
+ info("RTCP stats received!");
+ },
+ );
+ test.setMediaConstraints([{audio: true}, {video: true}], []);
+ await test.run();
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_scaleResolution.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_scaleResolution.html
new file mode 100644
index 0000000000..4be6873fa6
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_scaleResolution.html
@@ -0,0 +1,119 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1244913",
+ title: "Scale resolution down on a PeerConnection",
+ visible: true
+ });
+
+ async function checkForH264Support() {
+ const pc = new RTCPeerConnection();
+ const offer = await pc.createOffer({offerToReceiveVideo: true});
+ return offer.sdp.match(/a=rtpmap:[1-9][0-9]* H264/);
+ }
+
+ let resolutionAlignment = 1;
+
+ var mustRejectWith = (msg, reason, f) =>
+ f().then(() => ok(false, msg),
+ e => is(e.name, reason, msg));
+
+ async function testScale(codec) {
+ var pc1 = new RTCPeerConnection();
+ var pc2 = new RTCPeerConnection();
+
+ var add = (pc, can, failed) => can && pc.addIceCandidate(can).catch(failed);
+ pc1.onicecandidate = e => add(pc2, e.candidate, generateErrorCallback());
+ pc2.onicecandidate = e => add(pc1, e.candidate, generateErrorCallback());
+
+ info("testing scaling with " + codec);
+
+ let stream = await navigator.mediaDevices.getUserMedia({ video: true });
+
+ var v1 = createMediaElement('video', 'v1');
+ var v2 = createMediaElement('video', 'v2');
+
+ var ontrackfired = new Promise(resolve => pc2.ontrack = e => resolve(e));
+ var v2loadedmetadata = new Promise(resolve => v2.onloadedmetadata = resolve);
+
+ is(v2.currentTime, 0, "v2.currentTime is zero at outset");
+
+ v1.srcObject = stream;
+ var sender = pc1.addTrack(stream.getVideoTracks()[0], stream);
+ let parameters = sender.getParameters();
+ is(parameters.encodings.length, 1, "Default number of encodings should be 1");
+ parameters.encodings[0].scaleResolutionDownBy = 0.5;
+
+ await mustRejectWith(
+ "Invalid scaleResolutionDownBy must reject", "RangeError",
+ () => sender.setParameters(parameters)
+ );
+
+ parameters = sender.getParameters();
+ parameters.encodings[0].scaleResolutionDownBy = 2;
+ parameters.encodings[0].maxBitrate = 60000;
+
+ await sender.setParameters(parameters);
+
+ parameters = sender.getParameters();
+ is(parameters.encodings[0].scaleResolutionDownBy, 2, "Should be able to set scaleResolutionDownBy");
+ is(parameters.encodings[0].maxBitrate, 60000, "Should be able to set maxBitrate");
+
+ let offer = await pc1.createOffer();
+ if (codec == "VP8") {
+ offer.sdp = sdputils.removeAllButPayloadType(offer.sdp, 126);
+ }
+ await pc1.setLocalDescription(offer);
+ await pc2.setRemoteDescription(pc1.localDescription);
+
+ let answer = await pc2.createAnswer();
+ await pc2.setLocalDescription(answer);
+ await pc1.setRemoteDescription(pc2.localDescription);
+ let trackevent = await ontrackfired;
+
+ v2.srcObject = trackevent.streams[0];
+
+ await v2loadedmetadata;
+
+ await waitUntil(() => v2.currentTime > 0);
+ ok(v2.currentTime > 0, "v2.currentTime is moving (" + v2.currentTime + ")");
+
+ ok(v1.videoWidth > 0, "source width is positive");
+ ok(v1.videoHeight > 0, "source height is positive");
+ const expectedWidth =
+ v1.videoWidth / 2 - (v1.videoWidth / 2 % resolutionAlignment);
+ const expectedHeight =
+ v1.videoHeight / 2 - (v1.videoHeight / 2 % resolutionAlignment);
+ is(v2.videoWidth, expectedWidth,
+ "sink is half the width of the source");
+ is(v2.videoHeight, expectedHeight,
+ "sink is half the height of the source");
+ stream.getTracks().forEach(track => track.stop());
+ v1.srcObject = v2.srcObject = null;
+ pc1.close()
+ pc2.close()
+ }
+
+ runNetworkTest(async () => {
+ await matchPlatformH264CodecPrefs();
+ const hasH264 = await checkForH264Support();
+ if (hasH264 && navigator.userAgent.includes("Android")) {
+ // Android only has a hw encoder for h264
+ resolutionAlignment = 16;
+ }
+ await pushPrefs(['media.peerconnection.video.lock_scaling', true]);
+ await testScale("VP8");
+ if (hasH264) {
+ await testScale("H264");
+ }
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_scaleResolution_oldSetParameters.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_scaleResolution_oldSetParameters.html
new file mode 100644
index 0000000000..85a989ba32
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_scaleResolution_oldSetParameters.html
@@ -0,0 +1,122 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1244913",
+ title: "Scale resolution down on a PeerConnection",
+ visible: true
+ });
+
+ async function checkForH264Support() {
+ const pc = new RTCPeerConnection();
+ const offer = await pc.createOffer({offerToReceiveVideo: true});
+ return offer.sdp.match(/a=rtpmap:[1-9][0-9]* H264/);
+ }
+
+ let resolutionAlignment = 1;
+
+ var mustRejectWith = (msg, reason, f) =>
+ f().then(() => ok(false, msg),
+ e => is(e.name, reason, msg));
+
+ async function testScale(codec) {
+ var pc1 = new RTCPeerConnection();
+ var pc2 = new RTCPeerConnection();
+
+ var add = (pc, can, failed) => can && pc.addIceCandidate(can).catch(failed);
+ pc1.onicecandidate = e => add(pc2, e.candidate, generateErrorCallback());
+ pc2.onicecandidate = e => add(pc1, e.candidate, generateErrorCallback());
+
+ info("testing scaling with " + codec);
+
+ let stream = await navigator.mediaDevices.getUserMedia({ video: true });
+
+ var v1 = createMediaElement('video', 'v1');
+ var v2 = createMediaElement('video', 'v2');
+
+ var ontrackfired = new Promise(resolve => pc2.ontrack = e => resolve(e));
+ var v2loadedmetadata = new Promise(resolve => v2.onloadedmetadata = resolve);
+
+ is(v2.currentTime, 0, "v2.currentTime is zero at outset");
+
+ v1.srcObject = stream;
+ var sender = pc1.addTrack(stream.getVideoTracks()[0], stream);
+
+ const otherErrorStart = await GleanTest.rtcrtpsenderSetparameters.failOther.testGetValue();
+ const noTransactionIdWarningStart = await GleanTest.rtcrtpsenderSetparameters.warnNoTransactionid.testGetValue();
+
+ await mustRejectWith(
+ "Invalid scaleResolutionDownBy must reject", "RangeError",
+ () => sender.setParameters(
+ { encodings:[{ scaleResolutionDownBy: 0.5 } ] })
+ );
+
+ const otherErrorEnd = await GleanTest.rtcrtpsenderSetparameters.failOther.testGetValue();
+ const noTransactionIdWarningEnd = await GleanTest.rtcrtpsenderSetparameters.warnNoTransactionid.testGetValue();
+
+ // Make sure Glean is recording these statistics
+ is(otherErrorEnd.denominator, otherErrorStart.denominator, "No new RTCRtpSenders were created during this time");
+ is(otherErrorEnd.numerator, otherErrorStart.numerator + 1, "RTCRtpSender.setParameters reported a failure via Glean");
+ is(noTransactionIdWarningEnd.denominator, noTransactionIdWarningStart.denominator, "No new RTCRtpSenders were created during this time");
+ is(noTransactionIdWarningEnd.numerator, noTransactionIdWarningStart.numerator + 1, "Glean should have recorded a warning due to missing transactionId");
+
+ await sender.setParameters({ encodings: [{ maxBitrate: 60000,
+ scaleResolutionDownBy: 2 }] });
+
+ let offer = await pc1.createOffer();
+ if (codec == "VP8") {
+ offer.sdp = sdputils.removeAllButPayloadType(offer.sdp, 126);
+ }
+ await pc1.setLocalDescription(offer);
+ await pc2.setRemoteDescription(pc1.localDescription);
+
+ let answer = await pc2.createAnswer();
+ await pc2.setLocalDescription(answer);
+ await pc1.setRemoteDescription(pc2.localDescription);
+ let trackevent = await ontrackfired;
+
+ v2.srcObject = trackevent.streams[0];
+
+ await v2loadedmetadata;
+
+ await waitUntil(() => v2.currentTime > 0);
+ ok(v2.currentTime > 0, "v2.currentTime is moving (" + v2.currentTime + ")");
+
+ ok(v1.videoWidth > 0, "source width is positive");
+ ok(v1.videoHeight > 0, "source height is positive");
+ const expectedWidth =
+ v1.videoWidth / 2 - (v1.videoWidth / 2 % resolutionAlignment);
+ const expectedHeight =
+ v1.videoHeight / 2 - (v1.videoHeight / 2 % resolutionAlignment);
+ is(v2.videoWidth, expectedWidth,
+ "sink is half the width of the source");
+ is(v2.videoHeight, expectedHeight,
+ "sink is half the height of the source");
+ stream.getTracks().forEach(track => track.stop());
+ v1.srcObject = v2.srcObject = null;
+ pc1.close()
+ pc2.close()
+ }
+
+ runNetworkTest(async () => {
+ await matchPlatformH264CodecPrefs();
+ const hasH264 = await checkForH264Support();
+ if (hasH264 && navigator.userAgent.includes("Android")) {
+ // Android only has a hw encoder for h264
+ resolutionAlignment = 16;
+ }
+ await pushPrefs(['media.peerconnection.video.lock_scaling', true]);
+ await testScale("VP8");
+ if (hasH264) {
+ await testScale("H264");
+ }
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_sender_and_receiver_stats.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_sender_and_receiver_stats.html
new file mode 100644
index 0000000000..72749e8c50
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_sender_and_receiver_stats.html
@@ -0,0 +1,73 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="stats.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1355220",
+ title: "RTCRtpSender.getStats() and RTCRtpReceiver.getStats()",
+ visible: true
+ });
+
+ const checkStats = (sndReport, rcvReport, kind) => {
+ ok(sndReport instanceof window.RTCStatsReport, "sender stats are a RTCStatsReport");
+ ok(rcvReport instanceof window.RTCStatsReport, "receiver stats are a RTCStatsReport");
+ // Returns SSRCs and performs some checks
+ let getSsrcs = (report, kind) => {
+ return [...report.values()]
+ .filter(stat => stat.type.endsWith("bound-rtp")).map(stat =>{
+ isnot(parseInt(stat.id, 16), NaN,
+ `id ${stat.id} is an opaque (hex) number`);
+ is(stat.kind, kind, "kind of " + stat.id
+ + " is expected type " + kind);
+ return stat.ssrc;
+ }).sort().join("|");
+ };
+ let sndSsrcs = getSsrcs(sndReport, kind);
+ let rcvSsrcs = getSsrcs(rcvReport, kind);
+ ok(sndSsrcs, "sender SSRCs is not empty");
+ ok(rcvSsrcs, "receiver SSRCs is not empty");
+ is(sndSsrcs, rcvSsrcs, "sender SSRCs match receiver SSRCs");
+ };
+
+ // This MUST be run after PC_*_WAIT_FOR_MEDIA_FLOW to ensure that we have RTP
+ // before checking for RTCP.
+ // It will throw UnsyncedRtcpError if it times out waiting for sync.
+
+ runNetworkTest(function (options) {
+ test = new PeerConnectionTest(options);
+ test.chain.insertAfter("PC_REMOTE_WAIT_FOR_MEDIA_FLOW",
+ async function PC_LOCAL_AND_REMOTE_CHECK_SENDER_RECEIVER_STATS(test) {
+ await Promise.all([
+ waitForSyncedRtcp(test.pcLocal._pc),
+ waitForSyncedRtcp(test.pcRemote._pc),
+ ]);
+ let senders = test.pcLocal.getSenders();
+ let receivers = test.pcRemote.getReceivers();
+ is(senders.length, 2, "Have exactly two senders.");
+ is(receivers.length, 2, "Have exactly two receivers.");
+ for(let kind of ["audio", "video"]) {
+ let senderStats =
+ await senders.find(s => s.track.kind == kind).getStats();
+ is(senders.filter(s => s.track.kind == kind).length, 1,
+ "Exactly 1 sender of kind " + kind);
+ let receiverStats =
+ await receivers.find(r => r.track.kind == kind).getStats();
+ is(receivers.filter(r => r.track.kind == kind).length, 1,
+ "Exactly 1 receiver of kind " + kind);
+
+ checkStats(senderStats, receiverStats, kind);
+ }
+ }
+ );
+ test.setMediaConstraints([{audio: true}, {video: true}], []);
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_setLocalAnswerInHaveLocalOffer.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_setLocalAnswerInHaveLocalOffer.html
new file mode 100644
index 0000000000..07cdd7d6bd
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_setLocalAnswerInHaveLocalOffer.html
@@ -0,0 +1,34 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "784519",
+ title: "setLocalDescription (answer) in 'have-local-offer'"
+ });
+
+runNetworkTest(function () {
+ const test = new PeerConnectionTest();
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ test.chain.removeAfter("PC_LOCAL_SET_LOCAL_DESCRIPTION");
+
+ test.chain.append([
+ function PC_LOCAL_SET_LOCAL_ANSWER(test) {
+ test.pcLocal._latest_offer.type = "answer";
+ return test.pcLocal.setLocalDescriptionAndFail(test.pcLocal._latest_offer)
+ .then(err => {
+ is(err.name, "InvalidModificationError", "Error is InvalidModificationError");
+ });
+ }
+ ]);
+
+ return test.run();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_setLocalAnswerInStable.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_setLocalAnswerInStable.html
new file mode 100644
index 0000000000..e57c0640f4
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_setLocalAnswerInStable.html
@@ -0,0 +1,34 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "784519",
+ title: "setLocalDescription (answer) in 'stable'"
+ });
+
+runNetworkTest(function () {
+ const test = new PeerConnectionTest();
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ test.chain.removeAfter("PC_LOCAL_CREATE_OFFER");
+
+ test.chain.append([
+ function PC_LOCAL_SET_LOCAL_ANSWER(test) {
+ test.pcLocal._latest_offer.type = "answer";
+ return test.pcLocal.setLocalDescriptionAndFail(test.pcLocal._latest_offer)
+ .then(err => {
+ is(err.name, "InvalidModificationError", "Error is InvalidModificationError");
+ });
+ }
+ ]);
+
+ return test.run();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_setLocalOfferInHaveRemoteOffer.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_setLocalOfferInHaveRemoteOffer.html
new file mode 100644
index 0000000000..bd98a83635
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_setLocalOfferInHaveRemoteOffer.html
@@ -0,0 +1,31 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "784519",
+ title: "setLocalDescription (offer) in 'have-remote-offer'"
+ });
+
+runNetworkTest(function () {
+ const test = new PeerConnectionTest();
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ test.chain.removeAfter("PC_REMOTE_SET_REMOTE_DESCRIPTION");
+
+ test.chain.append([
+ async function PC_REMOTE_SET_LOCAL_OFFER(test) {
+ const err = await test.pcRemote.setLocalDescriptionAndFail(test.pcLocal._latest_offer);
+ is(err.name, "InvalidModificationError", "Error is InvalidModificationError");
+ }
+ ]);
+
+ return test.run();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_setParameters.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_setParameters.html
new file mode 100644
index 0000000000..5df97e39f5
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_setParameters.html
@@ -0,0 +1,470 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+ bug: "1230184",
+ title: "Set parameters on sender",
+ visible: true
+});
+
+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
+`;
+
+function buildMaximumSendEncodings() {
+ const sendEncodings = [];
+ while (true) {
+ // isDeeply does not see identical string primitives and String objects
+ // as the same, so we make this a string primitive.
+ sendEncodings.push({rid: `${sendEncodings.length}`});
+ const pc = new RTCPeerConnection();
+ const {sender} = pc.addTransceiver('video', {sendEncodings});
+ const {encodings} = sender.getParameters();
+ if (encodings.length < sendEncodings.length) {
+ sendEncodings.pop();
+ return sendEncodings;
+ }
+ }
+}
+
+async function queueAWebrtcTask() {
+ const pc = new RTCPeerConnection();
+ pc.addTransceiver('audio');
+ await new Promise(r => pc.onnegotiationneeded = r);
+ pc.close();
+}
+
+// setParameters is mostly tested in wpt, but we test a few
+// implementation-specific things here. Other mochitests check whether the
+// set parameters actually have the desired effect on the media streams.
+const tests = [
+
+ // wpt currently does not assume support for 3 encodings, which limits the
+ // effectiveness of its powers-of-2 test (since it can test only for 1 and 2)
+ async function checkScaleResolutionDownByAutoFillPowersOf2() {
+ const pc = new RTCPeerConnection();
+ const {sender} = pc.addTransceiver('video', {
+ sendEncodings: [{rid: "0"},{rid: "1"},{rid: "2"}]
+ });
+ const {encodings} = sender.getParameters();
+ const scaleValues = encodings.map(({scaleResolutionDownBy}) => scaleResolutionDownBy);
+ isDeeply(scaleValues, [4, 2, 1]);
+ },
+
+ // wpt currently does not assume support for 3 encodings, which limits the
+ // effectiveness of its fill-with-1 test
+ async function checkScaleResolutionDownByAutoFillWith1() {
+ const pc = new RTCPeerConnection();
+ const {sender} = pc.addTransceiver('video', {
+ sendEncodings: [
+ {rid: "0"},{rid: "1", scaleResolutionDownBy: 3},{rid: "2"}
+ ]
+ });
+ const {encodings} = sender.getParameters();
+ const scaleValues = encodings.map(({scaleResolutionDownBy}) => scaleResolutionDownBy);
+ isDeeply(scaleValues, [1, 3, 1]);
+ },
+
+ async function checkVideoEncodingLimit() {
+ const pc = new RTCPeerConnection();
+ const maxSendEncodings = buildMaximumSendEncodings();
+ const sendEncodings = maxSendEncodings.concat({rid: "a"});
+ const {sender} = pc.addTransceiver('video', {sendEncodings});
+ const {encodings} = sender.getParameters();
+
+ const rids = encodings.map(({rid}) => rid);
+ const expectedRids = maxSendEncodings.map(({rid}) => rid);
+ isDeeply(rids, expectedRids);
+
+ const scaleValues = encodings.map(({scaleResolutionDownBy}) => scaleResolutionDownBy);
+ const expectedScaleValues = [];
+ let scale = 1;
+ while (expectedScaleValues.length < maxSendEncodings.length) {
+ expectedScaleValues.push(scale);
+ scale *= 2;
+ }
+ isDeeply(scaleValues, expectedScaleValues.reverse());
+ },
+
+ async function checkScaleDownByInTrimmedEncoding() {
+ const pc = new RTCPeerConnection();
+ const maxSendEncodings = buildMaximumSendEncodings();
+ const sendEncodings = maxSendEncodings.concat({rid: "a", scaleResolutionDownBy: 3});
+ const {sender} = pc.addTransceiver('video', {sendEncodings});
+ const {encodings} = sender.getParameters();
+ const rids = encodings.map(({rid}) => rid);
+ const expectedRids = maxSendEncodings.map(({rid}) => rid);
+ isDeeply(rids, expectedRids);
+ const scaleValues = encodings.map(({scaleResolutionDownBy}) => scaleResolutionDownBy);
+ const expectedScaleValues = maxSendEncodings.map(() => 1);
+ isDeeply(scaleValues, expectedScaleValues);
+ },
+
+ async function checkLibwebrtcRidLengthLimit() {
+ const pc = new RTCPeerConnection();
+ try {
+ pc.addTransceiver('video', {
+ sendEncodings: [{rid: "wibblywobblyjeremybearimy"}]}
+ );
+ ok(false, "Rid should be too long for libwebrtc!");
+ } catch (e) {
+ is(e.name, "TypeError",
+ "Rid that is too long for libwebrtc should result in a TypeError");
+ }
+ },
+
+ async function checkErrorsInTrimmedEncodings() {
+ const pc = new RTCPeerConnection();
+ const maxSendEncodings = buildMaximumSendEncodings();
+ try {
+ const sendEncodings = maxSendEncodings.concat({rid: "foo-bar"});
+ pc.addTransceiver('video', { sendEncodings });
+ ok(false, "Should throw due to invalid rid characters");
+ } catch (e) {
+ is(e.name, "TypeError")
+ }
+ try {
+ const sendEncodings = maxSendEncodings.concat({rid: "wibblywobblyjeremybearimy"});
+ pc.addTransceiver('video', { sendEncodings });
+ ok(false, "Should throw because rid too long");
+ } catch (e) {
+ is(e.name, "TypeError")
+ }
+ try {
+ const sendEncodings = maxSendEncodings.concat({scaleResolutionDownBy: 2});
+ pc.addTransceiver('video', { sendEncodings });
+ ok(false, "Should throw due to missing rid");
+ } catch (e) {
+ is(e.name, "TypeError")
+ }
+ try {
+ const sendEncodings = maxSendEncodings.concat(maxSendEncodings[0]);
+ pc.addTransceiver('video', { sendEncodings });
+ ok(false, "Should throw due to duplicate rid");
+ } catch (e) {
+ is(e.name, "TypeError")
+ }
+ try {
+ const sendEncodings = maxSendEncodings.concat({rid: maxSendEncodings.length, scaleResolutionDownBy: 0});
+ pc.addTransceiver('video', { sendEncodings });
+ ok(false, "Should throw due to invalid scaleResolutionDownBy");
+ } catch (e) {
+ is(e.name, "RangeError")
+ }
+ try {
+ const sendEncodings = maxSendEncodings.concat({rid: maxSendEncodings.length, maxFramerate: -1});
+ pc.addTransceiver('video', { sendEncodings });
+ ok(false, "Should throw due to invalid maxFramerate");
+ } catch (e) {
+ is(e.name, "RangeError")
+ }
+ },
+
+ async function checkCompatModeUnicastSetParametersAllowsSimulcastOffer() {
+ await pushPrefs(
+ ["media.peerconnection.allow_old_setParameters", true]);
+ const pc1 = new RTCPeerConnection();
+ const stream = await navigator.mediaDevices.getUserMedia({video: true});
+ 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);
+ isDeeply(rids, ["foo", "bar"]);
+ is(encodings[0].scaleResolutionDownBy, 2.0);
+ is(encodings[1].scaleResolutionDownBy, 1.0);
+ },
+
+ async function checkCompatModeUnicastSetParametersInterruptAllowsSimulcastOffer() {
+ await pushPrefs(
+ ["media.peerconnection.allow_old_setParameters", true]);
+ const pc1 = new RTCPeerConnection();
+ const stream = await navigator.mediaDevices.getUserMedia({video: true});
+ 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);
+ isDeeply(rids, ["foo", "bar"]);
+ is(encodings[0].scaleResolutionDownBy, 2.0);
+ is(encodings[1].scaleResolutionDownBy, 1.0);
+ },
+
+ async function checkCompatModeSimulcastSetParametersSetsSimulcastEnvelope() {
+ await pushPrefs(
+ ["media.peerconnection.allow_old_setParameters", true]);
+ const pc1 = new RTCPeerConnection();
+ const stream = await navigator.mediaDevices.getUserMedia({video: true});
+ const sender = pc1.addTrack(stream.getTracks()[0]);
+ const parameters = sender.getParameters();
+ parameters.encodings[0].rid = "1";
+ parameters.encodings.push({rid: "2"});
+ await sender.setParameters(parameters);
+
+ await pc1.setRemoteDescription({type: "offer", sdp: simulcastOffer});
+
+ const {encodings} = sender.getParameters();
+ const rids = encodings.map(({rid}) => rid);
+ // No overlap in rids -> unicast
+ isDeeply(rids, [undefined]);
+ },
+
+ async function checkCompatModeSimulcastSetParametersRacesLocalUnicastOffer() {
+ await pushPrefs(
+ ["media.peerconnection.allow_old_setParameters", true]);
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ const stream = await navigator.mediaDevices.getUserMedia({video: true});
+ const sender = pc1.addTrack(stream.getTracks()[0]);
+ // unicast offer
+ const offer = await pc1.createOffer();
+ const aTask = queueAWebrtcTask();
+ const sldPromise = pc1.setLocalDescription(offer);
+
+ // Right now, we have aTask queued. The success task for sLD is not queued
+ // yet, because Firefox performs the initial steps on the microtask queue,
+ // which we have not allowed to run yet. Awaiting aTask will first clear
+ // the microtask queue, then run the task queue until aTask is finished.
+ // That _should_ result in the success task for sLD(offer) being queued.
+ await aTask;
+
+ const parameters = sender.getParameters();
+ parameters.encodings[0].rid = "foo";
+ parameters.encodings.push({rid: "bar"});
+ // simulcast setParameters; the task to update [[SendEncodings]] should be
+ // queued after the success task for sLD(offer)
+ await sender.setParameters(parameters);
+ await sldPromise;
+
+ const {encodings} = sender.getParameters();
+ const rids = encodings.map(({rid}) => rid);
+ // Compat mode lets this slide, but won't try to negotiate it since we've
+ // already applied a unicast local offer.
+ isDeeply(rids, ["foo", "bar"]);
+
+ // Let negotiation finish, so we can generate a new offer
+ await pc2.setRemoteDescription(pc1.localDescription);
+ await pc2.setLocalDescription();
+ await pc1.setRemoteDescription(pc2.localDescription);
+
+ const reoffer = await pc1.createOffer();
+ ok(!reoffer.sdp.includes("a=simulcast"), "reoffer should be unicast");
+ },
+
+ async function checkCompatModeSimulcastSetParametersRacesRemoteOffer() {
+ await pushPrefs(
+ ["media.peerconnection.allow_old_setParameters", true]);
+ const pc1 = new RTCPeerConnection();
+ const stream = await navigator.mediaDevices.getUserMedia({video: true});
+ const sender = pc1.addTrack(stream.getTracks()[0]);
+ const parameters = sender.getParameters();
+ parameters.encodings[0].rid = "foo";
+ parameters.encodings.push({rid: "bar"});
+ const p = sender.setParameters(parameters);
+ await pc1.setRemoteDescription({type: "offer", sdp: simulcastOffer});
+ await p;
+ const answer = await pc1.createAnswer();
+
+ const {encodings} = sender.getParameters();
+ const rids = encodings.map(({rid}) => rid);
+ isDeeply(rids, ["foo", "bar"]);
+ ok(answer.sdp.includes("a=simulcast:send foo;bar"), "answer should be simulcast");
+ },
+
+ async function checkCompatModeSimulcastSetParametersRacesLocalAnswer() {
+ await pushPrefs(
+ ["media.peerconnection.allow_old_setParameters", true]);
+ // We do an initial negotiation, and while the local answer is pending,
+ // perform a setParameters on a not-yet-negotiated video sender. The intent
+ // here is to have the success task for sLD(answer) run while the
+ // setParameters is pending.
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+
+ const audioStream = await navigator.mediaDevices.getUserMedia({audio: true});
+ // We use this later on, but set it up now so we don't inadvertently
+ // crank the event loop more than we intend below.
+ const videoStream = await navigator.mediaDevices.getUserMedia({video: true});
+ pc2.addTrack(audioStream.getTracks()[0]);
+ await pc2.setLocalDescription();
+ await pc1.setRemoteDescription(pc2.localDescription);
+ const answer = await pc1.createAnswer();
+ const aTask = queueAWebrtcTask();
+ const sldPromise = pc1.setLocalDescription(answer);
+
+ // Right now, we have aTask queued. The success task for sLD is not queued
+ // yet, because Firefox performs the initial steps on the microtask queue,
+ // which we have not allowed to run yet. Awaiting aTask will first clear
+ // the microtask queue, then run the task queue until aTask is finished.
+ // That _should_ result in the success task for sLD(answer) being queued.
+ await aTask;
+
+ // The success task for sLD(answer) should be queued now. Don't relinquish
+ // the event loop!
+
+ // New sender that has nothing to do with the negotiation in progress.
+ const sender = pc1.addTrack(videoStream.getTracks()[0]);
+ const parameters = sender.getParameters();
+ parameters.encodings[0].rid = "foo";
+ parameters.encodings.push({rid: "bar"});
+
+ // We have not relinquished the event loop, so the sLD(answer) should still
+ // be queued. The task that updates [[SendEncodings]] (from setParameters)
+ // should be queued behind it. Let them both run.
+ await sender.setParameters(parameters);
+ await sldPromise;
+
+ const offer = await pc1.createOffer();
+
+ const {encodings} = sender.getParameters();
+ const rids = encodings.map(({rid}) => rid);
+ isDeeply(rids, ["foo", "bar"]);
+ ok(offer.sdp.includes("a=simulcast:send foo;bar"), "offer should be simulcast");
+ },
+
+ async function checkCompatModeSimulcastSetParametersRacesRemoteAnswer() {
+ await pushPrefs(
+ ["media.peerconnection.allow_old_setParameters", true]);
+ // We do an initial negotiation, and while the remote answer is pending,
+ // perform a setParameters on a not-yet-negotiated video sender. The intent
+ // here is to have the success task for sRD(answer) run while the
+ // setParameters is pending.
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+
+ const audioStream = await navigator.mediaDevices.getUserMedia({audio: true});
+ // We use this later on, but set it up now so we don't inadvertently
+ // crank the event loop more than we intend below.
+ const videoStream = await navigator.mediaDevices.getUserMedia({video: true});
+ pc1.addTrack(audioStream.getTracks()[0]);
+ await pc1.setLocalDescription();
+ await pc2.setRemoteDescription(pc1.localDescription);
+ await pc2.setLocalDescription();
+ const aTask = queueAWebrtcTask();
+ const srdPromise = pc1.setRemoteDescription(pc2.localDescription);
+
+ // Right now, we have aTask queued. The success task for sRD is not queued
+ // yet, because Firefox performs the initial steps on the microtask queue,
+ // which we have not allowed to run yet. Awaiting aTask will first clear
+ // the microtask queue, then run the task queue until aTask is finished.
+ // That _should_ result in the success task for sRD(answer) being queued.
+ await aTask;
+
+ // The success task for sRD(answer) should be queued now. Don't relinquish
+ // the event loop!
+
+ const sender = pc1.addTrack(videoStream.getTracks()[0]);
+ const parameters = sender.getParameters();
+ parameters.encodings[0].rid = "foo";
+ parameters.encodings.push({rid: "bar"});
+
+ // We have not relinquished the event loop, so the sRD(answer) should still
+ // be queued. The task that updates [[SendEncodings]] (from setParameters)
+ // should be queued behind it. Let them both run.
+ await sender.setParameters(parameters);
+ await srdPromise;
+
+ const offer = await pc1.createOffer();
+
+ const {encodings} = sender.getParameters();
+ const rids = encodings.map(({rid}) => rid);
+ isDeeply(rids, ["foo", "bar"]);
+ ok(offer.sdp.includes("a=simulcast:send foo;bar"), "offer should be simulcast");
+ },
+
+ async function checkCompatModeSimulcastRidlessSetParametersRacesLocalOffer() {
+ await pushPrefs(
+ ["media.peerconnection.allow_old_setParameters", true]);
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ const stream = await navigator.mediaDevices.getUserMedia({video: true});
+ const sender = pc1.addTrack(stream.getTracks()[0]);
+ // unicast offer
+ const aTask = queueAWebrtcTask();
+ const sldPromise = pc1.setLocalDescription();
+
+ // Right now, we have aTask queued. The success task for sLD is not queued
+ // yet, because Firefox performs the initial steps on the microtask queue,
+ // which we have not allowed to run yet. Awaiting aTask will first clear
+ // the microtask queue, then run the task queue until aTask is finished.
+ // That _should_ result in the success task for sLD(offer) being queued.
+ await aTask;
+
+ // simulcast setParameters; the task to update [[SendEncodings]] should be
+ // queued after the success task for sLD(offer)
+ try {
+ await sender.setParameters({"encodings": [{}, {}]});
+ ok(false, "setParameters with two ridless encodings should fail");
+ } catch (e) {
+ ok(true, "setParameters with two ridless encodings should fail");
+ }
+ await sldPromise;
+
+ const {encodings} = sender.getParameters();
+ const rids = encodings.map(({rid}) => rid);
+ // Compat mode lets this slide, but won't try to negotiate it since we've
+ // already applied a unicast local offer.
+ isDeeply(rids, [undefined]);
+
+ // Let negotiation finish, so we can generate a new offer
+ await pc2.setRemoteDescription(pc1.localDescription);
+ await pc2.setLocalDescription();
+ await pc1.setRemoteDescription(pc2.localDescription);
+
+ const reoffer = await pc1.createOffer();
+ ok(!reoffer.sdp.includes("a=simulcast"), "reoffer should be unicast");
+ },
+
+];
+
+runNetworkTest(async () => {
+ await pushPrefs(
+ ["media.peerconnection.allow_old_setParameters", false]);
+ for (const test of tests) {
+ info(`Running test: ${test.name}`);
+ await test();
+ info(`Done running test: ${test.name}`);
+ }
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_setParameters_maxFramerate.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_setParameters_maxFramerate.html
new file mode 100644
index 0000000000..8047719775
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_setParameters_maxFramerate.html
@@ -0,0 +1,63 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+ bug: "1611957",
+ title: "Live-updating maxFramerate"
+});
+
+let sender, receiver;
+
+async function checkMaxFrameRate(rate) {
+ const parameters = sender.getParameters();
+ parameters.encodings[0].maxFramerate = rate;
+ await sender.setParameters(parameters);
+ await wait(2000);
+ const stats = Array.from((await receiver.getStats()).values());
+ const inboundRtp = stats.find(stat => stat.type == "inbound-rtp");
+ info(`inbound-rtp stats: ${JSON.stringify(inboundRtp)}`);
+ const fps = inboundRtp.framesPerSecond;
+ ok(fps <= (rate * 1.1) + 1,
+ `fps is an appropriate value (${fps}) for rate (${rate})`);
+}
+
+runNetworkTest(async function (options) {
+ let test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{video: true}], []);
+ test.chain.append([
+ function CHECK_PRECONDITIONS() {
+ is(test.pcLocal._pc.getSenders().length, 1,
+ "Should have 1 local sender");
+ is(test.pcRemote._pc.getReceivers().length, 1,
+ "Should have 1 remote receiver");
+
+ sender = test.pcLocal._pc.getSenders()[0];
+ receiver = test.pcRemote._pc.getReceivers()[0];
+ },
+ function PC_LOCAL_SET_MAX_FRAMERATE_2() {
+ return checkMaxFrameRate(2);
+ },
+ function PC_LOCAL_SET_MAX_FRAMERATE_4() {
+ return checkMaxFrameRate(4);
+ },
+ function PC_LOCAL_SET_MAX_FRAMERATE_15() {
+ return checkMaxFrameRate(15);
+ },
+ function PC_LOCAL_SET_MAX_FRAMERATE_8() {
+ return checkMaxFrameRate(8);
+ },
+ function PC_LOCAL_SET_MAX_FRAMERATE_1() {
+ return checkMaxFrameRate(1);
+ },
+ ]);
+ await test.run();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_setParameters_maxFramerate_oldSetParameters.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_setParameters_maxFramerate_oldSetParameters.html
new file mode 100644
index 0000000000..9c68a31c0a
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_setParameters_maxFramerate_oldSetParameters.html
@@ -0,0 +1,60 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+ bug: "1611957",
+ title: "Live-updating maxFramerate"
+});
+
+let sender, receiver;
+
+async function checkMaxFrameRate(rate) {
+ sender.setParameters({ encodings: [{ maxFramerate: rate }] });
+ await wait(2000);
+ const stats = Array.from((await receiver.getStats()).values());
+ const inboundRtp = stats.find(stat => stat.type == "inbound-rtp");
+ info(`inbound-rtp stats: ${JSON.stringify(inboundRtp)}`);
+ const fps = inboundRtp.framesPerSecond;
+ ok(fps <= (rate * 1.1) + 1, `fps is an appropriate value (${fps}) for rate (${rate})`);
+}
+
+runNetworkTest(async function (options) {
+ let test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{video: true}], []);
+ test.chain.append([
+ function CHECK_PRECONDITIONS() {
+ is(test.pcLocal._pc.getSenders().length, 1,
+ "Should have 1 local sender");
+ is(test.pcRemote._pc.getReceivers().length, 1,
+ "Should have 1 remote receiver");
+
+ sender = test.pcLocal._pc.getSenders()[0];
+ receiver = test.pcRemote._pc.getReceivers()[0];
+ },
+ function PC_LOCAL_SET_MAX_FRAMERATE_2() {
+ return checkMaxFrameRate(2);
+ },
+ function PC_LOCAL_SET_MAX_FRAMERATE_4() {
+ return checkMaxFrameRate(4);
+ },
+ function PC_LOCAL_SET_MAX_FRAMERATE_15() {
+ return checkMaxFrameRate(15);
+ },
+ function PC_LOCAL_SET_MAX_FRAMERATE_8() {
+ return checkMaxFrameRate(8);
+ },
+ function PC_LOCAL_SET_MAX_FRAMERATE_1() {
+ return checkMaxFrameRate(1);
+ },
+ ]);
+ await test.run();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_setParameters_oldSetParameters.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_setParameters_oldSetParameters.html
new file mode 100644
index 0000000000..2b55ec46e6
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_setParameters_oldSetParameters.html
@@ -0,0 +1,86 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+ bug: "1230184",
+ title: "Set parameters on sender",
+ visible: true
+});
+
+function parameterstest(pc) {
+ ok(pc.getSenders().length, "have senders");
+ var sender = pc.getSenders()[0];
+
+ var testParameters = (params, errorName, errorMsg) => {
+ info("Trying to set " + JSON.stringify(params));
+
+ var validateParameters = (a, b) => {
+ var validateEncoding = (a, b) => {
+ is(a.rid, b.rid, "same rid");
+ is(a.maxBitrate, b.maxBitrate, "same maxBitrate");
+ is(a.maxFramerate, b.maxFramerate, "same maxFramerate");
+ is(a.scaleResolutionDownBy, b.scaleResolutionDownBy,
+ "same scaleResolutionDownBy");
+ };
+ is(a.encodings.length, (b.encodings || []).length, "same encodings");
+ a.encodings.forEach((en, i) => validateEncoding(en, b.encodings[i]));
+ };
+
+ var before = JSON.stringify(sender.getParameters());
+ isnot(JSON.stringify(params), before, "starting condition");
+
+ var p = sender.setParameters(params)
+ .then(() => {
+ isnot(JSON.stringify(sender.getParameters()), before, "parameters changed");
+ validateParameters(sender.getParameters(), params);
+ is(null, errorName || null, "is success expected");
+ }, e => {
+ is(e.name, errorName, "correct error name");
+ is(e.message, errorMsg, "correct error message");
+ });
+ is(JSON.stringify(sender.getParameters()), before, "parameters not set yet");
+ return p;
+ };
+
+ return [
+ [{ encodings: [ { rid: "foo", maxBitrate: 40000, scaleResolutionDownBy: 2 },
+ { rid: "bar", maxBitrate: 10000, scaleResolutionDownBy: 4 }]
+ }],
+ [{ encodings: [{ maxBitrate: 10000, scaleResolutionDownBy: 4 }]}],
+ [{ encodings: [{ maxFramerate: 0.0, scaleResolutionDownBy: 1 }]}],
+ [{ encodings: [{ maxFramerate: 30.5, scaleResolutionDownBy: 1 }]}],
+ [{ encodings: [{ maxFramerate: -1, scaleResolutionDownBy: 1 }]}, "RangeError", "maxFramerate must be non-negative"],
+ [{ encodings: [{ maxBitrate: 40000 },
+ { rid: "bar", maxBitrate: 10000 }] }, "TypeError", "Missing rid"],
+ [{ encodings: [{ rid: "foo", maxBitrate: 40000 },
+ { rid: "bar", maxBitrate: 10000 },
+ { rid: "bar", maxBitrate: 20000 }] }, "TypeError", "Duplicate rid"],
+ [{}, "TypeError", `RTCRtpSender.setParameters: Missing required 'encodings' member of RTCRtpSendParameters.`]
+ ].reduce((p, args) => p.then(() => testParameters.apply(this, args)),
+ Promise.resolve());
+}
+
+runNetworkTest(() => {
+ const test = new PeerConnectionTest();
+ test.setMediaConstraints([{video: true}], [{video: true}]);
+ test.chain.removeAfter("PC_REMOTE_WAIT_FOR_MEDIA_FLOW");
+
+ // Test sender parameters.
+ test.chain.append([
+ function PC_LOCAL_SET_PARAMETERS(test) {
+ return parameterstest(test.pcLocal._pc);
+ }
+ ]);
+
+ return test.run();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_setParameters_scaleResolutionDownBy.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_setParameters_scaleResolutionDownBy.html
new file mode 100644
index 0000000000..d1275d6523
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_setParameters_scaleResolutionDownBy.html
@@ -0,0 +1,98 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+ bug: "1253499",
+ title: "Live-updating scaleResolutionDownBy"
+});
+
+async function checkForH264Support() {
+ const pc = new RTCPeerConnection();
+ const offer = await pc.createOffer({offerToReceiveVideo: true});
+ return offer.sdp.match(/a=rtpmap:[1-9][0-9]* H264/);
+}
+
+let sender, localElem, remoteElem;
+let originalWidth, originalHeight;
+let resolutionAlignment = 1;
+
+async function checkScaleDownBy(scale) {
+ const parameters = sender.getParameters();
+ parameters.encodings[0].scaleResolutionDownBy = scale;
+ await sender.setParameters(parameters);
+ await haveEvent(remoteElem, "resize", wait(5000, new Error("Timeout")));
+
+ // Find the expected resolution. Internally we floor the exact scaling, then
+ // shrink each dimension to the alignment requested by the encoder.
+ let expectedWidth =
+ originalWidth / scale - (originalWidth / scale % resolutionAlignment);
+ let expectedHeight =
+ originalHeight / scale - (originalHeight / scale % resolutionAlignment);
+
+ is(remoteElem.videoWidth, expectedWidth,
+ `Width should have scaled down by ${scale}`);
+ is(remoteElem.videoHeight, expectedHeight,
+ `Height should have scaled down by ${scale}`);
+}
+
+runNetworkTest(async function (options) {
+ await pushPrefs(['media.peerconnection.video.lock_scaling', true]);
+ // [TODO] re-enable HW decoder after bug 1526207 is fixed.
+ if (navigator.userAgent.includes("Android")) {
+ if (await checkForH264Support()) {
+ // Android only has h264 in hw, so now we know it will use vp8 in hw too.
+ resolutionAlignment = 16;
+ }
+ await pushPrefs(["media.navigator.mediadatadecoder_vpx_enabled", false],
+ ["media.webrtc.hw.h264.enabled", false]);
+ }
+
+ let test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{video: true}], []);
+ test.chain.append([
+ function CHECK_PRECONDITIONS() {
+ is(test.pcLocal._pc.getSenders().length, 1,
+ "Should have 1 local sender");
+ is(test.pcLocal.localMediaElements.length, 1,
+ "Should have 1 local sending media element");
+ is(test.pcRemote.remoteMediaElements.length, 1,
+ "Should have 1 remote media element");
+
+ sender = test.pcLocal._pc.getSenders()[0];
+ localElem = test.pcLocal.localMediaElements[0];
+ remoteElem = test.pcRemote.remoteMediaElements[0];
+
+ remoteElem.addEventListener("resize", () =>
+ info(`Video resized to ${remoteElem.videoWidth}x${remoteElem.videoHeight}`));
+
+ originalWidth = localElem.videoWidth;
+ originalHeight = localElem.videoHeight;
+ info(`Original width is ${originalWidth}`);
+ },
+ function PC_LOCAL_SCALEDOWNBY_2() {
+ return checkScaleDownBy(2);
+ },
+ function PC_LOCAL_SCALEDOWNBY_4() {
+ return checkScaleDownBy(4);
+ },
+ function PC_LOCAL_SCALEDOWNBY_15() {
+ return checkScaleDownBy(15);
+ },
+ function PC_LOCAL_SCALEDOWNBY_8() {
+ return checkScaleDownBy(8);
+ },
+ function PC_LOCAL_SCALEDOWNBY_1() {
+ return checkScaleDownBy(1);
+ },
+ ]);
+ await test.run();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_setParameters_scaleResolutionDownBy_oldSetParameters.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_setParameters_scaleResolutionDownBy_oldSetParameters.html
new file mode 100644
index 0000000000..4d515bd5c1
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_setParameters_scaleResolutionDownBy_oldSetParameters.html
@@ -0,0 +1,96 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+ bug: "1253499",
+ title: "Live-updating scaleResolutionDownBy"
+});
+
+async function checkForH264Support() {
+ const pc = new RTCPeerConnection();
+ const offer = await pc.createOffer({offerToReceiveVideo: true});
+ return offer.sdp.match(/a=rtpmap:[1-9][0-9]* H264/);
+}
+
+let sender, localElem, remoteElem;
+let originalWidth, originalHeight;
+let resolutionAlignment = 1;
+
+async function checkScaleDownBy(scale) {
+ sender.setParameters({ encodings: [{ scaleResolutionDownBy: scale }] });
+ await haveEvent(remoteElem, "resize", wait(5000, new Error("Timeout")));
+
+ // Find the expected resolution. Internally we floor the exact scaling, then
+ // shrink each dimension to the alignment requested by the encoder.
+ let expectedWidth =
+ originalWidth / scale - (originalWidth / scale % resolutionAlignment);
+ let expectedHeight =
+ originalHeight / scale - (originalHeight / scale % resolutionAlignment);
+
+ is(remoteElem.videoWidth, expectedWidth,
+ `Width should have scaled down by ${scale}`);
+ is(remoteElem.videoHeight, expectedHeight,
+ `Height should have scaled down by ${scale}`);
+}
+
+runNetworkTest(async function (options) {
+ await pushPrefs(['media.peerconnection.video.lock_scaling', true]);
+ // [TODO] re-enable HW decoder after bug 1526207 is fixed.
+ if (navigator.userAgent.includes("Android")) {
+ if (await checkForH264Support()) {
+ // Android only has h264 in hw, so now we know it will use vp8 in hw too.
+ resolutionAlignment = 16;
+ }
+ await pushPrefs(["media.navigator.mediadatadecoder_vpx_enabled", false],
+ ["media.webrtc.hw.h264.enabled", false]);
+ }
+
+ let test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{video: true}], []);
+ test.chain.append([
+ function CHECK_PRECONDITIONS() {
+ is(test.pcLocal._pc.getSenders().length, 1,
+ "Should have 1 local sender");
+ is(test.pcLocal.localMediaElements.length, 1,
+ "Should have 1 local sending media element");
+ is(test.pcRemote.remoteMediaElements.length, 1,
+ "Should have 1 remote media element");
+
+ sender = test.pcLocal._pc.getSenders()[0];
+ localElem = test.pcLocal.localMediaElements[0];
+ remoteElem = test.pcRemote.remoteMediaElements[0];
+
+ remoteElem.addEventListener("resize", () =>
+ info(`Video resized to ${remoteElem.videoWidth}x${remoteElem.videoHeight}`));
+
+ originalWidth = localElem.videoWidth;
+ originalHeight = localElem.videoHeight;
+ info(`Original width is ${originalWidth}`);
+ },
+ function PC_LOCAL_SCALEDOWNBY_2() {
+ return checkScaleDownBy(2);
+ },
+ function PC_LOCAL_SCALEDOWNBY_4() {
+ return checkScaleDownBy(4);
+ },
+ function PC_LOCAL_SCALEDOWNBY_15() {
+ return checkScaleDownBy(15);
+ },
+ function PC_LOCAL_SCALEDOWNBY_8() {
+ return checkScaleDownBy(8);
+ },
+ function PC_LOCAL_SCALEDOWNBY_1() {
+ return checkScaleDownBy(1);
+ },
+ ]);
+ await test.run();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_setRemoteAnswerInHaveRemoteOffer.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_setRemoteAnswerInHaveRemoteOffer.html
new file mode 100644
index 0000000000..1912835160
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_setRemoteAnswerInHaveRemoteOffer.html
@@ -0,0 +1,34 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "784519",
+ title: "setRemoteDescription (answer) in 'have-remote-offer'"
+ });
+
+runNetworkTest(function () {
+ const test = new PeerConnectionTest();
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ test.chain.removeAfter("PC_REMOTE_SET_REMOTE_DESCRIPTION");
+
+ test.chain.append([
+ function PC_REMOTE_SET_REMOTE_ANSWER(test) {
+ test.pcLocal._latest_offer.type = "answer";
+ test.pcRemote._pc.setRemoteDescription(test.pcLocal._latest_offer)
+ .then(generateErrorCallback('setRemoteDescription should fail'),
+ err =>
+ is(err.name, "InvalidStateError", "Error is InvalidStateError"));
+ }
+ ]);
+
+ return test.run();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_setRemoteAnswerInStable.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_setRemoteAnswerInStable.html
new file mode 100644
index 0000000000..6208fdea3e
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_setRemoteAnswerInStable.html
@@ -0,0 +1,34 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "784519",
+ title: "setRemoteDescription (answer) in 'stable'"
+ });
+
+runNetworkTest(function () {
+ const test = new PeerConnectionTest();
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ test.chain.removeAfter("PC_LOCAL_CREATE_OFFER");
+
+ test.chain.append([
+ function PC_LOCAL_SET_REMOTE_ANSWER(test) {
+ test.pcLocal._latest_offer.type = "answer";
+ test.pcLocal._pc.setRemoteDescription(test.pcLocal._latest_offer)
+ .then(generateErrorCallback('setRemoteDescription should fail'),
+ err =>
+ is(err.name, "InvalidStateError", "Error is InvalidStateError"));
+ }
+ ]);
+
+ return test.run();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_setRemoteOfferInHaveLocalOffer.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_setRemoteOfferInHaveLocalOffer.html
new file mode 100644
index 0000000000..20236f442c
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_setRemoteOfferInHaveLocalOffer.html
@@ -0,0 +1,37 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "784519",
+ title: "setRemoteDescription (offer) in 'have-local-offer'"
+ });
+
+runNetworkTest(function () {
+ const test = new PeerConnectionTest();
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ test.chain.removeAfter("PC_LOCAL_SET_LOCAL_DESCRIPTION");
+
+ test.chain.append([
+ async function PC_LOCAL_SET_REMOTE_OFFER(test) {
+ const p = test.pcLocal._pc.setRemoteDescription(test.pcLocal._latest_offer);
+ await new Promise(r => test.pcLocal.onsignalingstatechange = r);
+ is(test.pcLocal._pc.signalingState, 'stable', 'should fire stable');
+ await new Promise(r => test.pcLocal.onsignalingstatechange = r);
+ is(test.pcLocal._pc.signalingState, 'have-remote-offer',
+ 'should fire have-remote-offer');
+ await p;
+ ok(true, 'setRemoteDescription should succeed');
+ }
+ ]);
+
+ return test.run();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastAnswer.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastAnswer.html
new file mode 100644
index 0000000000..ba75c72022
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastAnswer.html
@@ -0,0 +1,121 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script>
+ <script type="application/javascript" src="helpers_from_wpt/sdp.js"></script>
+ <script type="application/javascript" src="simulcast.js"></script>
+ <script type="application/javascript" src="stats.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1231507",
+ title: "Basic video-only peer connection with Simulcast answer",
+ visible: true
+ });
+
+ runNetworkTest(async () => {
+ await pushPrefs(
+ // 180Kbps was determined empirically, set well-higher than
+ // the 80Kbps+overhead needed for the two simulcast streams.
+ // 100Kbps was apparently too low.
+ ['media.peerconnection.video.min_bitrate_estimate', 180*1000]);
+
+
+ const offerer = new RTCPeerConnection();
+ const answerer = new RTCPeerConnection();
+
+ const add = (pc, can, failed) => can && pc.addIceCandidate(can).catch(failed);
+ offerer.onicecandidate = e => add(answerer, e.candidate, generateErrorCallback());
+ answerer.onicecandidate = e => add(offerer, e.candidate, generateErrorCallback());
+
+ const metadataToBeLoaded = [];
+ offerer.ontrack = (e) => {
+ metadataToBeLoaded.push(getPlaybackWithLoadedMetadata(e.track));
+ };
+
+ // Two recv transceivers, one for each simulcast stream
+ offerer.addTransceiver('video', { direction: 'recvonly' });
+ offerer.addTransceiver('video', { direction: 'recvonly' });
+
+ const offer = await offerer.createOffer();
+
+ const mungedOffer = midToRid(offer);
+ info(`Transformed recv offer to simulcast: ${offer.sdp} to ${mungedOffer}`);
+
+ await answerer.setRemoteDescription({type: 'offer', sdp: mungedOffer});
+
+ // One send transceiver, that will be used to send both simulcast streams
+ const emitter = new VideoFrameEmitter();
+ const videoStream = emitter.stream();
+ const sender = answerer.addTrack(videoStream.getVideoTracks()[0], videoStream);
+ let parameters = sender.getParameters();
+ is(parameters.encodings.length, 2);
+ is(answerer.getSenders().length, 1);
+ emitter.start();
+
+ await offerer.setLocalDescription(offer);
+
+ const rids = offerer.getTransceivers().map(t => t.mid);
+ is(rids.length, 2, 'Should have 2 mids in offer');
+ ok(rids[0] != '', 'First mid should be non-empty');
+ ok(rids[1] != '', 'Second mid should be non-empty');
+ info(`rids: ${JSON.stringify(rids)}`);
+
+ parameters = sender.getParameters();
+ info(`parameters: ${JSON.stringify(parameters)}`);
+ const observedRids = parameters.encodings.map(({rid}) => rid);
+ isDeeply(observedRids, rids);
+ parameters.encodings[0].maxBitrate = 40000;
+ parameters.encodings[0].scaleResolutionDownBy = 1;
+ parameters.encodings[1].maxBitrate = 40000;
+ parameters.encodings[1].scaleResolutionDownBy = 2;
+ await sender.setParameters(parameters);
+
+ const answer = await answerer.createAnswer();
+
+ const mungedAnswer = ridToMid(answer);
+ info(`Transformed send simulcast answer to multiple m-sections: ${answer.sdp} to ${mungedAnswer}`);
+ await offerer.setRemoteDescription({type: 'answer', sdp: mungedAnswer});
+ await answerer.setLocalDescription(answer);
+
+ is(metadataToBeLoaded.length, 2, 'Offerer should have gotten 2 ontrack events');
+ info('Waiting for 2 loadedmetadata events');
+ const videoElems = await Promise.all(metadataToBeLoaded);
+
+ const statsReady =
+ Promise.all([waitForSyncedRtcp(offerer), waitForSyncedRtcp(answerer)]);
+
+ const helper = new VideoStreamHelper();
+ info('Waiting for first video element to start playing');
+ await helper.checkVideoPlaying(videoElems[0]);
+ info('Waiting for second video element to start playing');
+ await helper.checkVideoPlaying(videoElems[1]);
+
+ is(videoElems[0].videoWidth, 50,
+ "sink is same width as source, modulo our cropping algorithm");
+ is(videoElems[0].videoHeight, 50,
+ "sink is same height as source, modulo our cropping algorithm");
+ is(videoElems[1].videoWidth, 25,
+ "sink is 1/2 width of source, modulo our cropping algorithm");
+ is(videoElems[1].videoHeight, 25,
+ "sink is 1/2 height of source, modulo our cropping algorithm");
+
+ await statsReady;
+ info("Stats ready");
+ const senderStats = await sender.getStats();
+ checkSenderStats(senderStats, 2);
+ checkExpectedFields(senderStats);
+ pedanticChecks(senderStats);
+
+ emitter.stop();
+ videoStream.getVideoTracks()[0].stop();
+ offerer.close();
+ answerer.close();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastAnswer_lowResFirst.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastAnswer_lowResFirst.html
new file mode 100644
index 0000000000..00c6e4ad3a
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastAnswer_lowResFirst.html
@@ -0,0 +1,113 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script>
+ <script type="application/javascript" src="helpers_from_wpt/sdp.js"></script>
+ <script type="application/javascript" src="simulcast.js"></script>
+ <script type="application/javascript" src="stats.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1231507",
+ title: "Basic video-only peer connection with Simulcast answer, first rid has lowest resolution",
+ visible: true
+ });
+
+ runNetworkTest(async () => {
+ await pushPrefs(
+ // 180Kbps was determined empirically, set well-higher than
+ // the 80Kbps+overhead needed for the two simulcast streams.
+ // 100Kbps was apparently too low.
+ ['media.peerconnection.video.min_bitrate_estimate', 180*1000]);
+
+
+ const offerer = new RTCPeerConnection();
+ const answerer = new RTCPeerConnection();
+
+ const add = (pc, can, failed) => can && pc.addIceCandidate(can).catch(failed);
+ offerer.onicecandidate = e => add(answerer, e.candidate, generateErrorCallback());
+ answerer.onicecandidate = e => add(offerer, e.candidate, generateErrorCallback());
+
+ const metadataToBeLoaded = [];
+ offerer.ontrack = (e) => {
+ metadataToBeLoaded.push(getPlaybackWithLoadedMetadata(e.track));
+ };
+
+ // Two recv transceivers, one for each simulcast stream
+ offerer.addTransceiver('video', { direction: 'recvonly' });
+ offerer.addTransceiver('video', { direction: 'recvonly' });
+
+ // One send transceiver, that will be used to send both simulcast streams
+ const emitter = new VideoFrameEmitter();
+ const videoStream = emitter.stream();
+ answerer.addTrack(videoStream.getVideoTracks()[0], videoStream);
+ emitter.start();
+
+ const offer = await offerer.createOffer();
+
+ const mungedOffer = midToRid(offer);
+ info(`Transformed recv offer to simulcast: ${offer.sdp} to ${mungedOffer}`);
+
+ await answerer.setRemoteDescription({type: 'offer', sdp: mungedOffer});
+ await offerer.setLocalDescription(offer);
+
+ const rids = offerer.getTransceivers().map(t => t.mid);
+ is(rids.length, 2, 'Should have 2 mids in offer');
+ ok(rids[0] != '', 'First mid should be non-empty');
+ ok(rids[1] != '', 'Second mid should be non-empty');
+ info(`rids: ${JSON.stringify(rids)}`);
+
+ const sender = answerer.getSenders()[0];
+ const parameters = sender.getParameters();
+ parameters.encodings[0].maxBitrate = 40000;
+ parameters.encodings[0].scaleResolutionDownBy = 2;
+ parameters.encodings[1].maxBitrate = 40000;
+ await sender.setParameters(parameters);
+
+ const answer = await answerer.createAnswer();
+
+ const mungedAnswer = ridToMid(answer);
+ info(`Transformed send simulcast answer to multiple m-sections: ${answer.sdp} to ${mungedAnswer}`);
+ await offerer.setRemoteDescription({type: 'answer', sdp: mungedAnswer});
+ await answerer.setLocalDescription(answer);
+
+ is(metadataToBeLoaded.length, 2, 'Offerer should have gotten 2 ontrack events');
+ info('Waiting for 2 loadedmetadata events');
+ const videoElems = await Promise.all(metadataToBeLoaded);
+
+ const statsReady =
+ Promise.all([waitForSyncedRtcp(offerer), waitForSyncedRtcp(answerer)]);
+
+ const helper = new VideoStreamHelper();
+ info('Waiting for first video element to start playing');
+ await helper.checkVideoPlaying(videoElems[0]);
+ info('Waiting for second video element to start playing');
+ await helper.checkVideoPlaying(videoElems[1]);
+
+ is(videoElems[1].videoWidth, 50,
+ "sink is same width as source, modulo our cropping algorithm");
+ is(videoElems[1].videoHeight, 50,
+ "sink is same height as source, modulo our cropping algorithm");
+ is(videoElems[0].videoWidth, 25,
+ "sink is 1/2 width of source, modulo our cropping algorithm");
+ is(videoElems[0].videoHeight, 25,
+ "sink is 1/2 height of source, modulo our cropping algorithm");
+
+ await statsReady;
+ const senderStats = await sender.getStats();
+ checkSenderStats(senderStats, 2);
+ checkExpectedFields(senderStats);
+ pedanticChecks(senderStats);
+
+ emitter.stop();
+ videoStream.getVideoTracks()[0].stop();
+ offerer.close();
+ answerer.close();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastAnswer_lowResFirst_oldSetParameters.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastAnswer_lowResFirst_oldSetParameters.html
new file mode 100644
index 0000000000..c2aafc4575
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastAnswer_lowResFirst_oldSetParameters.html
@@ -0,0 +1,115 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script>
+ <script type="application/javascript" src="helpers_from_wpt/sdp.js"></script>
+ <script type="application/javascript" src="simulcast.js"></script>
+ <script type="application/javascript" src="stats.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1231507",
+ title: "Basic video-only peer connection with Simulcast answer, first rid has lowest resolution",
+ visible: true
+ });
+
+ runNetworkTest(async () => {
+ await pushPrefs(
+ ['media.peerconnection.simulcast', true],
+ // 180Kbps was determined empirically, set well-higher than
+ // the 80Kbps+overhead needed for the two simulcast streams.
+ // 100Kbps was apparently too low.
+ ['media.peerconnection.video.min_bitrate_estimate', 180*1000]);
+
+
+ const offerer = new RTCPeerConnection();
+ const answerer = new RTCPeerConnection();
+
+ const add = (pc, can, failed) => can && pc.addIceCandidate(can).catch(failed);
+ offerer.onicecandidate = e => add(answerer, e.candidate, generateErrorCallback());
+ answerer.onicecandidate = e => add(offerer, e.candidate, generateErrorCallback());
+
+ const metadataToBeLoaded = [];
+ offerer.ontrack = (e) => {
+ metadataToBeLoaded.push(getPlaybackWithLoadedMetadata(e.track));
+ };
+
+ // Two recv transceivers, one for each simulcast stream
+ offerer.addTransceiver('video', { direction: 'recvonly' });
+ offerer.addTransceiver('video', { direction: 'recvonly' });
+
+ // One send transceiver, that will be used to send both simulcast streams
+ const emitter = new VideoFrameEmitter();
+ const videoStream = emitter.stream();
+ answerer.addTrack(videoStream.getVideoTracks()[0], videoStream);
+ emitter.start();
+
+ const offer = await offerer.createOffer();
+
+ const mungedOffer = midToRid(offer);
+ info(`Transformed recv offer to simulcast: ${offer.sdp} to ${mungedOffer}`);
+
+ await answerer.setRemoteDescription({type: "offer", sdp: mungedOffer});
+ await offerer.setLocalDescription(offer);
+
+ const rids = offerer.getTransceivers().map(t => t.mid);
+ is(rids.length, 2, 'Should have 2 mids in offer');
+ ok(rids[0] != '', 'First mid should be non-empty');
+ ok(rids[1] != '', 'Second mid should be non-empty');
+ info(`rids: ${JSON.stringify(rids)}`);
+
+ const sender = answerer.getSenders()[0];
+ sender.setParameters({
+ encodings: [
+ { rid: rids[0], maxBitrate: 40000, scaleResolutionDownBy: 2 },
+ { rid: rids[1], maxBitrate: 40000 }
+ ]
+ });
+
+ const answer = await answerer.createAnswer();
+
+ const mungedAnswer = ridToMid(answer);
+ info(`Transformed send simulcast answer to multiple m-sections: ${answer.sdp} to ${mungedAnswer}`);
+ await offerer.setRemoteDescription({type: "answer", sdp: mungedAnswer});
+ await answerer.setLocalDescription(answer);
+
+ is(metadataToBeLoaded.length, 2, 'Offerer should have gotten 2 ontrack events');
+ info('Waiting for 2 loadedmetadata events');
+ const videoElems = await Promise.all(metadataToBeLoaded);
+
+ const statsReady =
+ Promise.all([waitForSyncedRtcp(offerer), waitForSyncedRtcp(answerer)]);
+
+ const helper = new VideoStreamHelper();
+ info('Waiting for first video element to start playing');
+ await helper.checkVideoPlaying(videoElems[0]);
+ info('Waiting for second video element to start playing');
+ await helper.checkVideoPlaying(videoElems[1]);
+
+ is(videoElems[1].videoWidth, 50,
+ "sink is same width as source, modulo our cropping algorithm");
+ is(videoElems[1].videoHeight, 50,
+ "sink is same height as source, modulo our cropping algorithm");
+ is(videoElems[0].videoWidth, 25,
+ "sink is 1/2 width of source, modulo our cropping algorithm");
+ is(videoElems[0].videoHeight, 25,
+ "sink is 1/2 height of source, modulo our cropping algorithm");
+
+ await statsReady;
+ const senderStats = await sender.getStats();
+ checkSenderStats(senderStats, 2);
+ checkExpectedFields(senderStats);
+ pedanticChecks(senderStats);
+
+ emitter.stop();
+ videoStream.getVideoTracks()[0].stop();
+ offerer.close();
+ answerer.close();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastAnswer_oldSetParameters.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastAnswer_oldSetParameters.html
new file mode 100644
index 0000000000..bc0b9f71cc
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastAnswer_oldSetParameters.html
@@ -0,0 +1,115 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script>
+ <script type="application/javascript" src="helpers_from_wpt/sdp.js"></script>
+ <script type="application/javascript" src="simulcast.js"></script>
+ <script type="application/javascript" src="stats.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1231507",
+ title: "Basic video-only peer connection with Simulcast answer",
+ visible: true
+ });
+
+ runNetworkTest(async () => {
+ await pushPrefs(
+ ['media.peerconnection.simulcast', true],
+ // 180Kbps was determined empirically, set well-higher than
+ // the 80Kbps+overhead needed for the two simulcast streams.
+ // 100Kbps was apparently too low.
+ ['media.peerconnection.video.min_bitrate_estimate', 180*1000]);
+
+
+ const offerer = new RTCPeerConnection();
+ const answerer = new RTCPeerConnection();
+
+ const add = (pc, can, failed) => can && pc.addIceCandidate(can).catch(failed);
+ offerer.onicecandidate = e => add(answerer, e.candidate, generateErrorCallback());
+ answerer.onicecandidate = e => add(offerer, e.candidate, generateErrorCallback());
+
+ const metadataToBeLoaded = [];
+ offerer.ontrack = (e) => {
+ metadataToBeLoaded.push(getPlaybackWithLoadedMetadata(e.track));
+ };
+
+ // Two recv transceivers, one for each simulcast stream
+ offerer.addTransceiver('video', { direction: 'recvonly' });
+ offerer.addTransceiver('video', { direction: 'recvonly' });
+
+ // One send transceiver, that will be used to send both simulcast streams
+ const emitter = new VideoFrameEmitter();
+ const videoStream = emitter.stream();
+ answerer.addTrack(videoStream.getVideoTracks()[0], videoStream);
+ emitter.start();
+
+ const offer = await offerer.createOffer();
+
+ const mungedOffer = midToRid(offer);
+ info(`Transformed recv offer to simulcast: ${offer.sdp} to ${mungedOffer}`);
+
+ await answerer.setRemoteDescription({type: "offer", sdp: mungedOffer});
+ await offerer.setLocalDescription(offer);
+
+ const rids = offerer.getTransceivers().map(t => t.mid);
+ is(rids.length, 2, 'Should have 2 mids in offer');
+ ok(rids[0] != '', 'First mid should be non-empty');
+ ok(rids[1] != '', 'Second mid should be non-empty');
+ info(`rids: ${JSON.stringify(rids)}`);
+
+ const sender = answerer.getSenders()[0];
+ await sender.setParameters({
+ encodings: [
+ { rid: rids[0], maxBitrate: 40000 },
+ { rid: rids[1], maxBitrate: 40000, scaleResolutionDownBy: 2 }
+ ]
+ });
+
+ const answer = await answerer.createAnswer();
+
+ const mungedAnswer = ridToMid(answer);
+ info(`Transformed send simulcast answer to multiple m-sections: ${answer.sdp} to ${mungedAnswer}`);
+ await offerer.setRemoteDescription({type: "answer", sdp: mungedAnswer});
+ await answerer.setLocalDescription(answer);
+
+ is(metadataToBeLoaded.length, 2, 'Offerer should have gotten 2 ontrack events');
+ info('Waiting for 2 loadedmetadata events');
+ const videoElems = await Promise.all(metadataToBeLoaded);
+
+ const statsReady =
+ Promise.all([waitForSyncedRtcp(offerer), waitForSyncedRtcp(answerer)]);
+
+ const helper = new VideoStreamHelper();
+ info('Waiting for first video element to start playing');
+ await helper.checkVideoPlaying(videoElems[0]);
+ info('Waiting for second video element to start playing');
+ await helper.checkVideoPlaying(videoElems[1]);
+
+ is(videoElems[0].videoWidth, 50,
+ "sink is same width as source, modulo our cropping algorithm");
+ is(videoElems[0].videoHeight, 50,
+ "sink is same height as source, modulo our cropping algorithm");
+ is(videoElems[1].videoWidth, 25,
+ "sink is 1/2 width of source, modulo our cropping algorithm");
+ is(videoElems[1].videoHeight, 25,
+ "sink is 1/2 height of source, modulo our cropping algorithm");
+
+ await statsReady;
+ const senderStats = await sender.getStats();
+ checkSenderStats(senderStats, 2);
+ checkExpectedFields(senderStats);
+ pedanticChecks(senderStats);
+
+ emitter.stop();
+ videoStream.getVideoTracks()[0].stop();
+ offerer.close();
+ answerer.close();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastOddResolution.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastOddResolution.html
new file mode 100644
index 0000000000..c380b34f1a
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastOddResolution.html
@@ -0,0 +1,183 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script>
+ <script type="application/javascript" src="helpers_from_wpt/sdp.js"></script>
+ <script type="application/javascript" src="simulcast.js"></script>
+ <script type="application/javascript" src="stats.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1432793",
+ title: "Simulcast with odd resolution",
+ visible: true
+ });
+
+ runNetworkTest(async () => {
+ const helper = new VideoStreamHelper();
+ const emitter = new VideoFrameEmitter(helper.green, helper.red, 705, 528);
+
+ async function checkVideoElement(senderElement, receiverElement, encoding) {
+ info(`Waiting for receiver video element ${encoding.rid} to start playing`);
+ await helper.checkVideoPlaying(receiverElement);
+ const srcWidth = senderElement.videoWidth;
+ const srcHeight = senderElement.videoHeight;
+ info(`Source resolution is ${srcWidth}x${srcHeight}`);
+
+ const scaleDownBy = encoding.scaleResolutionDownBy;
+ const expectedWidth = srcWidth / scaleDownBy;
+ const expectedHeight = srcHeight / scaleDownBy;
+ const margin = srcWidth * 0.1;
+ const width = receiverElement.videoWidth;
+ const height = receiverElement.videoHeight;
+ const rid = encoding.rid;
+ ok(width >= expectedWidth - margin && width <= expectedWidth + margin,
+ `Width ${width} should be within 10% of ${expectedWidth} for rid '${rid}'`);
+ ok(height >= expectedHeight - margin && height <= expectedHeight + margin,
+ `Height ${height} should be within 10% of ${expectedHeight} for rid '${rid}'`);
+ }
+
+ async function checkVideoElements(senderElement, receiverElements, encodings) {
+ is(receiverElements.length, encodings.length, 'Number of video elements should match number of encodings');
+ info('Waiting for sender video element to start playing');
+ await helper.checkVideoPlaying(senderElement);
+ for (let i = 0; i < encodings.length; i++) {
+ await checkVideoElement(senderElement, receiverElements[i], encodings[i]);
+ }
+ }
+
+ const sendEncodings = [{ rid: "0", maxBitrate: 40000, scaleResolutionDownBy: 1.9 },
+ { rid: "1", maxBitrate: 40000, scaleResolutionDownBy: 3.5 },
+ { rid: "2", maxBitrate: 40000, scaleResolutionDownBy: 17.8 }];
+
+ async function checkSenderStats(sender) {
+ const senderStats = await sender.getStats();
+ checkSenderStats(senderStats, sendEncodings.length);
+ checkExpectedFields(senderStats);
+ pedanticChecks(senderStats);
+ }
+
+ async function waitForResizeEvents(elements) {
+ return Promise.all(elements.map(elem => haveEvent(elem, 'resize')));
+ }
+
+ await pushPrefs(
+ // 180Kbps was determined empirically, set well-higher than
+ // the 80Kbps+overhead needed for the two simulcast streams.
+ // 100Kbps was apparently too low.
+ ['media.peerconnection.video.min_bitrate_estimate', 180*1000]);
+
+
+ const offerer = new RTCPeerConnection();
+ const answerer = new RTCPeerConnection();
+
+ const add = (pc, can, failed) => can && pc.addIceCandidate(can).catch(failed);
+ offerer.onicecandidate = e => add(answerer, e.candidate, generateErrorCallback());
+ answerer.onicecandidate = e => add(offerer, e.candidate, generateErrorCallback());
+
+ const metadataToBeLoaded = [];
+ answerer.ontrack = (e) => {
+ metadataToBeLoaded.push(getPlaybackWithLoadedMetadata(e.track));
+ };
+
+ // One send transceiver, that will be used to send both simulcast streams
+ const videoStream = emitter.stream();
+ offerer.addTransceiver(videoStream.getVideoTracks()[0], {sendEncodings});
+ const senderElement = document.createElement('video');
+ senderElement.autoplay = true;
+ senderElement.srcObject = videoStream;
+ senderElement.id = videoStream.id
+
+ const sender = offerer.getSenders()[0];
+ let parameters = sender.getParameters();
+ is(parameters.encodings[0].maxBitrate, sendEncodings[0].maxBitrate);
+ isfuzzy(parameters.encodings[0].scaleResolutionDownBy,
+ sendEncodings[0].scaleResolutionDownBy, 0.01);
+ is(parameters.encodings[1].maxBitrate, sendEncodings[1].maxBitrate);
+ isfuzzy(parameters.encodings[1].scaleResolutionDownBy,
+ sendEncodings[1].scaleResolutionDownBy, 0.01);
+ is(parameters.encodings[2].maxBitrate, sendEncodings[2].maxBitrate);
+ isfuzzy(parameters.encodings[2].scaleResolutionDownBy,
+ sendEncodings[2].scaleResolutionDownBy, 0.01);
+
+ const offer = await offerer.createOffer();
+
+ const mungedOffer = ridToMid(offer);
+ info(`Transformed send simulcast offer to multiple m-sections: ${offer.sdp} to ${mungedOffer}`);
+
+ await answerer.setRemoteDescription({type: 'offer', sdp: mungedOffer});
+ await offerer.setLocalDescription(offer);
+
+ const rids = answerer.getTransceivers().map(t => t.mid);
+ is(rids.length, 3, 'Should have 3 mids in offer');
+ ok(rids[0], 'First mid should be non-empty');
+ ok(rids[1], 'Second mid should be non-empty');
+ ok(rids[2], 'Third mid should be non-empty');
+ info(`rids: ${JSON.stringify(rids)}`);
+
+ const answer = await answerer.createAnswer();
+
+ const mungedAnswer = midToRid(answer);
+ info(`Transformed recv answer to simulcast: ${answer.sdp} to ${mungedAnswer}`);
+ await offerer.setRemoteDescription({type: 'answer', sdp: mungedAnswer});
+ await answerer.setLocalDescription(answer);
+
+ is(metadataToBeLoaded.length, 3, 'Offerer should have gotten 3 ontrack events');
+ emitter.start();
+ info('Waiting for 3 loadedmetadata events');
+ const videoElems = await Promise.all(metadataToBeLoaded);
+ await checkVideoElements(senderElement, videoElems, parameters.encodings);
+ emitter.stop();
+
+ await Promise.all([waitForSyncedRtcp(offerer), waitForSyncedRtcp(answerer)]);
+
+ info(`Changing source resolution to 1280x720`);
+ emitter.size(1280, 720);
+ emitter.start();
+ await waitForResizeEvents([senderElement, ...videoElems]);
+ await checkVideoElements(senderElement, videoElems, parameters.encodings);
+ await checkSenderStats(sender);
+
+ parameters = sender.getParameters();
+ parameters.encodings[0].scaleResolutionDownBy = 1;
+ parameters.encodings[1].scaleResolutionDownBy = 2;
+ parameters.encodings[2].scaleResolutionDownBy = 3;
+ info(`Changing encodings to ${JSON.stringify(parameters.encodings)}`);
+ await sender.setParameters(parameters);
+ await waitForResizeEvents(videoElems);
+ await checkVideoElements(senderElement, videoElems, parameters.encodings);
+ await checkSenderStats(sender);
+
+ parameters = sender.getParameters();
+ parameters.encodings[0].scaleResolutionDownBy = 6;
+ parameters.encodings[1].scaleResolutionDownBy = 5;
+ parameters.encodings[2].scaleResolutionDownBy = 4;
+ info(`Changing encodings to ${JSON.stringify(parameters.encodings)}`);
+ await sender.setParameters(parameters);
+ await waitForResizeEvents(videoElems);
+ await checkVideoElements(senderElement, videoElems, parameters.encodings);
+ await checkSenderStats(sender);
+
+ parameters = sender.getParameters();
+ parameters.encodings[0].scaleResolutionDownBy = 4;
+ parameters.encodings[1].scaleResolutionDownBy = 1;
+ parameters.encodings[2].scaleResolutionDownBy = 2;
+ info(`Changing encodings to ${JSON.stringify(parameters.encodings)}`);
+ await sender.setParameters(parameters);
+ await waitForResizeEvents(videoElems);
+ await checkVideoElements(senderElement, videoElems, parameters.encodings);
+ await checkSenderStats(sender);
+
+ emitter.stop();
+ videoStream.getVideoTracks()[0].stop();
+ offerer.close();
+ answerer.close();
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastOddResolution_oldSetParameters.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastOddResolution_oldSetParameters.html
new file mode 100644
index 0000000000..0f6d3c8520
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastOddResolution_oldSetParameters.html
@@ -0,0 +1,172 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script>
+ <script type="application/javascript" src="helpers_from_wpt/sdp.js"></script>
+ <script type="application/javascript" src="simulcast.js"></script>
+ <script type="application/javascript" src="stats.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1432793",
+ title: "Simulcast with odd resolution",
+ visible: true
+ });
+
+ runNetworkTest(async () => {
+ const helper = new VideoStreamHelper();
+ const emitter = new VideoFrameEmitter(helper.green, helper.red, 705, 528);
+
+ async function checkVideoElement(senderElement, receiverElement, encoding) {
+ info(`Waiting for receiver video element ${encoding.rid} to start playing`);
+ await helper.checkVideoPlaying(receiverElement);
+ const srcWidth = senderElement.videoWidth;
+ const srcHeight = senderElement.videoHeight;
+ info(`Source resolution is ${srcWidth}x${srcHeight}`);
+
+ const scaleDownBy = encoding.scaleResolutionDownBy;
+ const expectedWidth = srcWidth / scaleDownBy;
+ const expectedHeight = srcHeight / scaleDownBy;
+ const margin = srcWidth * 0.1;
+ const width = receiverElement.videoWidth;
+ const height = receiverElement.videoHeight;
+ const rid = encoding.rid;
+ ok(width >= expectedWidth - margin && width <= expectedWidth + margin,
+ `Width ${width} should be within 10% of ${expectedWidth} for rid '${rid}'`);
+ ok(height >= expectedHeight - margin && height <= expectedHeight + margin,
+ `Height ${height} should be within 10% of ${expectedHeight} for rid '${rid}'`);
+ }
+
+ async function checkVideoElements(senderElement, receiverElements, encodings) {
+ is(receiverElements.length, encodings.length, 'Number of video elements should match number of encodings');
+ info('Waiting for sender video element to start playing');
+ await helper.checkVideoPlaying(senderElement);
+ for (let i = 0; i < encodings.length; i++) {
+ await checkVideoElement(senderElement, receiverElements[i], encodings[i]);
+ }
+ }
+
+ async function checkSenderStats(sender) {
+ const senderStats = await sender.getStats();
+ checkSenderStats(senderStats, encodings.length);
+ checkExpectedFields(senderStats);
+ pedanticChecks(senderStats);
+ }
+
+ async function waitForResizeEvents(elements) {
+ return Promise.all(elements.map(elem => haveEvent(elem, 'resize')));
+ }
+
+ const encodings = [{ rid: "0", maxBitrate: 40000, scaleResolutionDownBy: 1.9 },
+ { rid: "1", maxBitrate: 40000, scaleResolutionDownBy: 3.5 },
+ { rid: "2", maxBitrate: 40000, scaleResolutionDownBy: 17.8 }];
+
+ await pushPrefs(
+ ['media.peerconnection.simulcast', true],
+ // 180Kbps was determined empirically, set well-higher than
+ // the 80Kbps+overhead needed for the two simulcast streams.
+ // 100Kbps was apparently too low.
+ ['media.peerconnection.video.min_bitrate_estimate', 180*1000]);
+
+
+ const offerer = new RTCPeerConnection();
+ const answerer = new RTCPeerConnection();
+
+ const add = (pc, can, failed) => can && pc.addIceCandidate(can).catch(failed);
+ offerer.onicecandidate = e => add(answerer, e.candidate, generateErrorCallback());
+ answerer.onicecandidate = e => add(offerer, e.candidate, generateErrorCallback());
+
+ const metadataToBeLoaded = [];
+ answerer.ontrack = (e) => {
+ metadataToBeLoaded.push(getPlaybackWithLoadedMetadata(e.track));
+ };
+
+ // One send transceiver, that will be used to send both simulcast streams
+ const videoStream = emitter.stream();
+ offerer.addTrack(videoStream.getVideoTracks()[0], videoStream);
+ const senderElement = document.createElement('video');
+ senderElement.autoplay = true;
+ senderElement.srcObject = videoStream;
+ senderElement.id = videoStream.id
+
+ const sender = offerer.getSenders()[0];
+ sender.setParameters({encodings});
+
+ const offer = await offerer.createOffer();
+
+ const mungedOffer = ridToMid(offer);
+ info(`Transformed send simulcast offer to multiple m-sections: ${offer.sdp} to ${mungedOffer}`);
+
+ await answerer.setRemoteDescription({type: "offer", sdp: mungedOffer});
+ await offerer.setLocalDescription(offer);
+
+ const rids = answerer.getTransceivers().map(t => t.mid);
+ is(rids.length, 3, 'Should have 3 mids in offer');
+ ok(rids[0], 'First mid should be non-empty');
+ ok(rids[1], 'Second mid should be non-empty');
+ ok(rids[2], 'Third mid should be non-empty');
+ info(`rids: ${JSON.stringify(rids)}`);
+
+ const answer = await answerer.createAnswer();
+
+ const mungedAnswer = midToRid(answer);
+ info(`Transformed recv answer to simulcast: ${answer.sdp} to ${mungedAnswer}`);
+ await offerer.setRemoteDescription({type: "answer", sdp: mungedAnswer});
+ await answerer.setLocalDescription(answer);
+
+ is(metadataToBeLoaded.length, 3, 'Offerer should have gotten 3 ontrack events');
+ emitter.start();
+ info('Waiting for 3 loadedmetadata events');
+ const videoElems = await Promise.all(metadataToBeLoaded);
+ await checkVideoElements(senderElement, videoElems, encodings);
+ emitter.stop();
+
+ await Promise.all([waitForSyncedRtcp(offerer), waitForSyncedRtcp(answerer)]);
+
+ info(`Changing source resolution to 1280x720`);
+ emitter.size(1280, 720);
+ emitter.start();
+ await waitForResizeEvents([senderElement, ...videoElems]);
+ await checkVideoElements(senderElement, videoElems, encodings);
+ await checkSenderStats(sender);
+
+ encodings[0].scaleResolutionDownBy = 1;
+ encodings[1].scaleResolutionDownBy = 2;
+ encodings[2].scaleResolutionDownBy = 3;
+ info(`Changing encodings to ${JSON.stringify(encodings)}`);
+ await sender.setParameters({encodings});
+ await waitForResizeEvents(videoElems);
+ await checkVideoElements(senderElement, videoElems, encodings);
+ await checkSenderStats(sender);
+
+ encodings[0].scaleResolutionDownBy = 6;
+ encodings[1].scaleResolutionDownBy = 5;
+ encodings[2].scaleResolutionDownBy = 4;
+ info(`Changing encodings to ${JSON.stringify(encodings)}`);
+ await sender.setParameters({encodings});
+ await waitForResizeEvents(videoElems);
+ await checkVideoElements(senderElement, videoElems, encodings);
+ await checkSenderStats(sender);
+
+ encodings[0].scaleResolutionDownBy = 4;
+ encodings[1].scaleResolutionDownBy = 1;
+ encodings[2].scaleResolutionDownBy = 2;
+ info(`Changing encodings to ${JSON.stringify(encodings)}`);
+ await sender.setParameters({encodings});
+ await waitForResizeEvents(videoElems);
+ await checkVideoElements(senderElement, videoElems, encodings);
+ await checkSenderStats(sender);
+
+ emitter.stop();
+ videoStream.getVideoTracks()[0].stop();
+ offerer.close();
+ answerer.close();
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastOffer.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastOffer.html
new file mode 100644
index 0000000000..cb7c13a0d1
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastOffer.html
@@ -0,0 +1,109 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="parser_rtp.js"></script>
+ <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script>
+ <script type="application/javascript" src="helpers_from_wpt/sdp.js"></script>
+ <script type="application/javascript" src="simulcast.js"></script>
+ <script type="application/javascript" src="stats.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1231507",
+ title: "Basic video-only peer connection with Simulcast offer",
+ visible: true
+ });
+
+ runNetworkTest(async () => {
+ await pushPrefs(
+ // 180Kbps was determined empirically, set well-higher than
+ // the 80Kbps+overhead needed for the two simulcast streams.
+ // 100Kbps was apparently too low.
+ ['media.peerconnection.video.min_bitrate_estimate', 180*1000]);
+
+
+ const offerer = new RTCPeerConnection();
+ const answerer = new RTCPeerConnection();
+
+ const add = (pc, can, failed) => can && pc.addIceCandidate(can).catch(failed);
+ offerer.onicecandidate = e => add(answerer, e.candidate, generateErrorCallback());
+ answerer.onicecandidate = e => add(offerer, e.candidate, generateErrorCallback());
+
+ const metadataToBeLoaded = [];
+ answerer.ontrack = (e) => {
+ metadataToBeLoaded.push(getPlaybackWithLoadedMetadata(e.track));
+ };
+
+ // One send transceiver, that will be used to send both simulcast streams
+ const emitter = new VideoFrameEmitter();
+ const videoStream = emitter.stream();
+ const sendEncodings = [
+ { rid: '0', maxBitrate: 40000 },
+ { rid: '1', maxBitrate: 40000, scaleResolutionDownBy: 2 }
+ ];
+ offerer.addTransceiver(videoStream.getVideoTracks()[0], {sendEncodings});
+ emitter.start();
+
+ const sender = offerer.getSenders()[0];
+
+ const offer = await offerer.createOffer();
+
+ const mungedOffer = ridToMid(offer);
+ info(`Transformed send simulcast offer to multiple m-sections: ${offer.sdp} to ${mungedOffer}`);
+
+ await answerer.setRemoteDescription({type: 'offer', sdp: mungedOffer});
+ await offerer.setLocalDescription(offer);
+
+ const rids = answerer.getTransceivers().map(t => t.mid);
+ is(rids.length, 2, 'Should have 2 mids in offer');
+ ok(rids[0] != '', 'First mid should be non-empty');
+ ok(rids[1] != '', 'Second mid should be non-empty');
+ info(`rids: ${JSON.stringify(rids)}`);
+
+ const answer = await answerer.createAnswer();
+
+ const mungedAnswer = midToRid(answer);
+ info(`Transformed recv answer to simulcast: ${answer.sdp} to ${mungedAnswer}`);
+ await offerer.setRemoteDescription({type: 'answer', sdp: mungedAnswer});
+ await answerer.setLocalDescription(answer);
+
+ is(metadataToBeLoaded.length, 2, 'Offerer should have gotten 2 ontrack events');
+ info('Waiting for 2 loadedmetadata events');
+ const videoElems = await Promise.all(metadataToBeLoaded);
+
+ const statsReady =
+ Promise.all([waitForSyncedRtcp(offerer), waitForSyncedRtcp(answerer)]);
+
+ const helper = new VideoStreamHelper();
+ info('Waiting for first video element to start playing');
+ await helper.checkVideoPlaying(videoElems[0]);
+ info('Waiting for second video element to start playing');
+ await helper.checkVideoPlaying(videoElems[1]);
+
+ is(videoElems[0].videoWidth, 50,
+ "sink is same width as source, modulo our cropping algorithm");
+ is(videoElems[0].videoHeight, 50,
+ "sink is same height as source, modulo our cropping algorithm");
+ is(videoElems[1].videoWidth, 25,
+ "sink is 1/2 width of source, modulo our cropping algorithm");
+ is(videoElems[1].videoHeight, 25,
+ "sink is 1/2 height of source, modulo our cropping algorithm");
+
+ await statsReady;
+ const senderStats = await sender.getStats();
+ checkSenderStats(senderStats, 2);
+ checkExpectedFields(senderStats);
+ pedanticChecks(senderStats);
+
+ emitter.stop();
+ videoStream.getVideoTracks()[0].stop();
+ offerer.close();
+ answerer.close();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastOffer_lowResFirst.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastOffer_lowResFirst.html
new file mode 100644
index 0000000000..93141311f1
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastOffer_lowResFirst.html
@@ -0,0 +1,109 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="parser_rtp.js"></script>
+ <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script>
+ <script type="application/javascript" src="helpers_from_wpt/sdp.js"></script>
+ <script type="application/javascript" src="simulcast.js"></script>
+ <script type="application/javascript" src="stats.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1231507",
+ title: "Basic video-only peer connection with Simulcast offer, first rid has lowest resolution",
+ visible: true
+ });
+
+ runNetworkTest(async () => {
+ await pushPrefs(
+ // 180Kbps was determined empirically, set well-higher than
+ // the 80Kbps+overhead needed for the two simulcast streams.
+ // 100Kbps was apparently too low.
+ ['media.peerconnection.video.min_bitrate_estimate', 180*1000]);
+
+
+ const offerer = new RTCPeerConnection();
+ const answerer = new RTCPeerConnection();
+
+ const add = (pc, can, failed) => can && pc.addIceCandidate(can).catch(failed);
+ offerer.onicecandidate = e => add(answerer, e.candidate, generateErrorCallback());
+ answerer.onicecandidate = e => add(offerer, e.candidate, generateErrorCallback());
+
+ const metadataToBeLoaded = [];
+ answerer.ontrack = (e) => {
+ metadataToBeLoaded.push(getPlaybackWithLoadedMetadata(e.track));
+ };
+
+ // One send transceiver, that will be used to send both simulcast streams
+ const emitter = new VideoFrameEmitter();
+ const videoStream = emitter.stream();
+ const sendEncodings = [
+ { rid: '0', maxBitrate: 40000, scaleResolutionDownBy: 2 },
+ { rid: '1', maxBitrate: 40000 }
+ ];
+ offerer.addTransceiver(videoStream.getVideoTracks()[0], {sendEncodings});
+ emitter.start();
+
+ const sender = offerer.getSenders()[0];
+
+ const offer = await offerer.createOffer();
+
+ const mungedOffer = ridToMid(offer);
+ info(`Transformed send simulcast offer to multiple m-sections: ${offer.sdp} to ${mungedOffer}`);
+
+ await answerer.setRemoteDescription({type: 'offer', sdp: mungedOffer});
+ await offerer.setLocalDescription(offer);
+
+ const rids = answerer.getTransceivers().map(t => t.mid);
+ is(rids.length, 2, 'Should have 2 mids in offer');
+ ok(rids[0] != '', 'First mid should be non-empty');
+ ok(rids[1] != '', 'Second mid should be non-empty');
+ info(`rids: ${JSON.stringify(rids)}`);
+
+ const answer = await answerer.createAnswer();
+
+ const mungedAnswer = midToRid(answer);
+ info(`Transformed recv answer to simulcast: ${answer.sdp} to ${mungedAnswer}`);
+ await offerer.setRemoteDescription({type: 'answer', sdp: mungedAnswer});
+ await answerer.setLocalDescription(answer);
+
+ is(metadataToBeLoaded.length, 2, 'Offerer should have gotten 2 ontrack events');
+ info('Waiting for 2 loadedmetadata events');
+ const videoElems = await Promise.all(metadataToBeLoaded);
+
+ const statsReady =
+ Promise.all([waitForSyncedRtcp(offerer), waitForSyncedRtcp(answerer)]);
+
+ const helper = new VideoStreamHelper();
+ info('Waiting for first video element to start playing');
+ await helper.checkVideoPlaying(videoElems[0]);
+ info('Waiting for second video element to start playing');
+ await helper.checkVideoPlaying(videoElems[1]);
+
+ is(videoElems[1].videoWidth, 50,
+ "sink is same width as source, modulo our cropping algorithm");
+ is(videoElems[1].videoHeight, 50,
+ "sink is same height as source, modulo our cropping algorithm");
+ is(videoElems[0].videoWidth, 25,
+ "sink is 1/2 width of source, modulo our cropping algorithm");
+ is(videoElems[0].videoHeight, 25,
+ "sink is 1/2 height of source, modulo our cropping algorithm");
+
+ await statsReady;
+ const senderStats = await sender.getStats();
+ checkSenderStats(senderStats, 2);
+ checkExpectedFields(senderStats);
+ pedanticChecks(senderStats);
+
+ emitter.stop();
+ videoStream.getVideoTracks()[0].stop();
+ offerer.close();
+ answerer.close();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastOffer_lowResFirst_oldSetParameters.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastOffer_lowResFirst_oldSetParameters.html
new file mode 100644
index 0000000000..73e2d38eb2
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastOffer_lowResFirst_oldSetParameters.html
@@ -0,0 +1,112 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="parser_rtp.js"></script>
+ <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script>
+ <script type="application/javascript" src="helpers_from_wpt/sdp.js"></script>
+ <script type="application/javascript" src="simulcast.js"></script>
+ <script type="application/javascript" src="stats.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1231507",
+ title: "Basic video-only peer connection with Simulcast offer, first rid has lowest resolution",
+ visible: true
+ });
+
+ runNetworkTest(async () => {
+ await pushPrefs(
+ ['media.peerconnection.simulcast', true],
+ // 180Kbps was determined empirically, set well-higher than
+ // the 80Kbps+overhead needed for the two simulcast streams.
+ // 100Kbps was apparently too low.
+ ['media.peerconnection.video.min_bitrate_estimate', 180*1000]);
+
+
+ const offerer = new RTCPeerConnection();
+ const answerer = new RTCPeerConnection();
+
+ const add = (pc, can, failed) => can && pc.addIceCandidate(can).catch(failed);
+ offerer.onicecandidate = e => add(answerer, e.candidate, generateErrorCallback());
+ answerer.onicecandidate = e => add(offerer, e.candidate, generateErrorCallback());
+
+ const metadataToBeLoaded = [];
+ answerer.ontrack = (e) => {
+ metadataToBeLoaded.push(getPlaybackWithLoadedMetadata(e.track));
+ };
+
+ // One send transceiver, that will be used to send both simulcast streams
+ const emitter = new VideoFrameEmitter();
+ const videoStream = emitter.stream();
+ offerer.addTrack(videoStream.getVideoTracks()[0], videoStream);
+ emitter.start();
+
+ const sender = offerer.getSenders()[0];
+ sender.setParameters({
+ encodings: [
+ { rid: '0', maxBitrate: 40000, scaleResolutionDownBy: 2 },
+ { rid: '1', maxBitrate: 40000 }
+ ]
+ });
+
+ const offer = await offerer.createOffer();
+
+ const mungedOffer = ridToMid(offer);
+ info(`Transformed send simulcast offer to multiple m-sections: ${offer.sdp} to ${mungedOffer}`);
+
+ await answerer.setRemoteDescription({type: 'offer', sdp: mungedOffer});
+ await offerer.setLocalDescription(offer);
+
+ const rids = answerer.getTransceivers().map(t => t.mid);
+ is(rids.length, 2, 'Should have 2 mids in offer');
+ ok(rids[0] != '', 'First mid should be non-empty');
+ ok(rids[1] != '', 'Second mid should be non-empty');
+ info(`rids: ${JSON.stringify(rids)}`);
+
+ const answer = await answerer.createAnswer();
+
+ const mungedAnswer = midToRid(answer);
+ info(`Transformed recv answer to simulcast: ${answer.sdp} to ${mungedAnswer}`);
+ await offerer.setRemoteDescription({type: 'answer', sdp: mungedAnswer});
+ await answerer.setLocalDescription(answer);
+
+ is(metadataToBeLoaded.length, 2, 'Offerer should have gotten 2 ontrack events');
+ info('Waiting for 2 loadedmetadata events');
+ const videoElems = await Promise.all(metadataToBeLoaded);
+
+ const statsReady =
+ Promise.all([waitForSyncedRtcp(offerer), waitForSyncedRtcp(answerer)]);
+
+ const helper = new VideoStreamHelper();
+ info('Waiting for first video element to start playing');
+ await helper.checkVideoPlaying(videoElems[0]);
+ info('Waiting for second video element to start playing');
+ await helper.checkVideoPlaying(videoElems[1]);
+
+ is(videoElems[1].videoWidth, 50,
+ "sink is same width as source, modulo our cropping algorithm");
+ is(videoElems[1].videoHeight, 50,
+ "sink is same height as source, modulo our cropping algorithm");
+ is(videoElems[0].videoWidth, 25,
+ "sink is 1/2 width of source, modulo our cropping algorithm");
+ is(videoElems[0].videoHeight, 25,
+ "sink is 1/2 height of source, modulo our cropping algorithm");
+
+ await statsReady;
+ const senderStats = await sender.getStats();
+ checkSenderStats(senderStats, 2);
+ checkExpectedFields(senderStats);
+ pedanticChecks(senderStats);
+
+ emitter.stop();
+ videoStream.getVideoTracks()[0].stop();
+ offerer.close();
+ answerer.close();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastOffer_oldSetParameters.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastOffer_oldSetParameters.html
new file mode 100644
index 0000000000..551273af5e
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastOffer_oldSetParameters.html
@@ -0,0 +1,112 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="parser_rtp.js"></script>
+ <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script>
+ <script type="application/javascript" src="helpers_from_wpt/sdp.js"></script>
+ <script type="application/javascript" src="simulcast.js"></script>
+ <script type="application/javascript" src="stats.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1231507",
+ title: "Basic video-only peer connection with Simulcast offer",
+ visible: true
+ });
+
+ runNetworkTest(async () => {
+ await pushPrefs(
+ ['media.peerconnection.simulcast', true],
+ // 180Kbps was determined empirically, set well-higher than
+ // the 80Kbps+overhead needed for the two simulcast streams.
+ // 100Kbps was apparently too low.
+ ['media.peerconnection.video.min_bitrate_estimate', 180*1000]);
+
+
+ const offerer = new RTCPeerConnection();
+ const answerer = new RTCPeerConnection();
+
+ const add = (pc, can, failed) => can && pc.addIceCandidate(can).catch(failed);
+ offerer.onicecandidate = e => add(answerer, e.candidate, generateErrorCallback());
+ answerer.onicecandidate = e => add(offerer, e.candidate, generateErrorCallback());
+
+ const metadataToBeLoaded = [];
+ answerer.ontrack = (e) => {
+ metadataToBeLoaded.push(getPlaybackWithLoadedMetadata(e.track));
+ };
+
+ // One send transceiver, that will be used to send both simulcast streams
+ const emitter = new VideoFrameEmitter();
+ const videoStream = emitter.stream();
+ offerer.addTrack(videoStream.getVideoTracks()[0], videoStream);
+ emitter.start();
+
+ const sender = offerer.getSenders()[0];
+ sender.setParameters({
+ encodings: [
+ { rid: '0', maxBitrate: 40000 },
+ { rid: '1', maxBitrate: 40000, scaleResolutionDownBy: 2 }
+ ]
+ });
+
+ const offer = await offerer.createOffer();
+
+ const mungedOffer = ridToMid(offer);
+ info(`Transformed send simulcast offer to multiple m-sections: ${offer.sdp} to ${mungedOffer}`);
+
+ await answerer.setRemoteDescription({type: "offer", sdp: mungedOffer});
+ await offerer.setLocalDescription(offer);
+
+ const rids = answerer.getTransceivers().map(t => t.mid);
+ is(rids.length, 2, 'Should have 2 mids in offer');
+ ok(rids[0] != '', 'First mid should be non-empty');
+ ok(rids[1] != '', 'Second mid should be non-empty');
+ info(`rids: ${JSON.stringify(rids)}`);
+
+ const answer = await answerer.createAnswer();
+
+ const mungedAnswer = midToRid(answer);
+ info(`Transformed recv answer to simulcast: ${answer.sdp} to ${mungedAnswer}`);
+ await offerer.setRemoteDescription({type: "answer", sdp: mungedAnswer});
+ await answerer.setLocalDescription(answer);
+
+ is(metadataToBeLoaded.length, 2, 'Offerer should have gotten 2 ontrack events');
+ info('Waiting for 2 loadedmetadata events');
+ const videoElems = await Promise.all(metadataToBeLoaded);
+
+ const statsReady =
+ Promise.all([waitForSyncedRtcp(offerer), waitForSyncedRtcp(answerer)]);
+
+ const helper = new VideoStreamHelper();
+ info('Waiting for first video element to start playing');
+ await helper.checkVideoPlaying(videoElems[0]);
+ info('Waiting for second video element to start playing');
+ await helper.checkVideoPlaying(videoElems[1]);
+
+ is(videoElems[0].videoWidth, 50,
+ "sink is same width as source, modulo our cropping algorithm");
+ is(videoElems[0].videoHeight, 50,
+ "sink is same height as source, modulo our cropping algorithm");
+ is(videoElems[1].videoWidth, 25,
+ "sink is 1/2 width of source, modulo our cropping algorithm");
+ is(videoElems[1].videoHeight, 25,
+ "sink is 1/2 height of source, modulo our cropping algorithm");
+
+ await statsReady;
+ const senderStats = await sender.getStats();
+ checkSenderStats(senderStats, 2);
+ checkExpectedFields(senderStats);
+ pedanticChecks(senderStats);
+
+ emitter.stop();
+ videoStream.getVideoTracks()[0].stop();
+ offerer.close();
+ answerer.close();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_stats.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_stats.html
new file mode 100644
index 0000000000..2ef98dc9c8
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_stats.html
@@ -0,0 +1,42 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="stats.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+ bug: "1337525",
+ title: "webRtc Stats composition and sanity"
+});
+
+runNetworkTest(async function (options) {
+ // We don't know how to get QP value when using Android system codecs.
+ if (navigator.userAgent.includes("Android")) {
+ await pushPrefs(["media.navigator.mediadatadecoder_vpx_enabled", false],
+ ["media.webrtc.hw.h264.enabled", false]);
+ }
+
+ // For accurate comparisons of `remoteTimestamp` (not using reduced precision)
+ // to `timestamp` (using reduced precision).
+ await pushPrefs(["privacy.resistFingerprinting.reduceTimerPrecision.jitter",
+ false]);
+
+ const test = new PeerConnectionTest(options);
+
+ test.chain.insertAfter("PC_LOCAL_WAIT_FOR_MEDIA_FLOW",
+ [PC_LOCAL_TEST_LOCAL_STATS]);
+
+ test.chain.insertAfter("PC_REMOTE_WAIT_FOR_MEDIA_FLOW",
+ [PC_REMOTE_TEST_REMOTE_STATS]);
+
+ test.setMediaConstraints([{audio: true}, {video: true}],
+ [{audio: true}, {video: true}]);
+ await test.run();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_stats_jitter.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_stats_jitter.html
new file mode 100644
index 0000000000..6e1ef698b4
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_stats_jitter.html
@@ -0,0 +1,58 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="stats.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1672590",
+ title: "Jitter sanity check"
+ });
+
+const checkJitter = stats => {
+ stats.forEach((stat, mapKey) => {
+ if (stat.type == "remote-inbound-rtp") {
+ // This should be much lower for audio, TODO: Bug 1330575
+ const expectedJitter = stat.kind == "video" ? 0.5 : 1;
+
+ ok(stat.jitter < expectedJitter,
+ stat.type + ".jitter is sane number for a local only test. value="
+ + stat.jitter);
+ }
+ });
+};
+
+const PC_LOCAL_TEST_LOCAL_JITTER = async test => {
+ checkJitter(await waitForSyncedRtcp(test.pcLocal._pc));
+}
+
+const PC_REMOTE_TEST_REMOTE_JITTER = async test => {
+ checkJitter(await waitForSyncedRtcp(test.pcRemote._pc));
+}
+
+runNetworkTest(async function (options) {
+ // We don't know how to get QP value when using Android system codecs.
+ if (navigator.userAgent.includes("Android")) {
+ await pushPrefs(["media.navigator.mediadatadecoder_vpx_enabled", false],
+ ["media.webrtc.hw.h264.enabled", false]);
+ }
+
+ const test = new PeerConnectionTest(options);
+
+ test.chain.insertAfter("PC_LOCAL_WAIT_FOR_MEDIA_FLOW",
+ [PC_LOCAL_TEST_LOCAL_JITTER]);
+
+ test.chain.insertAfter("PC_REMOTE_WAIT_FOR_MEDIA_FLOW",
+ [PC_REMOTE_TEST_REMOTE_JITTER]);
+
+ test.setMediaConstraints([{audio: true}, {video: true}],
+ [{audio: true}, {video: true}]);
+ await test.run();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_stats_oneway.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_stats_oneway.html
new file mode 100644
index 0000000000..02ace530a9
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_stats_oneway.html
@@ -0,0 +1,65 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="stats.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+ bug: "1225722",
+ title: "WebRTC Stats composition and sanity for a one-way peer connection"
+});
+
+runNetworkTest(async function (options) {
+ // We don't know how to get QP value when using Android system codecs.
+ if (navigator.userAgent.includes("Android")) {
+ await pushPrefs(["media.navigator.mediadatadecoder_vpx_enabled", false],
+ ["media.webrtc.hw.h264.enabled", false]);
+ }
+
+ // For accurate comparisons of `remoteTimestamp` (not using reduced precision)
+ // to `timestamp` (using reduced precision).
+ await pushPrefs(["privacy.resistFingerprinting.reduceTimerPrecision.jitter",
+ false]);
+
+ const test = new PeerConnectionTest(options);
+
+ test.chain.insertAfter("PC_LOCAL_WAIT_FOR_MEDIA_FLOW",
+ [PC_LOCAL_TEST_LOCAL_STATS]);
+
+ test.chain.insertAfter("PC_REMOTE_WAIT_FOR_MEDIA_FLOW",
+ [PC_REMOTE_TEST_REMOTE_STATS]);
+
+ const testOneWayStats = (stats, codecType) => {
+ const codecs = [];
+ stats.forEach(stat => {
+ if (stat.type == "codec") {
+ codecs.push(stat);
+ is(stat.codecType, codecType, "One-way codec has specific codecType");
+ }
+ });
+ is(codecs.length, 2, "One audio and one video codec");
+ if (codecs.length == 2) {
+ isnot(codecs[0].mimeType.slice(0, 5), codecs[1].mimeType.slice(0, 5),
+ "Different media type for audio vs video mime types");
+ }
+ };
+
+ test.chain.append([
+ async function PC_LOCAL_TEST_CODECTYPE_ENCODE(test) {
+ testOneWayStats(await test.pcLocal._pc.getStats(), "encode");
+ },
+ async function PC_REMOTE_TEST_CODECTYPE_DECODE(test) {
+ testOneWayStats(await test.pcRemote._pc.getStats(), "decode");
+ },
+ ]);
+
+ test.setMediaConstraints([{audio: true}, {video: true}], []);
+ await test.run();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_stats_relayProtocol.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_stats_relayProtocol.html
new file mode 100644
index 0000000000..cdc328fd2b
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_stats_relayProtocol.html
@@ -0,0 +1,58 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="nonTrickleIce.js"></script>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+ bug: "1435789",
+ title: "WebRTC local-candidate relayProtocol stats attribute"
+});
+
+// This test uses the NAT simulator in order to get srflx candidates.
+// It doesn't work in https, so we turn on getUserMedia in http, which requires
+// a reload.
+if (!("mediaDevices" in navigator)) {
+ SpecialPowers.pushPrefEnv({set: [['media.devices.insecure.enabled', true]]},
+ () => location.reload());
+} else {
+ runNetworkTest(async (options = {}) => {
+ await pushPrefs(
+ ['media.peerconnection.nat_simulator.filtering_type', 'ENDPOINT_INDEPENDENT'],
+ ['media.peerconnection.nat_simulator.mapping_type', 'ENDPOINT_INDEPENDENT'],
+ ['media.getusermedia.insecure.enabled', true]);
+ const test = new PeerConnectionTest(options);
+ makeOffererNonTrickle(test.chain);
+ makeAnswererNonTrickle(test.chain);
+
+ test.chain.removeAfter("PC_LOCAL_WAIT_FOR_MEDIA_FLOW");
+ test.chain.append([PC_LOCAL_TEST_LOCAL_STATS_RELAYCANDIDATE]);
+
+ test.setMediaConstraints([{ audio: true }], [{ audio: true }]);
+ await test.run();
+ }, { useIceServer: true });
+}
+
+const PC_LOCAL_TEST_LOCAL_STATS_RELAYCANDIDATE = test => {
+ return test.pcLocal.getStats().then(stats => {
+ let haveRelayProtocol = {};
+ for (let [k, v] of stats) {
+ if (v.type == "local-candidate") {
+ haveRelayProtocol[v.candidateType + "-" + v.relayProtocol] = v.relayProtocol;
+ }
+ }
+ is(haveRelayProtocol["host-undefined"], undefined, "relayProtocol not set for host candidates");
+ is(haveRelayProtocol["srflx-undefined"], undefined, "relayProtocol not set for server reflexive candidates");
+ ok(haveRelayProtocol["relay-udp"], "Has UDP relay candidate");
+ ok(haveRelayProtocol["relay-tcp"], "Has TCP relay candidate");
+ ok(haveRelayProtocol["relay-tls"], "Has TLS relay candidate");
+ is(Object.keys(haveRelayProtocol).length, 5, "All candidate types are accounted for");
+ });
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_stereoFmtpPref.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_stereoFmtpPref.html
new file mode 100644
index 0000000000..ab7811fe82
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_stereoFmtpPref.html
@@ -0,0 +1,61 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1793776",
+ title: "Test that media.peerconnection.sdp.disable_stereo_fmtp works"
+ });
+
+ const tests = [
+ async function testStereo() {
+ const offerer = new RTCPeerConnection();
+ offerer.addTransceiver('audio');
+ const answerer = new RTCPeerConnection();
+ await offerer.setLocalDescription();
+ ok(offerer.localDescription.sdp.includes('stereo=1'),
+ 'Offer uses stereo=1 when media.peerconnection.sdp.disable_stereo_fmtp is not set');
+ await answerer.setRemoteDescription(offerer.localDescription);
+ const {sdp} = await answerer.createAnswer();
+ ok(sdp.includes('stereo=1'), 'Answer uses stereo=1 when media.peerconnection.sdp.disable_stereo_fmtp is not set');
+ },
+
+ async function testNoStereo() {
+ await pushPrefs(
+ ['media.peerconnection.sdp.disable_stereo_fmtp', true]);
+
+ const offerer = new RTCPeerConnection();
+ offerer.addTransceiver('audio');
+ const answerer = new RTCPeerConnection();
+ await offerer.setLocalDescription();
+ ok(offerer.localDescription.sdp.includes('stereo=0'),
+ 'Offer uses stereo=0 when media.peerconnection.sdp.disable_stereo_fmtp is set');
+ await answerer.setRemoteDescription(offerer.localDescription);
+ const {sdp} = await answerer.createAnswer();
+ ok(sdp.includes('stereo=0'), 'Answer uses stereo=0 when media.peerconnection.sdp.disable_stereo_fmtp is set');
+ },
+ ];
+
+ runNetworkTest(async () => {
+ for (const test of tests) {
+ info(`Running test: ${test.name}`);
+ try {
+ await test();
+ } catch (e) {
+ ok(false, `Caught ${e.name}: ${e.message} ${e.stack}`);
+ }
+ info(`Done running test: ${test.name}`);
+ // Make sure we don't build up a pile of GC work, and also get PCImpl to
+ // print their timecards.
+ await new Promise(r => SpecialPowers.exactGC(r));
+ }
+
+ await SpecialPowers.popPrefEnv();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_syncSetDescription.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_syncSetDescription.html
new file mode 100644
index 0000000000..98f0de1b4a
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_syncSetDescription.html
@@ -0,0 +1,53 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1063971",
+ title: "Legacy sync setDescription calls",
+ visible: true
+ });
+
+// Test setDescription without callbacks, which many webrtc examples still do
+
+function PC_LOCAL_SET_LOCAL_DESCRIPTION_SYNC(test) {
+ test.pcLocal.onsignalingstatechange = function() {};
+ test.pcLocal._pc.setLocalDescription(test.originalOffer);
+}
+
+function PC_REMOTE_SET_REMOTE_DESCRIPTION_SYNC(test) {
+ test.pcRemote.onsignalingstatechange = function() {};
+ test.pcRemote._pc.setRemoteDescription(test._local_offer,
+ test.pcRemote.releaseIceCandidates,
+ generateErrorCallback("pcRemote._pc.setRemoteDescription() sync failed"));
+}
+function PC_REMOTE_SET_LOCAL_DESCRIPTION_SYNC(test) {
+ test.pcRemote.onsignalingstatechange = function() {};
+ test.pcRemote._pc.setLocalDescription(test.originalAnswer);
+}
+function PC_LOCAL_SET_REMOTE_DESCRIPTION_SYNC(test) {
+ test.pcLocal.onsignalingstatechange = function() {};
+ test.pcLocal._pc.setRemoteDescription(test._remote_answer,
+ test.pcLocal.releaseIceCandidates,
+ generateErrorCallback("pcLocal._pc.setRemoteDescription() sync failed"));
+}
+
+runNetworkTest(() => {
+ const test = new PeerConnectionTest();
+ test.setMediaConstraints([{video: true}], [{video: true}]);
+ test.chain.replace("PC_LOCAL_SET_LOCAL_DESCRIPTION", PC_LOCAL_SET_LOCAL_DESCRIPTION_SYNC);
+ test.chain.replace("PC_REMOTE_SET_REMOTE_DESCRIPTION", PC_REMOTE_SET_REMOTE_DESCRIPTION_SYNC);
+ test.chain.remove("PC_REMOTE_CHECK_CAN_TRICKLE_SYNC");
+ test.chain.replace("PC_REMOTE_SET_LOCAL_DESCRIPTION", PC_REMOTE_SET_LOCAL_DESCRIPTION_SYNC);
+ test.chain.replace("PC_LOCAL_SET_REMOTE_DESCRIPTION", PC_LOCAL_SET_REMOTE_DESCRIPTION_SYNC);
+ test.chain.remove("PC_LOCAL_CHECK_CAN_TRICKLE_SYNC");
+ return test.run();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_telephoneEventFirst.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_telephoneEventFirst.html
new file mode 100644
index 0000000000..bde51c1fd0
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_telephoneEventFirst.html
@@ -0,0 +1,56 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+ title: "RTCPeerConnection with telephone-event codec first in SDP",
+ bug: "1581898",
+ visible: true
+});
+
+const test = async () => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ const stream = await navigator.mediaDevices.getUserMedia({audio:true});
+ pc1.addTrack(stream.getAudioTracks()[0], stream);
+ pc2.addTrack(stream.getAudioTracks()[0], stream);
+
+ const offer = await pc1.createOffer();
+ await pc1.setLocalDescription(offer);
+
+ const regex = /^(m=audio \d+ [^ ]+) (.*) 101(.*)$/m;
+
+ // Rewrite offer so payload type 101 comes first
+ offer.sdp = offer.sdp.replace(regex, '$1 101 $2 $3');
+
+ ok(offer.sdp.match(/^m=audio \d+ [^ ]+ 101 /m),
+ "Payload type 101 should be first on the m-line");
+
+ await pc2.setRemoteDescription(offer);
+ const answer = await pc2.createAnswer();
+
+ pc1.onicecandidate = e => { pc2.addIceCandidate(e.candidate); }
+ pc2.onicecandidate = e => { pc1.addIceCandidate(e.candidate); }
+
+ await pc1.setRemoteDescription(answer);
+ await pc2.setLocalDescription(answer);
+ await new Promise(resolve => {
+ pc1.oniceconnectionstatechange = e => {
+ if (pc1.iceConnectionState == "connected") {
+ resolve();
+ }
+ };
+ });
+ await wait(1000);
+};
+
+runNetworkTest(test);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_threeUnbundledConnections.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_threeUnbundledConnections.html
new file mode 100644
index 0000000000..75f0d12463
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_threeUnbundledConnections.html
@@ -0,0 +1,134 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1342579",
+ title: "Unbundled PC connects to two different PCs",
+ visible: true
+ });
+
+ const fakeFingerPrint = "a=fingerprint:sha-256 11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11";
+
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ const pc3 = new RTCPeerConnection();
+
+ const add = (pc, can, failed) => can && pc.addIceCandidate(can).catch(failed);
+ pc1.onicecandidate = e => {
+ if (e.candidate) {
+ if (e.candidate.sdpMid === "1") {
+ add(pc2, e.candidate, generateErrorCallback())
+ } else {
+ add(pc3, e.candidate, generateErrorCallback())
+ }
+ }
+ };
+ pc2.onicecandidate = e => add(pc1, e.candidate, generateErrorCallback());
+ pc3.onicecandidate = e => add(pc1, e.candidate, generateErrorCallback());
+
+ let ice1Finished, ice2Finished, ice3Finished;
+ const ice1Done = new Promise(r => ice1Finished = r);
+ const ice2Done = new Promise(r => ice2Finished = r);
+ const ice3Done = new Promise(r => ice3Finished = r);
+
+ const icsc = (pc, str, resolve) => {
+ const state = pc.iceConnectionState;
+ info(str + " ICE connection state is: " + state);
+ if (state == "connected") {
+ ok(true, str + " ICE connected");
+ resolve();
+ } else if (state == "failed") {
+ ok(false, str + " ICE failed")
+ resolve();
+ }
+ };
+
+ pc1.oniceconnectionstatechange = e => icsc(pc1, "PC1", ice1Finished);
+ pc2.oniceconnectionstatechange = e => icsc(pc2, "PC2", ice2Finished);
+ pc3.oniceconnectionstatechange = e => icsc(pc3, "PC3", ice3Finished);
+
+
+ function combineAnswer(origAnswer, answer) {
+ const sdplines = origAnswer.sdp.split('\r\n');
+ const fpIndex = sdplines.findIndex(l => l.match('^a=fingerprint'));
+ const FP = sdplines[fpIndex];
+ const audioIndex = sdplines.findIndex(l => l.match(/^m=audio [1-9]/));
+ const videoIndex = sdplines.findIndex(l => l.match(/^m=video [1-9]/));
+ if (audioIndex > -1) {
+ var ss = sdplines.slice(0, audioIndex);
+ ss.splice(fpIndex, 1);
+ answer.sessionSection = ss;
+ const rejectedVideoIndex = sdplines.findIndex(l => l.match('m=video 0'));
+ var ams = sdplines.slice(audioIndex, rejectedVideoIndex);
+ ams.push(FP);
+ ams.push(fakeFingerPrint);
+ answer.audioMsection = ams;
+ }
+ if (videoIndex > -1) {
+ var vms = sdplines.slice(videoIndex, sdplines.length -1);
+ vms.push(fakeFingerPrint);
+ vms.push(FP);
+ answer.videoMsection = vms;
+ }
+ return answer;
+ }
+
+runNetworkTest(async () => {
+ const v1 = createMediaElement('video', 'v1');
+ const v2 = createMediaElement('video', 'v2');
+ const v3 = createMediaElement('video', 'v3');
+
+ const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
+ (v1.srcObject = stream).getTracks().forEach(t => pc1.addTrack(t, stream));
+
+ const stream2 = await navigator.mediaDevices.getUserMedia({ video: true });
+ (v2.srcObject = stream2).getTracks().forEach(t => pc2.addTrack(t, stream2));
+
+ const stream3 = await navigator.mediaDevices.getUserMedia({ audio: true });
+ (v3.srcObject = stream3).getTracks().forEach(t => pc3.addTrack(t, stream3));
+
+ const offer = await pc1.createOffer();
+ await pc1.setLocalDescription(offer);
+
+ //info("Original OFFER: " + JSON.stringify(offer));
+ offer.sdp = sdputils.removeBundle(offer.sdp);
+ //info("OFFER w/o BUNDLE: " + JSON.stringify(offer));
+ const offerAudio = new RTCSessionDescription(JSON.parse(JSON.stringify(offer)));
+ offerAudio.sdp = offerAudio.sdp.replace('m=video 9', 'm=video 0');
+ //info("offerAudio: " + JSON.stringify(offerAudio));
+ const offerVideo = new RTCSessionDescription(JSON.parse(JSON.stringify(offer)));
+ offerVideo.sdp = offerVideo.sdp.replace('m=audio 9', 'm=audio 0');
+ //info("offerVideo: " + JSON.stringify(offerVideo));
+
+ // We need to do these in parallel, otherwise pc1 will start firing
+ // icecandidate events before pc3 is ready.
+ await Promise.all([pc2.setRemoteDescription(offerVideo), pc3.setRemoteDescription(offerAudio)]);
+
+ const answerVideo = await pc2.createAnswer();
+ const answerAudio = await pc3.createAnswer();
+
+ const answer = combineAnswer(answerAudio, combineAnswer(answerVideo, {}));
+ const fakeAnswer = answer.sessionSection.concat(answer.audioMsection, answer.videoMsection).join('\r\n');
+ info("ANSWER: " + fakeAnswer);
+
+ // We want to do these in parallel, because if we do them seqentially, by the
+ // time pc3.sLD completes pc2 could have fired icecandidate events, when we
+ // haven't called pc1.sRD yet.
+ await Promise.all(
+ [pc2.setLocalDescription(answerVideo),
+ pc3.setLocalDescription(answerAudio),
+ pc1.setRemoteDescription({type: 'answer', sdp: fakeAnswer})]);
+
+ await Promise.all([ice1Done, ice2Done, ice3Done]);
+
+ ok(true, "Connected.");
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_throwInCallbacks.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_throwInCallbacks.html
new file mode 100644
index 0000000000..5a3872c120
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_throwInCallbacks.html
@@ -0,0 +1,83 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "857765",
+ title: "Throw in PeerConnection callbacks"
+ });
+
+runNetworkTest(function () {
+ let finish;
+ const onfinished = new Promise(r => finish = async () => {
+ window.onerror = oldOnError;
+ is(error_count, 7, "Seven expected errors verified.");
+ r();
+ });
+
+ function getFail() {
+ return err => {
+ window.onerror = oldOnError;
+ generateErrorCallback()(err);
+ };
+ }
+
+ let error_count = 0;
+ let oldOnError = window.onerror;
+ window.onerror = (errorMsg, url, lineNumber) => {
+ if (!errorMsg.includes("Expected")) {
+ getFail()(errorMsg);
+ }
+ error_count += 1;
+ info("onerror " + error_count + ": " + errorMsg);
+ if (error_count == 7) {
+ finish();
+ }
+ throw new Error("window.onerror may throw");
+ return false;
+ }
+
+ let pc0, pc1, pc2;
+ // Test failure callbacks (limited to 1 for now)
+ pc0 = new RTCPeerConnection();
+ pc0.close();
+ pc0.createOffer(getFail(), function(err) {
+ pc1 = new RTCPeerConnection();
+ pc2 = new RTCPeerConnection();
+
+ // Test success callbacks (happy path)
+ navigator.mozGetUserMedia({video:true}, function(video1) {
+ pc1.addStream(video1);
+ pc1.createOffer(function(offer) {
+ pc1.setLocalDescription(offer, function() {
+ pc2.setRemoteDescription(offer, function() {
+ pc2.createAnswer(function(answer) {
+ pc2.setLocalDescription(answer, function() {
+ pc1.setRemoteDescription(answer, function() {
+ throw new Error("Expected");
+ }, getFail());
+ throw new Error("Expected");
+ }, getFail());
+ throw new Error("Expected");
+ }, getFail());
+ throw new Error("Expected");
+ }, getFail());
+ throw new Error("Expected");
+ }, getFail());
+ throw new Error("Expected");
+ }, getFail());
+ }, getFail());
+ throw new Error("Expected");
+ });
+
+ return onfinished;
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_toJSON.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_toJSON.html
new file mode 100644
index 0000000000..96c2c42b78
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_toJSON.html
@@ -0,0 +1,39 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "928304",
+ title: "test toJSON() on RTCSessionDescription and RTCIceCandidate"
+ });
+
+ runNetworkTest(function () {
+ /** Test for Bug 872377 **/
+
+ var rtcSession = new RTCSessionDescription({ sdp: "Picklechips!",
+ type: "offer" });
+ var jsonCopy = JSON.parse(JSON.stringify(rtcSession));
+ for (key in rtcSession) {
+ if (typeof(rtcSession[key]) == "function") continue;
+ is(rtcSession[key], jsonCopy[key], "key " + key + " should match.");
+ }
+
+ /** Test for Bug 928304 **/
+
+ var rtcIceCandidate = new RTCIceCandidate({ candidate: "dummy",
+ sdpMid: "test",
+ sdpMLineIndex: 3 });
+ jsonCopy = JSON.parse(JSON.stringify(rtcIceCandidate));
+ for (key in rtcIceCandidate) {
+ if (typeof(rtcIceCandidate[key]) == "function") continue;
+ is(rtcIceCandidate[key], jsonCopy[key], "key " + key + " should match.");
+ }
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_trackDisabling.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_trackDisabling.html
new file mode 100644
index 0000000000..73323cf007
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_trackDisabling.html
@@ -0,0 +1,108 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+ bug: "1219711",
+ title: "Disabling locally should be reflected remotely"
+});
+
+runNetworkTest(async () => {
+ const test = new PeerConnectionTest();
+
+ await pushPrefs(
+ ["media.getusermedia.camera.stop_on_disable.enabled", true],
+ ["media.getusermedia.camera.stop_on_disable.delay_ms", 0],
+ ["media.getusermedia.microphone.stop_on_disable.enabled", true],
+ ["media.getusermedia.microphone.stop_on_disable.delay_ms", 0],
+ // Always use fake tracks since we depend on video to be somewhat green and
+ // audio to have a large 1000Hz component (or 440Hz if using fake devices).
+ ['media.audio_loopback_dev', ''],
+ ['media.video_loopback_dev', ''],
+ ['media.navigator.streams.fake', true]);
+
+ test.setMediaConstraints([{audio: true, video: true}], []);
+ test.chain.append([
+ function CHECK_ASSUMPTIONS() {
+ is(test.pcLocal.localMediaElements.length, 2,
+ "pcLocal should have one media element");
+ is(test.pcRemote.remoteMediaElements.length, 2,
+ "pcRemote should have one media element");
+ is(test.pcLocal._pc.getLocalStreams().length, 1,
+ "pcLocal should have one stream");
+ is(test.pcRemote._pc.getRemoteStreams().length, 1,
+ "pcRemote should have one stream");
+ },
+ async function CHECK_VIDEO() {
+ const h = new CaptureStreamTestHelper2D();
+ const localVideo = test.pcLocal.localMediaElements
+ .find(e => e instanceof HTMLVideoElement);
+ const remoteVideo = test.pcRemote.remoteMediaElements
+ .find(e => e instanceof HTMLVideoElement);
+ // We check a pixel somewhere away from the top left corner since
+ // MediaEngineFake puts semi-transparent time indicators there.
+ const offsetX = 50;
+ const offsetY = 50;
+ const threshold = 128;
+
+ // We're regarding black as disabled here, and we're setting the alpha
+ // channel of the pixel to 255 to disregard alpha when testing.
+ const checkVideoEnabled = video => h.waitForPixel(video,
+ px => (px[3] = 255, h.isPixelNot(px, h.black, threshold)),
+ { offsetX, offsetY }
+ );
+ const checkVideoDisabled = video => h.waitForPixel(video,
+ px => (px[3] = 255, h.isPixel(px, h.black, threshold)),
+ { offsetX, offsetY }
+ );
+
+ info("Checking local video enabled");
+ await checkVideoEnabled(localVideo);
+ info("Checking remote video enabled");
+ await checkVideoEnabled(remoteVideo);
+
+ info("Disabling original");
+ test.pcLocal._pc.getLocalStreams()[0].getVideoTracks()[0].enabled = false;
+
+ info("Checking local video disabled");
+ await checkVideoDisabled(localVideo);
+ info("Checking remote video disabled");
+ await checkVideoDisabled(remoteVideo);
+ },
+ async function CHECK_AUDIO() {
+ const ac = new AudioContext();
+ const localAnalyser = new AudioStreamAnalyser(ac, test.pcLocal._pc.getLocalStreams()[0]);
+ const remoteAnalyser = new AudioStreamAnalyser(ac, test.pcRemote._pc.getRemoteStreams()[0]);
+
+ const checkAudio = (analyser, fun) => analyser.waitForAnalysisSuccess(fun);
+
+ const freq = localAnalyser.binIndexForFrequency(TEST_AUDIO_FREQ);
+ const checkAudioEnabled = analyser =>
+ checkAudio(analyser, array => array[freq] > 200);
+ const checkAudioDisabled = analyser =>
+ checkAudio(analyser, array => array[freq] < 50);
+
+ info("Checking local audio enabled");
+ await checkAudioEnabled(localAnalyser);
+ info("Checking remote audio enabled");
+ await checkAudioEnabled(remoteAnalyser);
+
+ test.pcLocal._pc.getLocalStreams()[0].getAudioTracks()[0].enabled = false;
+
+ info("Checking local audio disabled");
+ await checkAudioDisabled(localAnalyser);
+ info("Checking remote audio disabled");
+ await checkAudioDisabled(remoteAnalyser);
+ },
+ ]);
+ await test.run();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_trackDisabling_clones.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_trackDisabling_clones.html
new file mode 100644
index 0000000000..ae7647fa1a
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_trackDisabling_clones.html
@@ -0,0 +1,162 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+ bug: "1219711",
+ title: "Disabling locally should be reflected remotely, individually for clones"
+});
+
+runNetworkTest(async () => {
+ var test = new PeerConnectionTest();
+
+ await pushPrefs(
+ ["media.getusermedia.camera.stop_on_disable.enabled", true],
+ ["media.getusermedia.camera.stop_on_disable.delay_ms", 0],
+ ["media.getusermedia.microphone.stop_on_disable.enabled", true],
+ ["media.getusermedia.microphone.stop_on_disable.delay_ms", 0],
+ // Always use fake tracks since we depend on audio to have a large 1000Hz
+ // component.
+ ['media.audio_loopback_dev', ''],
+ ['media.navigator.streams.fake', true]);
+ // [TODO] re-enable HW decoder after bug 1526207 is fixed.
+ if (navigator.userAgent.includes("Android")) {
+ await pushPrefs(["media.navigator.mediadatadecoder_vpx_enabled", false],
+ ["media.webrtc.hw.h264.enabled", false]);
+ }
+
+ var originalStream;
+ var localVideoOriginal;
+
+ test.setMediaConstraints([{audio: true, video: true}], []);
+ test.chain.replace("PC_LOCAL_GUM", [
+ function PC_LOCAL_GUM_CLONE() {
+ return getUserMedia(test.pcLocal.constraints[0]).then(stream => {
+ originalStream = stream;
+ localVideoOriginal =
+ createMediaElement("video", "local-original");
+ localVideoOriginal.srcObject = stream;
+ test.pcLocal.attachLocalStream(originalStream.clone());
+ });
+ }
+ ]);
+ test.chain.append([
+ function CHECK_ASSUMPTIONS() {
+ is(test.pcLocal.localMediaElements.length, 2,
+ "pcLocal should have one media element");
+ is(test.pcRemote.remoteMediaElements.length, 2,
+ "pcRemote should have one media element");
+ is(test.pcLocal._pc.getLocalStreams().length, 1,
+ "pcLocal should have one stream");
+ is(test.pcRemote._pc.getRemoteStreams().length, 1,
+ "pcRemote should have one stream");
+ },
+ async function CHECK_VIDEO() {
+ info("Checking video");
+ var h = new CaptureStreamTestHelper2D();
+ var localVideoClone = test.pcLocal.localMediaElements
+ .find(e => e instanceof HTMLVideoElement);
+ var remoteVideoClone = test.pcRemote.remoteMediaElements
+ .find(e => e instanceof HTMLVideoElement);
+
+ // We check a pixel somewhere away from the top left corner since
+ // MediaEngineFake puts semi-transparent time indicators there.
+ const offsetX = 50;
+ const offsetY = 50;
+ const threshold = 128;
+ const remoteDisabledColor = h.black;
+
+ // We're regarding black as disabled here, and we're setting the alpha
+ // channel of the pixel to 255 to disregard alpha when testing.
+ var checkVideoEnabled = video => h.waitForPixel(video,
+ px => (px[3] = 255, h.isPixelNot(px, h.black, threshold)),
+ { offsetX, offsetY }
+ );
+ var checkVideoDisabled = video => h.waitForPixel(video,
+ px => (px[3] = 255, h.isPixel(px, h.black, threshold)),
+ { offsetX, offsetY }
+ );
+
+ info("Checking local original enabled");
+ await checkVideoEnabled(localVideoOriginal);
+ info("Checking local clone enabled");
+ await checkVideoEnabled(localVideoClone);
+ info("Checking remote clone enabled");
+ await checkVideoEnabled(remoteVideoClone);
+
+ info("Disabling original");
+ originalStream.getVideoTracks()[0].enabled = false;
+
+ info("Checking local original disabled");
+ await checkVideoDisabled(localVideoOriginal);
+ info("Checking local clone enabled");
+ await checkVideoEnabled(localVideoClone);
+ info("Checking remote clone enabled");
+ await checkVideoEnabled(remoteVideoClone);
+
+ info("Re-enabling original; disabling clone");
+ originalStream.getVideoTracks()[0].enabled = true;
+ test.pcLocal._pc.getLocalStreams()[0].getVideoTracks()[0].enabled = false;
+
+ info("Checking local original enabled");
+ await checkVideoEnabled(localVideoOriginal);
+ info("Checking local clone disabled");
+ await checkVideoDisabled(localVideoClone);
+ info("Checking remote clone disabled");
+ await checkVideoDisabled(remoteVideoClone);
+ },
+ async function CHECK_AUDIO() {
+ info("Checking audio");
+ var ac = new AudioContext();
+ var localAnalyserOriginal = new AudioStreamAnalyser(ac, originalStream);
+ var localAnalyserClone =
+ new AudioStreamAnalyser(ac, test.pcLocal._pc.getLocalStreams()[0]);
+ var remoteAnalyserClone =
+ new AudioStreamAnalyser(ac, test.pcRemote._pc.getRemoteStreams()[0]);
+
+ var freq = localAnalyserOriginal.binIndexForFrequency(TEST_AUDIO_FREQ);
+ var checkAudioEnabled = analyser =>
+ analyser.waitForAnalysisSuccess(array => array[freq] > 200);
+ var checkAudioDisabled = analyser =>
+ analyser.waitForAnalysisSuccess(array => array[freq] < 50);
+
+ info("Checking local original enabled");
+ await checkAudioEnabled(localAnalyserOriginal);
+ info("Checking local clone enabled");
+ await checkAudioEnabled(localAnalyserClone);
+ info("Checking remote clone enabled");
+ await checkAudioEnabled(remoteAnalyserClone);
+
+ info("Disabling original");
+ originalStream.getAudioTracks()[0].enabled = false;
+
+ info("Checking local original disabled");
+ await checkAudioDisabled(localAnalyserOriginal);
+ info("Checking local clone enabled");
+ await checkAudioEnabled(localAnalyserClone);
+ info("Checking remote clone enabled");
+ await checkAudioEnabled(remoteAnalyserClone);
+
+ info("Re-enabling original; disabling clone");
+ originalStream.getAudioTracks()[0].enabled = true;
+ test.pcLocal._pc.getLocalStreams()[0].getAudioTracks()[0].enabled = false;
+
+ info("Checking local original enabled");
+ await checkAudioEnabled(localAnalyserOriginal);
+ info("Checking local clone disabled");
+ await checkAudioDisabled(localAnalyserClone);
+ info("Checking remote clone disabled");
+ await checkAudioDisabled(remoteAnalyserClone);
+ },
+ ]);
+ await test.run();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_trackless_sender_stats.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_trackless_sender_stats.html
new file mode 100644
index 0000000000..f0356f5655
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_trackless_sender_stats.html
@@ -0,0 +1,56 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="stats.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1452673",
+ title: "Trackless RTCRtpSender.getStats()",
+ visible: true
+ });
+
+ // Calling getstats() on a trackless RTCRtpSender should yield an empty
+ // stats report. When track stats are added in the future, the stats
+ // for the removed tracks should continue to appear.
+
+ runNetworkTest(function (options) {
+ const test = new PeerConnectionTest(options);
+ test.chain.removeAfter("PC_REMOTE_WAIT_FOR_MEDIA_FLOW");
+ test.chain.append(
+ async function PC_LOCAL_AND_REMOTE_TRACKLESS_SENDER_STATS(test) {
+ await Promise.all([
+ waitForSyncedRtcp(test.pcLocal._pc),
+ waitForSyncedRtcp(test.pcRemote._pc),
+ ]);
+ let senders = test.pcLocal.getSenders();
+ let receivers = test.pcRemote.getReceivers();
+ is(senders.length, 2, "Have exactly two senders.");
+ is(receivers.length, 2, "Have exactly two receivers.");
+ for(let kind of ["audio", "video"]) {
+ is(senders.filter(s => s.track.kind == kind).length, 1,
+ "Exactly 1 sender of kind " + kind);
+ is(receivers.filter(r => r.track.kind == kind).length, 1,
+ "Exactly 1 receiver of kind " + kind);
+ }
+ // Remove tracks from senders
+ for (const sender of senders) {
+ await sender.replaceTrack(null);
+ is(sender.track, null, "Sender track removed");
+ let stats = await sender.getStats();
+ ok(stats instanceof window.RTCStatsReport, "Stats is instance of RTCStatsReport");
+ // Number of stats in the report. This should be 0.
+ is(stats.size, 0, "Trackless sender stats report is empty");
+ }
+ }
+ );
+ test.setMediaConstraints([{audio: true}, {video: true}], []);
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_twoAudioStreams.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_twoAudioStreams.html
new file mode 100644
index 0000000000..7ea18ab3dd
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_twoAudioStreams.html
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1091242",
+ title: "Multistream: Two audio streams"
+ });
+
+ runNetworkTest(function (options) {
+ const test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{audio: true}, {audio: true}],
+ [{audio: true}, {audio: true}]);
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_twoAudioTracksInOneStream.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_twoAudioTracksInOneStream.html
new file mode 100644
index 0000000000..99d4ad625a
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_twoAudioTracksInOneStream.html
@@ -0,0 +1,37 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1145407",
+ title: "Multistream: Two audio tracks in one stream"
+ });
+
+ runNetworkTest(function (options) {
+ const test = new PeerConnectionTest(options);
+ test.chain.insertAfter("PC_REMOTE_GET_OFFER", [
+ function PC_REMOTE_OVERRIDE_STREAM_IDS_IN_OFFER(test) {
+ test._local_offer.sdp = test._local_offer.sdp.replace(
+ /a=msid:[^\s]*/g,
+ "a=msid:foo");
+ }
+ ]);
+ test.chain.insertAfter("PC_LOCAL_GET_ANSWER", [
+ function PC_LOCAL_OVERRIDE_STREAM_IDS_IN_ANSWER(test) {
+ test._remote_answer.sdp = test._remote_answer.sdp.replace(
+ /a=msid:[^\s]*/g,
+ "a=msid:foo");
+ }
+ ]);
+ test.setMediaConstraints([{audio: true}, {audio: true}],
+ [{audio: true}, {audio: true}]);
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_twoAudioVideoStreams.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_twoAudioVideoStreams.html
new file mode 100644
index 0000000000..5f4bd463d4
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_twoAudioVideoStreams.html
@@ -0,0 +1,26 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+
+ createHTML({
+ bug: "1091242",
+ title: "Multistream: Two audio streams, two video streams"
+ });
+
+ runNetworkTest(function (options) {
+ const test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{audio: true}, {video: true}, {audio: true},
+ {video: true}],
+ [{audio: true}, {video: true}, {audio: true},
+ {video: true}]);
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_twoAudioVideoStreamsCombined.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_twoAudioVideoStreamsCombined.html
new file mode 100644
index 0000000000..fcc9c6c8fa
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_twoAudioVideoStreamsCombined.html
@@ -0,0 +1,70 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="stats.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+
+ createHTML({
+ bug: "1091242",
+ title: "Multistream: Two audio/video streams"
+ });
+
+ runNetworkTest(async (options) => {
+ // Disable platform encodre for SW MFT encoder causes some stats
+ // exceeding the test thresholds.
+ // E.g. inbound-rtp.packetsDiscarded value=118 >= 100.
+ await matchPlatformH264CodecPrefs();
+
+ const test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{audio: true, video: true},
+ {audio: true, video: true}],
+ [{audio: true, video: true},
+ {audio: true, video: true}]);
+
+ // Test stats, including coalescing of codec stats.
+ test.chain.insertAfter("PC_LOCAL_WAIT_FOR_MEDIA_FLOW",
+ [PC_LOCAL_TEST_LOCAL_STATS]);
+
+ test.chain.insertAfter("PC_REMOTE_WAIT_FOR_MEDIA_FLOW",
+ [PC_REMOTE_TEST_REMOTE_STATS]);
+
+ const testCoalescedCodecStats = stats => {
+ is([...stats.values()].filter(({type}) => type.endsWith("rtp")).length,
+ 16,
+ "Expected: 4 outbound, 4 remote-inbound, 4 inbound, 4 remote-inbound");
+ const codecs = [...stats.values()]
+ .filter(({type}) => type == "codec")
+ .sort((a, b) => a.mimeType > b.mimeType);
+ is(codecs.length, 2, "Should have registered two codecs (coalesced)");
+ is(new Set(codecs.map(({transportId}) => transportId)).size, 1,
+ "Should have registered only one transport with BUNDLE");
+ const codecTypes = new Set(codecs.map(({codecType}) => codecType));
+ is(codecTypes.size, 1,
+ "Should have identical encode and decode configurations (and stats)");
+ is(codecTypes[0], undefined,
+ "Should have identical encode and decode configurations (and stats)");
+ is(codecs[0].mimeType.slice(0, 5), "audio",
+ "Should have registered an audio codec");
+ is(codecs[1].mimeType.slice(0, 5), "video",
+ "Should have registered a video codec");
+ };
+
+ test.chain.append([
+ async function PC_LOCAL_TEST_COALESCED_CODEC_STATS() {
+ testCoalescedCodecStats(await test.pcLocal._pc.getStats());
+ },
+ async function PC_REMOTE_TEST_COALESCED_CODEC_STATS() {
+ testCoalescedCodecStats(await test.pcRemote._pc.getStats());
+ },
+ ]);
+
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_twoAudioVideoStreamsCombinedNoBundle.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_twoAudioVideoStreamsCombinedNoBundle.html
new file mode 100644
index 0000000000..8b825db617
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_twoAudioVideoStreamsCombinedNoBundle.html
@@ -0,0 +1,107 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="stats.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+ bug: "1225722",
+ title: "Multistream: Two audio/video streams without BUNDLE"
+});
+
+runNetworkTest(async (options = {}) => {
+ // Disable platform encodre for SW MFT encoder causes some stats
+ // exceeding the test thresholds.
+ // E.g. inbound-rtp.packetsDiscarded value=118 >= 100.
+ await matchPlatformH264CodecPrefs();
+
+ options.bundle = false;
+ const test = new PeerConnectionTest(options);
+ test.setMediaConstraints(
+ [{audio: true, video: true}, {audio: true, video: true}],
+ [{audio: true, video: true}, {audio: true, video: true}]
+ );
+
+ // Test stats, including that codec stats do not coalesce without BUNDLE.
+ const testNonBundledStats = async pc => {
+ // This is basically PC_*_TEST_*_STATS fleshed out, but uses
+ // sender/receiver.getStats instead of pc.getStats, since the codec stats
+ // code assumes at most one sender and at most one receiver.
+ await waitForSyncedRtcp(pc);
+ const senderPromises = pc.getSenders().map(obj => obj.getStats());
+ const receiverPromises = pc.getReceivers().map(obj => obj.getStats());
+ const senderStats = await Promise.all(senderPromises);
+ const receiverStats = await Promise.all(receiverPromises);
+ for (const stats of [...senderStats, ...receiverStats]) {
+ checkExpectedFields(stats);
+ pedanticChecks(stats);
+ }
+ for (const stats of senderStats) {
+ checkSenderStats(stats, 1);
+ }
+ };
+
+ test.chain.insertAfter("PC_LOCAL_WAIT_FOR_MEDIA_FLOW", [
+ async function PC_LOCAL_TEST_LOCAL_NONBUNDLED_STATS(test) {
+ await testNonBundledStats(test.pcLocal._pc);
+ },
+ ]);
+
+ test.chain.insertAfter("PC_REMOTE_WAIT_FOR_MEDIA_FLOW", [
+ async function PC_REMOTE_TEST_LOCAL_NONBUNDLED_STATS(test) {
+ await testNonBundledStats(test.pcRemote._pc);
+ },
+ ]);
+
+ const testNonCoalescedCodecStats = stats => {
+ const codecs = [...stats.values()]
+ .filter(({type}) => type == "codec");
+ is([...stats.values()].filter(({type}) => type.endsWith("rtp")).length, 16,
+ "Expected: 4 outbound, 4 remote-inbound, 4 inbound, 4 remote-inbound");
+ const codecTypes = new Set(codecs.map(({codecType}) => codecType));
+ is(codecTypes.size, 1,
+ "Should have identical encode and decode configurations (and stats)");
+ is(codecTypes[0], undefined,
+ "Should have identical encode and decode configurations (and stats)");
+ const transportIds = new Set(codecs.map(({transportId}) => transportId));
+ is(transportIds.size, 4,
+ "Should have registered four transports for two sendrecv streams");
+ for (const transportId of transportIds) {
+ is(codecs.filter(c => c.transportId == transportId).length, 1,
+ "Should have registered one codec per transport without BUNDLE");
+ }
+ for (const prefix of ["audio", "video"]) {
+ const prefixed = codecs.filter(c => c.mimeType.startsWith(prefix));
+ is(prefixed.length, 2, `Should have registered two ${prefix} codecs`);
+ if (prefixed.length == 2) {
+ is(prefixed[0].payloadType, prefixed[1].payloadType,
+ "same payloadType");
+ isnot(prefixed[0].transportId, prefixed[1].transportId,
+ "different transportIds");
+ is(prefixed[0].mimeType, prefixed[1].mimeType, "same mimeType");
+ is(prefixed[0].clockRate, prefixed[1].clockRate, "same clockRate");
+ is(prefixed[0].channels, prefixed[1].channels, "same channels");
+ is(prefixed[0].sdpFmtpLine, prefixed[1].sdpFmtpLine,
+ "same sdpFmtpLine");
+ }
+ }
+ };
+
+ test.chain.append([
+ async function PC_LOCAL_TEST_NON_COALESCED_CODEC_STATS() {
+ testNonCoalescedCodecStats(await test.pcLocal._pc.getStats());
+ },
+ async function PC_REMOTE_TEST_NON_COALESCED_CODEC_STATS() {
+ testNonCoalescedCodecStats(await test.pcRemote._pc.getStats());
+ },
+ ]);
+
+ return test.run();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_twoVideoStreams.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_twoVideoStreams.html
new file mode 100644
index 0000000000..0ab180cc55
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_twoVideoStreams.html
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1091242",
+ title: "Multistream: Two video streams"
+ });
+
+ runNetworkTest(function (options) {
+ const test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{video: true}, {video: true}],
+ [{video: true}, {video: true}]);
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_twoVideoTracksInOneStream.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_twoVideoTracksInOneStream.html
new file mode 100644
index 0000000000..4eaf8b3f48
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_twoVideoTracksInOneStream.html
@@ -0,0 +1,37 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1145407",
+ title: "Multistream: Two video tracks in offerer stream"
+ });
+
+ runNetworkTest(function (options) {
+ const test = new PeerConnectionTest(options);
+ test.chain.insertAfter("PC_REMOTE_GET_OFFER", [
+ function PC_REMOTE_OVERRIDE_STREAM_IDS_IN_OFFER(test) {
+ test._local_offer.sdp = test._local_offer.sdp.replace(
+ /a=msid:[^\s]*/g,
+ "a=msid:foo");
+ }
+ ]);
+ test.chain.insertAfter("PC_LOCAL_GET_ANSWER", [
+ function PC_LOCAL_OVERRIDE_STREAM_IDS_IN_ANSWER(test) {
+ test._remote_answer.sdp = test._remote_answer.sdp.replace(
+ /a=msid:[^\s]*/g,
+ "a=msid:foo");
+ }
+ ]);
+ test.setMediaConstraints([{video: true}, {video: true}],
+ [{video: true}, {video: true}]);
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_verifyAudioAfterRenegotiation.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_verifyAudioAfterRenegotiation.html
new file mode 100644
index 0000000000..86ef6d4678
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_verifyAudioAfterRenegotiation.html
@@ -0,0 +1,99 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1166832",
+ title: "Renegotiation: verify audio after renegotiation"
+ });
+
+ runNetworkTest(function (options) {
+ const test = new PeerConnectionTest(options);
+ const helper = new AudioStreamHelper();
+
+ test.chain.append([
+ function CHECK_ASSUMPTIONS() {
+ is(test.pcLocal.localMediaElements.length, 1,
+ "pcLocal should have one media element");
+ is(test.pcRemote.remoteMediaElements.length, 1,
+ "pcRemote should have one media element");
+ is(test.pcLocal._pc.getLocalStreams().length, 1,
+ "pcLocal should have one stream");
+ is(test.pcRemote._pc.getRemoteStreams().length, 1,
+ "pcRemote should have one stream");
+ },
+ function CHECK_AUDIO() {
+ return Promise.resolve()
+ .then(() => info("Checking local audio enabled"))
+ .then(() => helper.checkAudioFlowing(test.pcLocal._pc.getLocalStreams()[0]))
+ .then(() => info("Checking remote audio enabled"))
+ .then(() => helper.checkAudioFlowing(test.pcRemote._pc.getRemoteStreams()[0]))
+
+ .then(() => test.pcLocal._pc.getLocalStreams()[0].getAudioTracks()[0].enabled = false)
+
+ .then(() => info("Checking local audio disabled"))
+ .then(() => helper.checkAudioNotFlowing(test.pcLocal._pc.getLocalStreams()[0]))
+ .then(() => info("Checking remote audio disabled"))
+ .then(() => helper.checkAudioNotFlowing(test.pcRemote._pc.getRemoteStreams()[0]))
+ }
+ ]);
+
+ addRenegotiation(test.chain,
+ [
+ function PC_LOCAL_ADD_SECOND_STREAM(test) {
+ test.setMediaConstraints([{audio: true}],
+ []);
+ return test.pcLocal.getAllUserMediaAndAddStreams([{audio: true}]);
+ },
+ ]
+ );
+
+ test.chain.append([
+ function CHECK_ASSUMPTIONS2() {
+ is(test.pcLocal.localMediaElements.length, 2,
+ "pcLocal should have two media elements");
+ is(test.pcRemote.remoteMediaElements.length, 2,
+ "pcRemote should have two media elements");
+ is(test.pcLocal._pc.getLocalStreams().length, 2,
+ "pcLocal should have two streams");
+ is(test.pcRemote._pc.getRemoteStreams().length, 2,
+ "pcRemote should have two streams");
+ },
+ function RE_CHECK_AUDIO() {
+ return Promise.resolve()
+ .then(() => info("Checking local audio enabled"))
+ .then(() => helper.checkAudioNotFlowing(test.pcLocal._pc.getLocalStreams()[0]))
+ .then(() => info("Checking remote audio enabled"))
+ .then(() => helper.checkAudioNotFlowing(test.pcRemote._pc.getRemoteStreams()[0]))
+
+ .then(() => info("Checking local2 audio enabled"))
+ .then(() => helper.checkAudioFlowing(test.pcLocal._pc.getLocalStreams()[1]))
+ .then(() => info("Checking remote2 audio enabled"))
+ .then(() => helper.checkAudioFlowing(test.pcRemote._pc.getRemoteStreams()[1]))
+
+ .then(() => test.pcLocal._pc.getLocalStreams()[1].getAudioTracks()[0].enabled = false)
+ .then(() => test.pcLocal._pc.getLocalStreams()[0].getAudioTracks()[0].enabled = true)
+
+ .then(() => info("Checking local2 audio disabled"))
+ .then(() => helper.checkAudioNotFlowing(test.pcLocal._pc.getLocalStreams()[1]))
+ .then(() => info("Checking remote2 audio disabled"))
+ .then(() => helper.checkAudioNotFlowing(test.pcRemote._pc.getRemoteStreams()[1]))
+
+ .then(() => info("Checking local audio enabled"))
+ .then(() => helper.checkAudioFlowing(test.pcLocal._pc.getLocalStreams()[0]))
+ .then(() => info("Checking remote audio enabled"))
+ .then(() => helper.checkAudioFlowing(test.pcRemote._pc.getRemoteStreams()[0]))
+ }
+ ]);
+
+ test.setMediaConstraints([{audio: true}], []);
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_verifyDescriptions.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_verifyDescriptions.html
new file mode 100644
index 0000000000..f685f7c99a
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_verifyDescriptions.html
@@ -0,0 +1,58 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1264479",
+ title: "PeerConnection verify current and pending descriptions"
+ });
+
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+
+ var add = (pc, can, failed) => can && pc.addIceCandidate(can).catch(failed);
+ pc1.onicecandidate = e => add(pc2, e.candidate, generateErrorCallback());
+ pc2.onicecandidate = e => add(pc1, e.candidate, generateErrorCallback());
+
+
+ runNetworkTest(function() {
+ const v1 = createMediaElement('video', 'v1');
+ const v2 = createMediaElement('video', 'v2');
+
+ return navigator.mediaDevices.getUserMedia({ video: true, audio: true })
+ .then(stream => (v1.srcObject = stream).getTracks().forEach(t => pc1.addTrack(t, stream)))
+ .then(() => pc1.createOffer({})) // check that createOffer accepts arg.
+ .then(offer => pc1.setLocalDescription(offer))
+ .then(() => {
+ ok(!pc1.currentLocalDescription, "pc1 currentLocalDescription is empty");
+ ok(pc1.pendingLocalDescription, "pc1 pendingLocalDescription is set");
+ ok(pc1.localDescription, "pc1 localDescription is set");
+ })
+ .then(() => pc2.setRemoteDescription(pc1.localDescription))
+ .then(() => {
+ ok(!pc2.currentRemoteDescription, "pc2 currentRemoteDescription is empty");
+ ok(pc2.pendingRemoteDescription, "pc2 pendingRemoteDescription is set");
+ ok(pc2.remoteDescription, "pc2 remoteDescription is set");
+ })
+ .then(() => pc2.createAnswer({})) // check that createAnswer accepts arg.
+ .then(answer => pc2.setLocalDescription(answer))
+ .then(() => {
+ ok(pc2.currentLocalDescription, "pc2 currentLocalDescription is set");
+ ok(!pc2.pendingLocalDescription, "pc2 pendingLocalDescription is empty");
+ ok(pc2.localDescription, "pc2 localDescription is set");
+ })
+ .then(() => pc1.setRemoteDescription(pc2.localDescription))
+ .then(() => {
+ ok(pc1.currentRemoteDescription, "pc1 currentRemoteDescription is set");
+ ok(!pc1.pendingRemoteDescription, "pc1 pendingRemoteDescription is empty");
+ ok(pc1.remoteDescription, "pc1 remoteDescription is set");
+ });
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_verifyVideoAfterRenegotiation.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_verifyVideoAfterRenegotiation.html
new file mode 100644
index 0000000000..8d4155ddff
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_verifyVideoAfterRenegotiation.html
@@ -0,0 +1,123 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1166832",
+ title: "Renegotiation: verify video after renegotiation"
+ });
+
+runNetworkTest(async () => {
+ // [TODO] re-enable HW decoder after bug 1526207 is fixed.
+ if (navigator.userAgent.includes("Android")) {
+ await pushPrefs(["media.navigator.mediadatadecoder_vpx_enabled", false],
+ ["media.webrtc.hw.h264.enabled", false]);
+ }
+
+ const test = new PeerConnectionTest();
+
+ const h1 = new CaptureStreamTestHelper2D(50, 50);
+ const canvas1 = h1.createAndAppendElement('canvas', 'source_canvas1');
+ let stream1;
+ let vremote1;
+
+ const h2 = new CaptureStreamTestHelper2D(50, 50);
+ let canvas2;
+ let stream2;
+ let vremote2;
+
+ test.setMediaConstraints([{video: true}], []);
+ test.chain.replace("PC_LOCAL_GUM", [
+ function DRAW_INITIAL_LOCAL_GREEN(test) {
+ h1.drawColor(canvas1, h1.green);
+ },
+ function PC_LOCAL_CANVAS_CAPTURESTREAM(test) {
+ stream1 = canvas1.captureStream(0);
+ test.pcLocal.attachLocalStream(stream1);
+ let i = 0;
+ return setInterval(function() {
+ try {
+ info("draw " + i ? "green" : "red");
+ h1.drawColor(canvas1, i ? h1.green : h1.red);
+ i = 1 - i;
+ stream1.requestFrame();
+ if (stream2 != null) {
+ h2.drawColor(canvas2, i ? h2.green : h2.blue);
+ stream2.requestFrame();
+ }
+ } catch (e) {
+ // ignore; stream might have shut down, and we don't bother clearing
+ // the setInterval.
+ }
+ }, 500);
+ }
+ ]);
+
+ test.chain.append([
+ function FIND_REMOTE_VIDEO() {
+ vremote1 = test.pcRemote.remoteMediaElements[0];
+ ok(!!vremote1, "Should have remote video element for pcRemote");
+ },
+ function WAIT_FOR_REMOTE_GREEN() {
+ return h1.pixelMustBecome(vremote1, h1.green, {
+ threshold: 128,
+ infoString: "pcRemote's remote should become green",
+ });
+ },
+ function WAIT_FOR_REMOTE_RED() {
+ return h1.pixelMustBecome(vremote1, h1.red, {
+ threshold: 128,
+ infoString: "pcRemote's remote should become red",
+ });
+ }
+ ]);
+
+ addRenegotiation(test.chain,
+ [
+ function PC_LOCAL_ADD_SECOND_STREAM(test) {
+ canvas2 = h2.createAndAppendElement('canvas', 'source_canvas2');
+ h2.drawColor(canvas2, h2.blue);
+ stream2 = canvas2.captureStream(0);
+
+ // can't use test.pcLocal.getAllUserMediaAndAddStreams([{video: true}]);
+ // because it doesn't let us substitute the capture stream
+ test.pcLocal.attachLocalStream(stream2);
+ }
+ ]
+ );
+
+ test.chain.append([
+ function FIND_REMOTE2_VIDEO() {
+ vremote2 = test.pcRemote.remoteMediaElements[1];
+ ok(!!vremote2, "Should have remote2 video element for pcRemote");
+ },
+ function WAIT_FOR_REMOTE2_BLUE() {
+ return h2.pixelMustBecome(vremote2, h2.blue, {
+ threshold: 128,
+ infoString: "pcRemote's remote2 should become blue",
+ });
+ },
+ function DRAW_NEW_LOCAL_GREEN(test) {
+ stream1.requestFrame();
+ h1.drawColor(canvas1, h1.green);
+ },
+ function WAIT_FOR_REMOTE1_GREEN() {
+ return h1.pixelMustBecome(vremote1, h1.green, {
+ threshold: 128,
+ infoString: "pcRemote's remote1 should become green",
+ });
+ }
+ ]);
+
+ await test.run();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_videoCodecs.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_videoCodecs.html
new file mode 100644
index 0000000000..7a245b5d8c
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_videoCodecs.html
@@ -0,0 +1,142 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="stats.js"></script>
+ <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1395853",
+ title: "Verify video content over WebRTC for every video codec",
+ });
+
+ async function testVideoCodec(options = {}, codec) {
+ const test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{video: true}], []);
+
+ let payloadType;
+ test.chain.insertBefore("PC_LOCAL_SET_LOCAL_DESCRIPTION", [
+ function PC_LOCAL_FILTER_OUT_CODECS() {
+ const otherCodec = codecs.find(c => c != codec);
+ const otherId = sdputils.findCodecId(test.originalOffer.sdp, otherCodec.name, otherCodec.offset);
+ const otherRtpmapMatcher = new RegExp(`a=rtpmap:${otherId}.*\\r\\n`, "gi");
+
+ const id = sdputils.findCodecId(test.originalOffer.sdp, codec.name, codec.offset);
+ payloadType = Number(id);
+ if (codec.offset) {
+ isnot(id, sdputils.findCodecId(test.originalOffer.sdp, codec.name, 0),
+ "Different offsets should return different payload types");
+ }
+ test.originalOffer.sdp =
+ sdputils.removeAllButPayloadType(test.originalOffer.sdp, id);
+
+ ok(!test.originalOffer.sdp.match(new RegExp(`m=.*UDP/TLS/RTP/SAVPF.* ${otherId}[^0-9]`, "gi")),
+ `Other codec ${otherId} should be removed after filtering`);
+ ok(test.originalOffer.sdp.match(new RegExp(`m=.*UDP/TLS/RTP/SAVPF.* ${id}[^0-9]`, "gi")),
+ `Tested codec ${id} should remain after filtering`);
+
+ // We only set it now, or the framework would remove non-H264 codecs
+ // for us.
+ options.h264 = codec.name == "H264";
+ },
+ ]);
+
+ test.chain.insertAfter("PC_LOCAL_WAIT_FOR_MEDIA_FLOW",
+ [PC_LOCAL_TEST_LOCAL_STATS]);
+
+ test.chain.insertAfter("PC_REMOTE_WAIT_FOR_MEDIA_FLOW",
+ [PC_REMOTE_TEST_REMOTE_STATS]);
+
+ test.chain.append([
+ async function PC_LOCAL_TEST_CODEC() {
+ const stats = await test.pcLocal._pc.getStats();
+ let codecCount = 0;
+ stats.forEach(stat => {
+ if (stat.type == "codec") {
+ is(codecCount++, 0, "expected only one encode codec stat");
+ is(stat.payloadType, payloadType, "payloadType as expected");
+ is(stat.mimeType, `video/${codec.name}`, "mimeType as expected");
+ is(stat.codecType, "encode", "codecType as expected");
+ }
+ });
+ },
+ async function PC_REMOTE_TEST_CODEC() {
+ const stats = await test.pcRemote._pc.getStats();
+ let codecCount = 0;
+ stats.forEach(stat => {
+ if (stat.type == "codec") {
+ is(codecCount++, 0, "expected only one decode codec stat");
+ is(stat.payloadType, payloadType, "payloadType as expected");
+ is(stat.mimeType, `video/${codec.name}`, "mimeType as expected");
+ is(stat.codecType, "decode", "codecType as expected");
+ }
+ });
+ },
+ async function CHECK_VIDEO_FLOW() {
+ try {
+ const h = new VideoStreamHelper();
+ await h.checkVideoPlaying(
+ test.pcRemote.remoteMediaElements[0],
+ 10, 10, 128);
+ ok(true, `Got video flow for codec ${codec.name}, offset ${codec.offset}`);
+ } catch(e) {
+ ok(false, `No video flow for codec ${codec.name}, offset ${codec.offset}: ${e}`);
+ }
+ },
+ ]);
+
+ await test.run();
+ }
+
+ // We match the name against the sdp to figure out the payload type,
+ // so all other present codecs can be removed.
+ // Use `offset` when there are multiple instances of a codec expected in an sdp.
+ const codecs = [
+ { name: "VP8" },
+ { name: "VP9" },
+ { name: "H264" },
+ { name: "H264", offset: 1 },
+ ];
+
+ runNetworkTest(async (options) => {
+ // This test expects the video being captured will change color. Use fake
+ // video device as loopback does not currently change.
+ await pushPrefs(
+ ['media.video_loopback_dev', ''],
+ ['media.navigator.streams.fake', true]);
+ for (let codec of codecs) {
+ info(`Testing video for codec ${codec.name} offset ${codec.offset}`);
+ try {
+ let enc = SpecialPowers.getBoolPref('media.webrtc.platformencoder');
+ let dec = SpecialPowers.getBoolPref('media.navigator.mediadatadecoder_h264_enabled');
+ if (codec.name == "H264") {
+ await matchPlatformH264CodecPrefs();
+ if (codec.offset == 1) {
+ // Force fake GMP codec for H.264 mode 0 because not all platforms
+ // support slice size control. Re-enable it after
+ // a. SW encoder fallback support (bug 1726617), and
+ // b. returning valid bitstream from fake GMP encoder (bug 1509012).
+ await pushPrefs(
+ ['media.webrtc.platformencoder', false],
+ ['media.navigator.mediadatadecoder_h264_enabled', false],
+ );
+ }
+ }
+ await testVideoCodec(options, codec);
+ await pushPrefs(
+ ['media.webrtc.platformencoder', enc],
+ ['media.navigator.mediadatadecoder_h264_enabled', dec],
+ );
+ } catch(e) {
+ ok(false, `Error in test for codec ${codec.name}: ${e}\n${e.stack}`);
+ }
+ info(`Tested video for codec ${codec.name}`);
+ }
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_videoRenegotiationInactiveAnswer.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_videoRenegotiationInactiveAnswer.html
new file mode 100644
index 0000000000..b77633493d
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_videoRenegotiationInactiveAnswer.html
@@ -0,0 +1,95 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="sdpUtils.js"></script>
+ <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1213773",
+ title: "Renegotiation: answerer uses a=inactive for video"
+ });
+
+ runNetworkTest(async (options) => {
+ // [TODO] re-enable HW decoder after bug 1526207 is fixed.
+ if (navigator.userAgent.includes("Android")) {
+ await pushPrefs(["media.navigator.mediadatadecoder_vpx_enabled", false],
+ ["media.webrtc.hw.h264.enabled", false]);
+ }
+
+ const emitter = new VideoFrameEmitter();
+ const helper = new VideoStreamHelper();
+
+ const test = new PeerConnectionTest(options);
+
+ test.chain.replace("PC_LOCAL_GUM", [
+ function PC_LOCAL_CANVAS_CAPTURESTREAM(test) {
+ test.pcLocal.attachLocalStream(emitter.stream());
+ emitter.start();
+ }
+ ]);
+
+ var haveFirstUnmuteEvent;
+
+ test.chain.insertBefore("PC_REMOTE_SET_LOCAL_DESCRIPTION", [
+ function PC_REMOTE_SETUP_ONUNMUTE_1() {
+ haveFirstUnmuteEvent = haveEvent(test.pcRemote._pc.getReceivers()[0].track, "unmute");
+ }
+ ]);
+
+ test.chain.append([
+ function PC_REMOTE_CHECK_VIDEO_UNMUTED() {
+ return haveFirstUnmuteEvent;
+ },
+ function PC_REMOTE_WAIT_FOR_FRAMES() {
+ var vremote = test.pcRemote.remoteMediaElements[0];
+ ok(vremote, "Should have remote video element for pcRemote");
+ return addFinallyToPromise(helper.checkVideoPlaying(vremote))
+ .finally(() => emitter.stop());
+ }
+ ]);
+
+ addRenegotiation(test.chain, []);
+
+ test.chain.insertAfter("PC_LOCAL_GET_ANSWER", [
+ function PC_LOCAL_REWRITE_REMOTE_SDP_INACTIVE(test) {
+ test._remote_answer.sdp =
+ sdputils.setAllMsectionsInactive(test._remote_answer.sdp);
+ }
+ ], false, 1);
+
+ test.chain.append([
+ function PC_REMOTE_ENSURE_NO_FRAMES() {
+ var vremote = test.pcRemote.remoteMediaElements[0];
+ ok(vremote, "Should have remote video element for pcRemote");
+ emitter.start();
+ return addFinallyToPromise(helper.checkVideoPaused(vremote))
+ .finally(() => emitter.stop());
+ },
+ ]);
+
+ test.chain.remove("PC_REMOTE_CHECK_STATS", 1);
+ test.chain.remove("PC_LOCAL_CHECK_STATS", 1);
+
+ addRenegotiation(test.chain, []);
+
+ test.chain.append([
+ function PC_REMOTE_WAIT_FOR_FRAMES_2() {
+ var vremote = test.pcRemote.remoteMediaElements[0];
+ ok(vremote, "Should have remote video element for pcRemote");
+ emitter.start();
+ return addFinallyToPromise(helper.checkVideoPlaying(vremote))
+ .finally(() => emitter.stop());
+ }
+ ]);
+
+ test.setMediaConstraints([{video: true}], []);
+ await test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_webAudio.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_webAudio.html
new file mode 100644
index 0000000000..1d695ecbfa
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_webAudio.html
@@ -0,0 +1,43 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+ bug: "1081819",
+ title: "WebAudio on both input and output side of peerconnection"
+});
+
+// This tests WebAudio (a 700Hz OscillatorNode) as input to a PeerConnection.
+// It also tests that a PeerConnection works as input to WebAudio as the remote
+// stream is connected to an AnalyserNode and compared to the source node.
+
+runNetworkTest(function() {
+ const test = new PeerConnectionTest();
+ test.audioContext = new AudioContext();
+ test.setMediaConstraints([{audio: true}], []);
+ test.chain.replace("PC_LOCAL_GUM", [
+ function PC_LOCAL_WEBAUDIO_SOURCE(test) {
+ const oscillator = test.audioContext.createOscillator();
+ oscillator.type = 'sine';
+ oscillator.frequency.value = 700;
+ oscillator.start();
+ const dest = test.audioContext.createMediaStreamDestination();
+ oscillator.connect(dest);
+ test.pcLocal.attachLocalStream(dest.stream);
+ }
+ ]);
+ test.chain.append([
+ function CHECK_AUDIO_FLOW(test) {
+ return test.pcRemote.checkReceivingToneFrom(test.audioContext, test.pcLocal);
+ }
+ ]);
+ return test.run();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_selftest.html b/dom/media/webrtc/tests/mochitests/test_selftest.html
new file mode 100644
index 0000000000..3f1ce1402d
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_selftest.html
@@ -0,0 +1,37 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ title: "Self-test of harness functions",
+ visible: true
+ });
+
+function TEST(test) {}
+
+var catcher = func => {
+ try {
+ func();
+ return null;
+ } catch (e) {
+ return e.message;
+ }
+};
+
+runNetworkTest(() => {
+ var test = new PeerConnectionTest();
+ test.setMediaConstraints([{video: true}], [{video: true}]);
+ is(catcher(() => test.chain.replace("PC_LOCAL_SET_LOCAL_DESCRIPTION", TEST)),
+ null, "test.chain.replace works");
+ is(catcher(() => test.chain.replace("FOO", TEST)),
+ "Unknown test: FOO", "test.chain.replace catches typos");
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_setSinkId.html b/dom/media/webrtc/tests/mochitests/test_setSinkId.html
new file mode 100644
index 0000000000..0d85114a0e
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_setSinkId.html
@@ -0,0 +1,83 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+
+<script>
+ createHTML({
+ title: "SetSinkId in HTMLMediaElement",
+ bug: "934425",
+ });
+
+ const memoryReportPath = 'explicit/media/media-manager-aggregates';
+
+ /**
+ * Run a test to verify set sink id in audio element.
+ */
+ runTest(async () => {
+ await pushPrefs(["media.setsinkid.enabled", true]);
+
+ if (!SpecialPowers.getCharPref("media.audio_loopback_dev", "")) {
+ ok(false, "No loopback device set by framework. Try --use-test-media-devices");
+ return;
+ }
+
+ // Expose an audio output device.
+ SpecialPowers.wrap(document).notifyUserGestureActivation();
+ await navigator.mediaDevices.selectAudioOutput();
+
+ const allDevices = await navigator.mediaDevices.enumerateDevices();
+ const audioDevices = allDevices.filter(({kind}) => kind == 'audiooutput');
+ is(audioDevices.length, 1, "Number of output devices found");
+
+ const audio = createMediaElement("audio", "audio");
+ document.body.appendChild(audio);
+
+ is(audio.sinkId, "", "Initial value is empty string");
+
+ const p = audio.setSinkId(audioDevices[0].deviceId);
+ is(audio.sinkId, "", "Value is unchanged upon function return");
+ is(await p, undefined, "promise resolves with undefined");
+ is(audio.sinkId, audioDevices[0].deviceId, `Sink device is set, id: ${audio.sinkId}`);
+
+ await audio.setSinkId(audioDevices[0].deviceId);
+ ok(true, `Sink device is set for 2nd time for the same id: ${audio.sinkId}`);
+
+ try {
+ await audio.setSinkId("dummy sink id");
+ ok(false, "Never enter here, this must fail");
+ } catch (error) {
+ ok(true, `Set sink id expected to fail: ${error}`);
+ is(error.name, "NotFoundError", "Verify correct error");
+ }
+
+ const {usage: usage1} =
+ await collectMemoryUsage(memoryReportPath); // Provided by head.js
+
+ ok(usage1 > 0, "MediaManager memory usage should be non-zero to store \
+device ids after enumerateDevices");
+
+ const p2 = audio.setSinkId("");
+ is(audio.sinkId, audioDevices[0].deviceId,
+ 'sinkId after setSinkId("") return');
+ is(await p2, undefined,
+ "promise resolution value when sinkId parameter is empty");
+ is(audio.sinkId, "", 'sinkId after setSinkId("") resolution');
+
+ await audio.setSinkId(audioDevices[0].deviceId);
+
+ const {usage: usage2, reportCount} =
+ await collectMemoryUsage(memoryReportPath);
+ is(reportCount, 1,
+ 'Expect only one MediaManager to report in content processes.');
+ is(usage2, usage1, "MediaManager memory usage should return to previous \
+value after promise resolution");
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_setSinkId_default_addTrack.html b/dom/media/webrtc/tests/mochitests/test_setSinkId_default_addTrack.html
new file mode 100644
index 0000000000..64db4cad7c
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_setSinkId_default_addTrack.html
@@ -0,0 +1,52 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+
+<script>
+ createHTML({
+ title: "HTMLMediaElement.setSinkId with default device and adding a track",
+ bug: "1661649",
+ });
+
+ /**
+ * Run a test to verify set sink id in audio element.
+ */
+ runTest(async () => {
+ await pushPrefs(["media.setsinkid.enabled", true]);
+
+ // Expose an audio output device.
+ SpecialPowers.wrap(document).notifyUserGestureActivation();
+ await navigator.mediaDevices.selectAudioOutput();
+
+ const allDevices = await navigator.mediaDevices.enumerateDevices();
+ const audioDevices = allDevices.filter(({kind}) => kind == 'audiooutput');
+ info(`Found ${audioDevices.length} output devices`);
+ isnot(audioDevices.length, 0, "Found output devices");
+
+ const audio = createMediaElement("audio", "audio");
+ document.body.appendChild(audio);
+
+ audio.srcObject = await navigator.mediaDevices.getUserMedia({audio: true});
+ audio.play();
+
+ await audio.setSinkId(audioDevices[0].deviceId);
+ await audio.setSinkId("");
+ is(audio.sinkId, "", "sinkId restored to default");
+
+ audio.srcObject.addTrack((await navigator.mediaDevices.getUserMedia({audio: true})).getTracks()[0]);
+
+ await wait(0);
+
+ for (let t of audio.srcObject.getTracks()) {
+ t.stop();
+ }
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_setSinkId_preMutedElement.html b/dom/media/webrtc/tests/mochitests/test_setSinkId_preMutedElement.html
new file mode 100644
index 0000000000..fb65c3312f
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_setSinkId_preMutedElement.html
@@ -0,0 +1,100 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+ title: "Test changing sink and muting before the MediaStream is set",
+ bug: "1651049",
+ visible: true
+});
+
+let getOutputDeviceId = async () => {
+ let devices = await navigator.mediaDevices.enumerateDevices();
+ let audios = devices.filter(d => d.kind == "audiooutput");
+ ok(audios.length, "One or more output devices found.");
+ return audios[0].deviceId;
+}
+
+let verifyAudioTone = async (ac, stream, freq) => {
+ const toneAnalyser = new AudioStreamAnalyser(ac, stream);
+ return toneAnalyser.waitForAnalysisSuccess(array => {
+ const lowerFreq = freq / 2;
+ const upperFreq = freq + 1000;
+ const lowerMag = array[toneAnalyser.binIndexForFrequency(lowerFreq)];
+ const freqMag = array[toneAnalyser.binIndexForFrequency(freq)];
+ const upperMag = array[toneAnalyser.binIndexForFrequency(upperFreq)];
+ info("Audio tone expected. "
+ + lowerFreq + ": " + lowerMag + ", "
+ + freq + ": " + freqMag + ", "
+ + upperFreq + ": " + upperMag);
+ return lowerMag < 50 && freqMag > 200 && upperMag < 50;
+ });
+}
+
+let verifyNoAudioTone = async (ac, stream, freq) => {
+ const toneAnalyser = new AudioStreamAnalyser(ac, stream);
+ // repeat check 100 times to make sure that it is muted.
+ let retryCnt = 0;
+ return toneAnalyser.waitForAnalysisSuccess(array => {
+ const lowerFreq = freq / 2;
+ const upperFreq = freq + 1000;
+ const lowerMag = array[toneAnalyser.binIndexForFrequency(lowerFreq)];
+ const freqMag = array[toneAnalyser.binIndexForFrequency(freq)];
+ const upperMag = array[toneAnalyser.binIndexForFrequency(upperFreq)];
+ info("No audio tone expected. "
+ + lowerFreq + ": " + lowerMag + ", "
+ + freq + ": " + freqMag + ", "
+ + upperFreq + ": " + upperMag);
+ return lowerMag == 0 && freqMag == 0 && upperMag == 0 && ++retryCnt == 100;
+ });
+}
+
+runTest(async () => {
+ let audioDevice = SpecialPowers.getCharPref("media.audio_loopback_dev", "");
+ if (!audioDevice) {
+ todo(false, "No loopback device set by framework. Try --use-test-media-devices");
+ return;
+ }
+
+ await pushPrefs(["media.setsinkid.enabled", true]);
+
+ // Implicitly expose the loopback device sink by opening the source in the
+ // same group.
+ const verifyStream = await getUserMedia({audio: true});
+ // We gonna test our tone, stop the auto created one.
+ DefaultLoopbackTone.stop();
+
+ let sinkId = await getOutputDeviceId();
+ isnot(sinkId, "", "SinkId is not null");
+
+ let audioElement = createMediaElement('audio', 'audioElement');
+ audioElement.muted = true;
+ await audioElement.setSinkId(sinkId);
+ isnot(audioElement.sinkId, "", "sinkId property of the element is not null");
+
+ // The test stream is a sine tone of 1000 Hz
+ let ac = new AudioContext();
+ const frequency = 2000;
+ let stream = createOscillatorStream(ac, frequency);
+ await verifyAudioTone(ac, stream, frequency);
+
+ audioElement.srcObject = stream;
+ audioElement.play();
+
+ // Verify the silent output using the loopback device.
+ await verifyNoAudioTone(ac, verifyStream, frequency);
+ info("output is muted");
+
+ // Clean up
+ audioElement.pause();
+ audioElement.srcObject = null;
+ verifyStream.getTracks()[0].stop();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_unfocused_pref.html b/dom/media/webrtc/tests/mochitests/test_unfocused_pref.html
new file mode 100644
index 0000000000..22df020f7c
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_unfocused_pref.html
@@ -0,0 +1,49 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <script src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<script>
+"use strict";
+
+createHTML({
+ // This pref exists only for a partner testing framework without WebDriver
+ // switch-to-window nor SpecialPowers to set the active window.
+ // Prefer "focusmanager.testmode".
+ title: "Test media.devices.unfocused.enabled",
+ bug: "1740824"
+});
+
+const blank_url = "/tests/docshell/test/navigation/blank.html";
+
+async function resolveOnEvent(target, name) {
+ return new Promise(r => target.addEventListener(name, r, {once: true}));
+}
+
+runTest(async () => {
+ ok(document.hasFocus(), "This test expects initial focus on the document.");
+ // 'resizable' is requested for a separate OS window on relevant platforms
+ // so that this test tests OS focus changes rather than document visibility.
+ const other = window.open(blank_url, "", "resizable");
+ SimpleTest.registerCleanupFunction(() => {
+ other.close();
+ return SimpleTest.promiseFocus(window);
+ });
+ await Promise.all([
+ resolveOnEvent(window, 'blur'),
+ SimpleTest.promiseFocus(other),
+ pushPrefs(["media.devices.unfocused.enabled", true]),
+ ]);
+ ok(!document.hasFocus(), "!document.hasFocus()");
+ await navigator.mediaDevices.enumerateDevices();
+ ok(true, "enumerateDevices() completes without focus.");
+ // The focus requirement with media.devices.unfocused.enabled false
+ // (default) is tested in
+ // testing/web-platform/mozilla/tests/mediacapture-streams/enumerateDevices-without-focus.https.html
+});
+
+</script>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/turnConfig.js b/dom/media/webrtc/tests/mochitests/turnConfig.js
new file mode 100644
index 0000000000..1267de4ec5
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/turnConfig.js
@@ -0,0 +1,16 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* An example of how to specify two TURN server configs:
+ *
+ * Note: If turn URL uses FQDN rather then an IP address the TURN relay
+ * verification step in checkStatsIceConnectionType might fail.
+ *
+ * var turnServers = {
+ * local: { iceServers: [{"username":"mozilla","credential":"mozilla","url":"turn:10.0.0.1"}] },
+ * remote: { iceServers: [{"username":"firefox","credential":"firefox","url":"turn:10.0.0.2"}] }
+ * };
+ */
+
+var turnServers = {};