diff --git a/CMakeLists.txt b/CMakeLists.txt index 15b221f16..da95a7529 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -185,7 +185,18 @@ set_target_properties(libc++abi PROPERTIES IMPORTED_OBJECTS_DEBUG "${libc++abi_o # M114: branch-heads/5735 set(WEBRTC_REVISION branch-heads/5735) -file(STRINGS nix.gni NIX_GN_GEN_ARGS) +# nix.gni is generated by the Nix shell hook (shell.nix) with platform-specific +# GN args like clang_base_path and mac_sdk_path. For non-Nix builds, create an +# empty file or one with: is_clang=true\nuse_lld=false\nclang_use_chrome_plugins=false +if(EXISTS "${CMAKE_SOURCE_DIR}/nix.gni") + file(STRINGS nix.gni NIX_GN_GEN_ARGS) +else() + set(NIX_GN_GEN_ARGS + "is_clang=true" + "use_lld=false" + "clang_use_chrome_plugins=false" + ) +endif() list(APPEND GN_GEN_ARGS rtc_build_examples=false rtc_use_x11=false @@ -261,6 +272,7 @@ else() ${libwebrtc_binary_dir}/obj/api/video_codecs/libbuiltin_video_encoder_factory.a ${libwebrtc_binary_dir}/obj/api/video_codecs/libbuiltin_video_decoder_factory.a ${libwebrtc_binary_dir}/obj/media/librtc_internal_video_codecs.a + ${libwebrtc_binary_dir}/obj/media/librtc_simulcast_encoder_adapter.a ) endif() @@ -277,6 +289,7 @@ ExternalProject_Add( BUILD_BYPRODUCTS ${byproducts} DOWNLOAD_COMMAND ${CMAKE_COMMAND} -E env DEPOT_TOOLS=${depot_tools_install_dir} PLATFORM=${PLATFORM} WEBRTC_REVISION=${WEBRTC_REVISION} ${CMAKE_SOURCE_DIR}/scripts/download-webrtc.${suffix} + PATCH_COMMAND ${CMAKE_SOURCE_DIR}/scripts/patch-webrtc.sh ${CMAKE_SOURCE_DIR}/patches CONFIGURE_COMMAND ${CMAKE_COMMAND} -E env BINARY_DIR= DEPOT_TOOLS=${depot_tools_install_dir} GN_GEN_ARGS=${GN_GEN_ARGS} SOURCE_DIR= ${CMAKE_SOURCE_DIR}/scripts/configure-webrtc.${suffix} BUILD_COMMAND ${CMAKE_COMMAND} -E env CMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} DEPOT_TOOLS=${depot_tools_install_dir} ${CMAKE_SOURCE_DIR}/scripts/build-webrtc.${suffix} INSTALL_COMMAND "" @@ -321,6 +334,10 @@ add_library(librtc_internal_video_codecs STATIC IMPORTED) add_dependencies(librtc_internal_video_codecs project_libwebrtc) set_property(TARGET librtc_internal_video_codecs PROPERTY IMPORTED_LOCATION "${libwebrtc_binary_dir}/obj/media/librtc_internal_video_codecs.a") +add_library(librtc_simulcast_encoder_adapter STATIC IMPORTED) +add_dependencies(librtc_simulcast_encoder_adapter project_libwebrtc) +set_property(TARGET librtc_simulcast_encoder_adapter PROPERTY IMPORTED_LOCATION "${libwebrtc_binary_dir}/obj/media/librtc_simulcast_encoder_adapter.a") + set(libc++_include_dir "${libwebrtc_source_dir}/src/buildtools/third_party/libc++/trunk/include" "${libwebrtc_source_dir}/src/buildtools/third_party/libc++" @@ -403,6 +420,7 @@ target_link_libraries(${MODULE} PRIVATE libbuiltin_video_encoder_factory libbuiltin_video_decoder_factory librtc_internal_video_codecs + librtc_simulcast_encoder_adapter ${CMAKE_JS_LIB} ) diff --git a/package.json b/package.json index 0f2b7f3ad..37eb9e719 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,6 @@ "patch-package": "^8.0.0", "prettier": "^3.4.2", "recursive-copy": "^2.0.14", - "simple-peer": "~9.7.0", "tape": "^5.6.1", "temp": "^0.9.4" }, diff --git a/patches/webrtc-macos26-sdk.patch b/patches/webrtc-macos26-sdk.patch new file mode 100644 index 000000000..c67f9fbe1 --- /dev/null +++ b/patches/webrtc-macos26-sdk.patch @@ -0,0 +1,14 @@ +--- a/modules/desktop_capture/mac/screen_capturer_mac.mm ++++ b/modules/desktop_capture/mac/screen_capturer_mac.mm +@@ -24,6 +24,11 @@ + // These have the correct annotation. See https://crbug.com/1431897. + // TODO(thakis): Remove this once FB12109479 is fixed and we updated to an SDK + // with the fix. ++// NOTE: CG_AVAILABLE_BUT_DEPRECATED was removed in macOS 26 SDK. ++#ifndef CG_AVAILABLE_BUT_DEPRECATED ++#define CG_AVAILABLE_BUT_DEPRECATED(from, to, ...) \ ++ API_DEPRECATED(__VA_ARGS__, macos(from, to)) ++#endif + + static CGDisplayStreamRef __nullable + wrapCGDisplayStreamCreate(CGDirectDisplayID display, diff --git a/scripts/build-webrtc.sh b/scripts/build-webrtc.sh index b00f1f052..f3002e3c7 100755 --- a/scripts/build-webrtc.sh +++ b/scripts/build-webrtc.sh @@ -6,6 +6,6 @@ set -v # We want to use system ninja, _NOT_ depot_tools ninja, actually export PATH="${DEPOT_TOOLS}/python-bin:${PATH}:${DEPOT_TOOLS}" -export TARGETS="webrtc libjingle_peerconnection libc++ libc++abi builtin_video_encoder_factory builtin_video_decoder_factory rtc_internal_video_codecs" +export TARGETS="webrtc libjingle_peerconnection libc++ libc++abi builtin_video_encoder_factory builtin_video_decoder_factory rtc_internal_video_codecs rtc_simulcast_encoder_adapter" ninja $TARGETS diff --git a/scripts/patch-webrtc.sh b/scripts/patch-webrtc.sh new file mode 100755 index 000000000..833d9cf57 --- /dev/null +++ b/scripts/patch-webrtc.sh @@ -0,0 +1,17 @@ +#!/bin/bash +# Apply patches to the downloaded WebRTC source. +# Skips patches that have already been applied (idempotent). +set -e + +SOURCE_DIR="$1" +PATCHES_DIR="$2" + +for patch in "$PATCHES_DIR"/webrtc-*.patch; do + [ -f "$patch" ] || continue + if git -C "$SOURCE_DIR" apply --check "$patch" 2>/dev/null; then + echo "Applying patch: $(basename "$patch")" + git -C "$SOURCE_DIR" apply "$patch" + else + echo "Skipping already-applied patch: $(basename "$patch")" + fi +done diff --git a/src/interfaces/rtc_data_channel.cc b/src/interfaces/rtc_data_channel.cc index 65485a1bf..6d43dc563 100644 --- a/src/interfaces/rtc_data_channel.cc +++ b/src/interfaces/rtc_data_channel.cc @@ -30,7 +30,9 @@ DataChannelObserver::DataChannelObserver( PeerConnectionFactory *factory, rtc::scoped_refptr jingleDataChannel) : _factory(factory), _jingleDataChannel(std::move(jingleDataChannel)) { - _jingleDataChannel->RegisterObserver(this); + // Don't register here. Registration happens in RTCDataChannel's constructor, + // which avoids a double RegisterObserver that causes message loss through + // M114's ObserverAdapter signaling thread relay. } void DataChannelObserver::OnStateChange() { @@ -72,9 +74,20 @@ RTCDataChannel::RTCDataChannel(const Napi::CallbackInfo &info) _jingleDataChannel = observer->_jingleDataChannel; _jingleDataChannel->RegisterObserver(this); - // Re-queue cached observer events + // Re-queue any cached observer events (from the window between + // OnDataChannel and this constructor). requeue(*observer, *this); + // If the channel already transitioned to open before we registered, + // dispatch the open event so JS onopen handlers fire. + auto state = _jingleDataChannel->state(); + if (state == webrtc::DataChannelInterface::kOpen) { + Dispatch( + Callback1::Create([state](RTCDataChannel &channel) { + RTCDataChannel::HandleStateChange(channel, state); + })); + } + delete observer; // NOTE(mroberts): These doesn't actually matter yet. @@ -106,6 +119,8 @@ void RTCDataChannel::CleanupInternals() { void RTCDataChannel::OnPeerConnectionClosed() { if (_jingleDataChannel != nullptr) { + CleanupInternals(); + HandleStateChange(*this, webrtc::DataChannelInterface::kClosed); Stop(); } } diff --git a/src/interfaces/rtc_peer_connection.cc b/src/interfaces/rtc_peer_connection.cc index 628af31c8..a3c576413 100644 --- a/src/interfaces/rtc_peer_connection.cc +++ b/src/interfaces/rtc_peer_connection.cc @@ -777,10 +777,16 @@ Napi::Value RTCPeerConnection::Close(const Napi::CallbackInfo &info) { if (_jinglePeerConnection) { _cached_configuration = ExtendedRTCConfiguration( _jinglePeerConnection->GetConfiguration(), _port_range); - _jinglePeerConnection->Close(); - // NOTE(mroberts): Perhaps another way to do this is to just register all - // remote MediaStreamTracks against this RTCPeerConnection, not unlike what - // we do with RTCDataChannels. + + // Fire all JS close events proactively, matching Chrome/Blink's approach. + // Blink dispatches events synchronously during RTCPeerConnection.close() + // rather than relying on the C++ observer callback path. This is necessary + // because PeerConnection::Close() calls PrepareForShutdown() which + // deactivates the SafeTask safety flag, silently cancelling any pending + // async callbacks from the network thread. + for (auto channel : _channels) { + channel->OnPeerConnectionClosed(); + } if (_jinglePeerConnection->GetConfiguration().sdp_semantics == webrtc::SdpSemantics::kUnifiedPlan) { for (const auto &transceiver : _jinglePeerConnection->GetTransceivers()) { @@ -789,9 +795,7 @@ Napi::Value RTCPeerConnection::Close(const Napi::CallbackInfo &info) { track->OnPeerConnectionClosed(); } } - for (auto channel : _channels) { - channel->OnPeerConnectionClosed(); - } + _jinglePeerConnection->Close(); } // Clear the wrap caches before releasing the WebRTC peer connection. diff --git a/src/interfaces/rtc_peer_connection/peer_connection_factory.cc b/src/interfaces/rtc_peer_connection/peer_connection_factory.cc index e3b4c3046..5bb0e71a5 100644 --- a/src/interfaces/rtc_peer_connection/peer_connection_factory.cc +++ b/src/interfaces/rtc_peer_connection/peer_connection_factory.cc @@ -52,10 +52,22 @@ PeerConnectionFactory::PeerConnectionFactory(const Napi::CallbackInfo &info) // TODO(mroberts): Read `audioLayer` from some PeerConnectionFactoryOptions? auto audioLayer = MakeNothing(); - _workerThread = rtc::Thread::CreateWithSocketServer(); - assert(_workerThread); + _networkThread = rtc::Thread::CreateWithSocketServer(); + assert(_networkThread); bool result = + _networkThread->SetName("PeerConnectionFactory:networkThread", nullptr); + assert(result); + (void)result; + + result = _networkThread->Start(); + assert(result); + (void)result; + + _workerThread = rtc::Thread::Create(); + assert(_workerThread); + + result = _workerThread->SetName("PeerConnectionFactory:workerThread", nullptr); assert(result); (void)result; @@ -93,7 +105,7 @@ PeerConnectionFactory::PeerConnectionFactory(const Napi::CallbackInfo &info) (void)result; _factory = webrtc::CreatePeerConnectionFactory( - _workerThread.get(), _workerThread.get(), _signalingThread.get(), + _networkThread.get(), _workerThread.get(), _signalingThread.get(), _audioDeviceModule, webrtc::CreateBuiltinAudioEncoderFactory(), webrtc::CreateBuiltinAudioDecoderFactory(), webrtc::CreateBuiltinVideoEncoderFactory(), @@ -105,11 +117,11 @@ PeerConnectionFactory::PeerConnectionFactory(const Napi::CallbackInfo &info) _factory->SetOptions(options); _networkManager = std::unique_ptr( - new rtc::BasicNetworkManager(_workerThread->socketserver())); + new rtc::BasicNetworkManager(_networkThread->socketserver())); assert(_networkManager != nullptr); _socketFactory = std::unique_ptr( - new rtc::BasicPacketSocketFactory(_workerThread->socketserver())); + new rtc::BasicPacketSocketFactory(_networkThread->socketserver())); assert(_socketFactory != nullptr); } @@ -118,9 +130,11 @@ PeerConnectionFactory::~PeerConnectionFactory() { _workerThread->BlockingCall([this]() { this->_audioDeviceModule = nullptr; }); + _networkThread->Stop(); _workerThread->Stop(); _signalingThread->Stop(); + _networkThread = nullptr; _workerThread = nullptr; _signalingThread = nullptr; diff --git a/src/interfaces/rtc_peer_connection/peer_connection_factory.hh b/src/interfaces/rtc_peer_connection/peer_connection_factory.hh index bb6257007..f477076ba 100644 --- a/src/interfaces/rtc_peer_connection/peer_connection_factory.hh +++ b/src/interfaces/rtc_peer_connection/peer_connection_factory.hh @@ -62,6 +62,7 @@ public: static void Dispose(); private: + std::unique_ptr _networkThread; std::unique_ptr _signalingThread; std::unique_ptr _workerThread; diff --git a/src/node/error_factory.cc b/src/node/error_factory.cc index 35aafe0f3..e5ec72e72 100644 --- a/src/node/error_factory.cc +++ b/src/node/error_factory.cc @@ -94,6 +94,8 @@ node_webrtc::ErrorFactory::DOMExceptionNameToString(DOMExceptionName name) { return "NetworkError"; case kOperationError: return "OperationError"; + default: + return "UnknownError"; } } diff --git a/test/custom-settings.js b/test/custom-settings.js index 45d8b3534..668c3bf44 100644 --- a/test/custom-settings.js +++ b/test/custom-settings.js @@ -1,92 +1,107 @@ -/* eslint no-console:0 */ "use strict"; -var tape = require("tape"); -var SimplePeer = require("simple-peer"); -var wrtc = require(".."); +const tape = require("tape"); +const wrtc = require(".."); -tape("custom ports connect once", function (t) { - t.plan(1); - connectClientServer({ min: 9000, max: 9010 }, function (err) { - t.error(err, "connectClientServer callback"); - }); -}); +const RTCPeerConnection = wrtc.RTCPeerConnection; -tape("custom ports connect concurrently", function (t) { - const n = 2; +async function connectWithConfig(config) { + let resolve; + let reject; + const done = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); - t.plan(n); - const portRange = { min: 9000, max: 9010 }; + const pc1 = new RTCPeerConnection({ iceServers: [] }); + const pc2 = new RTCPeerConnection(Object.assign({ iceServers: [] }, config)); - function callback(err) { - t.error(err, "connectClientServer callback"); - } + pc1.onicecandidate = function (e) { + if (e.candidate) pc2.addIceCandidate(e.candidate); + }; - for (let i = 0; i < n; i++) { - connectClientServer(portRange, callback); - } -}); + pc2.onicecandidate = function (e) { + if (e.candidate) { + if (config.portRange) { + const { min, max } = config.portRange; + const port = parsePort(e.candidate.candidate); + if (port < min || port > max) { + pc1.close(); + pc2.close(); + reject( + new Error( + `candidate port ${port} outside range ${min} - ${max}: ${e.candidate.candidate}`, + ), + ); + return; + } + } + pc1.addIceCandidate(e.candidate); + } + }; -function connectClientServer(portRange, callback) { - const client = new SimplePeer({ - wrtc: wrtc, - initiator: true, - }); + const dc = pc1.createDataChannel("test"); + pc2.ondatachannel = function (evt) { + evt.channel.onmessage = function () { + pc1.close(); + pc2.close(); + resolve(); + }; + }; + dc.onopen = function () { + dc.send("hello"); + }; - const server = new SimplePeer({ - wrtc: wrtc, - initiator: false, - config: { - portRange: portRange, - }, - }); + try { + const offer = await pc1.createOffer(); + await pc1.setLocalDescription(offer); + await pc2.setRemoteDescription(pc1.localDescription); + const answer = await pc2.createAnswer(); + await pc2.setLocalDescription(answer); + await pc1.setRemoteDescription(pc2.localDescription); + } catch (err) { + pc1.close(); + pc2.close(); + reject(err); + } - client.on("signal", function (data) { - server.signal(data); - }); - server.on("signal", function (data) { - if ( - data.candidate && - !isValidCandidate( - data.candidate.candidate, - portRange || { min: 0, max: 65535 }, - true, - ) - ) { - callback( - `candidate must follow port range (${portRange}): ${data.candidate.candidate}`, - ); - } - client.signal(data); - }); - server.on("connect", function () { - server.send("xyz"); - }); - client.on("data", function () { - callback(); - server.destroy(); - client.destroy(); - }); - client.on("error", function (e) { - callback(e); - server.destroy(); - client.destroy(); - }); - server.on("error", function (e) { - callback(e); - client.destroy(); - server.destroy(); - }); + return done; } -function isValidCandidate(candidate, portRange) { - const port = candidate.replace( - /candidate:([^\s]+)\s([^\s]+)\s([^\s]+)\s([^\s]+)\s([^\s]+)\s([0-9]+)\styp.*/, - "$6", +function parsePort(candidate) { + const match = candidate.match( + /candidate:\S+\s\d+\s\S+\s\d+\s\S+\s(\d+)\styp/, ); + return match ? parseInt(match[1], 10) : -1; +} - const minPort = portRange.min; - const maxPort = portRange.max; +tape("custom ports connect once", async function (t) { + t.plan(1); + try { + await connectWithConfig({ portRange: { min: 9000, max: 9010 } }); - return minPort <= parseInt(port) && maxPort >= parseInt(port); -} + t.pass("ConnectClientServer pass"); + } catch (err) { + t.error(err, "connectClientServer callback"); + } +}); + +tape("custom ports connect concurrently", async function (t) { + const n = 2; + t.plan(n); + let promises = []; + + for (let i = 0; i < n; i++) { + promises.push( + connectWithConfig({ portRange: { min: 9000, max: 9010 } }) + .then(function () { + t.pass("connectClientServer pass"); + }) + .catch(function (err) { + t.error(err, "connectClientServer error"); + }), + ); + } + + await Promise.all(promises); +}); diff --git a/test/multiconnect.js b/test/multiconnect.js index 2e3cbda80..27b90a157 100644 --- a/test/multiconnect.js +++ b/test/multiconnect.js @@ -1,129 +1,119 @@ -/* eslint no-console:0, no-process-env:0 */ "use strict"; -var tape = require("tape"); -var SimplePeer = require("simple-peer"); -var wrtc = require(".."); +const tape = require("tape"); +const wrtc = require(".."); -var log = process.env.LOG ? console.log : function () {}; +const RTCPeerConnection = wrtc.RTCPeerConnection; -tape("connect once", function (t) { - t.plan(1); - log("###########################\n"); - connect(function (err) { - t.error(err, "connect once callback"); +async function connect() { + let resolve; + let reject; + const done = new Promise((res, rej) => { + resolve = res; + reject = rej; }); -}); + const pc1 = new RTCPeerConnection({ iceServers: [] }); + const pc2 = new RTCPeerConnection({ iceServers: [] }); + + pc1.onicecandidate = function (e) { + if (e.candidate) pc2.addIceCandidate(e.candidate); + }; + pc2.onicecandidate = function (e) { + if (e.candidate) pc1.addIceCandidate(e.candidate); + }; + + const dc = pc1.createDataChannel("test"); + + pc2.ondatachannel = function (evt) { + evt.channel.onmessage = function (msg) { + pc1.close(); + pc2.close(); + resolve(msg.data); + }; + }; + + dc.onopen = function () { + dc.send("hello"); + }; + + try { + const offer = await pc1.createOffer(); + await pc1.setLocalDescription(offer); + await pc2.setRemoteDescription(pc1.localDescription); + const answer = await pc2.createAnswer(); + await pc2.setLocalDescription(answer); + await pc1.setRemoteDescription(pc2.localDescription); + } catch (err) { + pc1.close(); + pc2.close(); + reject(err); + } -tape("connect loop", function (t) { - t.plan(1); - log("###########################\n"); - connectLoop(10, function (err) { - t.error(err, "connect loop callback"); - }); -}); + return done; +} -tape("connect concurrent", function (t) { - var n = 10; - t.plan(n); - log("###########################\n"); - for (var i = 0; i < n; i += 1) { - connect(callback); +async function connectLoop(count) { + for (let i = 0; i < count; i++) { + await connect(); } +} - function callback(err) { - t.error(err, "connect concurrent callback"); +tape("connect once", async function (t) { + t.plan(1); + try { + await connect(); + t.pass("connect once pass"); + } catch (err) { + t.error(err, "connect once callback"); } }); -tape("connect loop concurrent", function (t) { - var n = 10; - t.plan(n); - log("###########################\n"); - for (var i = 0; i < n; i += 1) { - connectLoop(10, callback); - } - - function callback(err) { - t.error(err, "connect loop concurrent callback"); +tape("connect loop", async function (t) { + t.plan(1); + try { + await connectLoop(10); + t.pass("connect loop completed"); + } catch (err) { + t.error(err, "connect loop callback"); } }); -var connIdGen = 1; - -function connect(callback) { - var connId = connIdGen; - var connName = "CONNECTION-" + connId; - connIdGen += 1; - log(connName, "starting"); - - // setup two peers with simple-peer - var peer1 = new SimplePeer({ - wrtc: wrtc, - }); - var peer2 = new SimplePeer({ - wrtc: wrtc, - initiator: true, - }); +tape("connect concurrent", async function (t) { + const n = 10; + t.plan(n); - function cleanup() { - if (peer1) { - peer1.destroy(); - peer1 = null; - } - if (peer2) { - peer2.destroy(); - peer2 = null; - } + let promises = []; + for (let i = 0; i < n; i++) { + promises.push( + connect() + .then(function () { + t.pass("connect concurrent pass"); + }) + .catch(function (err) { + t.error(err, "connect concurrent error"); + }), + ); } - // when peer1 has signaling data, give it to peer2, and vice versa - peer1.on("signal", function (data) { - log(connName, "signal peer1 -> peer2:"); - log(" ", data); - peer2.signal(data); - }); - peer2.on("signal", function (data) { - log(connName, "signal peer2 -> peer1:"); - log(" ", data); - peer1.signal(data); - }); - - peer1.on("error", function (err) { - log(connName, "peer1 error", err); - cleanup(); - callback(err); - }); - peer2.on("error", function (err) { - log(connName, "peer2 error", err); - cleanup(); - callback(err); - }); + await Promise.all(promises); +}); - // wait for 'connect' event - peer1.on("connect", function () { - log(connName, "sending message"); - peer1.send("peers are for kids"); - }); - peer2.on("data", function () { - log(connName, "completed"); - cleanup(); - callback(); - }); -} +tape("connect loop concurrent", async function (t) { + const n = 10; + t.plan(n); -function connectLoop(count, callback) { - if (count <= 0) { - log("connect loop completed"); - callback(); - } else { - log("connect loop remain", count); - connect(function (err) { - if (err) { - callback(err); - } else { - connectLoop(count - 1, callback); - } - }); + let promises = []; + for (let i = 0; i < n; i++) { + promises.push( + connectLoop(n) + .then(function () { + t.pass("connectLoop concurrent pass"); + }) + .catch(function (err) { + t.error(err, "connectLoop concurrent error"); + }), + ); } -} + + await Promise.all(promises); +}); diff --git a/test/rtcaudiosink.js b/test/rtcaudiosink.js index 05fc24618..c777c7db0 100644 --- a/test/rtcaudiosink.js +++ b/test/rtcaudiosink.js @@ -68,13 +68,9 @@ test("RTCAudioSink should send even ondata when ondata is defined in ontrack eve // console.log('pcB: onicegatheringstatechange:', e.target.iceGatheringState); // }; - pcB.ontrack = (e) => - setTimeout(() => { - sink = new RTCAudioSink(e.track); - sink.addEventListener("data", () => { - ondataDidFired += 1; - }); - }, 1); + var TARGET = 10; + var received = 0; + var interval; setupPerfectNegotiation(pcA, pcB, true); setupPerfectNegotiation(pcB, pcA, false); @@ -83,29 +79,51 @@ test("RTCAudioSink should send even ondata when ondata is defined in ontrack eve const track = source.createTrack(); pcA.addTrack(track); + function done() { + clearInterval(interval); + if (sink) sink.stop(); + track.stop(); + pcA.close(); + pcB.close(); + } + + pcB.ontrack = (e) => + setTimeout(() => { + sink = new RTCAudioSink(e.track); + sink.addEventListener("data", () => { + ondataDidFired += 1; + if (ondataDidFired >= TARGET && received === 0) { + received = ondataDidFired; + done(); + t.ok( + received >= TARGET, + "RTCAudioSink fired at least " + TARGET + " times", + ); + t.end(); + } + }); + }, 1); + const sampleRate = 8000; const samples = new Int16Array(sampleRate / 100); for (let n = 0; n < samples.length; n++) { samples[n] = Math.random() * 0xffff; } - const interval = setInterval(() => { + interval = setInterval(() => { source.onData({ samples, sampleRate }); }, 10); setTimeout(() => { - clearInterval(interval); - // yes > 9 and not 10 because some random thing in eventloop and setinterval/timeout result in values to be 9||10||11 - t.ok( - ondataDidFired >= 9, - "RTCAudioSink should have fired 10 time in 100ms" - ); - sink.stop(); - track.stop(); - pcA.close(); - pcB.close(); - t.end(); - }, 105); + if (received === 0) { + // Prevent a late data event (where ondataDidFired reaches TARGET + // after this timeout) from calling done()/t.end() a second time. + received = -1; + done(); + t.fail("RTCAudioSink only fired " + ondataDidFired + " times in 2s"); + t.end(); + } + }, 2000); }); test("RTCAudioSink should send ondata events when defined outside ontrack", (t) => { @@ -150,32 +168,52 @@ test("RTCAudioSink should send ondata events when defined outside ontrack", (t) pcA.addTrack(track); const sink = new RTCAudioSink(track); - sink.addEventListener("data", () => { - ondataDidFired += 1; - }); const sampleRate = 8000; const samples = new Int16Array(sampleRate / 100); for (let n = 0; n < samples.length; n++) { samples[n] = Math.random() * 0xffff; } - const interval = setInterval(() => { - source.onData({ samples, sampleRate }); - }, 10); - setTimeout(() => { + var TARGET = 10; + var received = 0; + var interval; + + function done() { clearInterval(interval); - // TODO(jack): yes >= 9 and not 10 because some random thing in eventloop and setinterval/timeout result in values to be 9||10||11 - t.ok( - ondataDidFired >= 9, - "RTCAudioSink should have fired 10 time in 100ms" - ); sink.stop(); track.stop(); pcA.close(); pcB.close(); - t.end(); - }, 105); + } + + sink.addEventListener("data", () => { + ondataDidFired += 1; + if (ondataDidFired >= TARGET && received === 0) { + received = ondataDidFired; + done(); + t.ok( + received >= TARGET, + "RTCAudioSink fired at least " + TARGET + " times", + ); + t.end(); + } + }); + + interval = setInterval(() => { + source.onData({ samples, sampleRate }); + }, 10); + + setTimeout(() => { + if (received === 0) { + // Prevent a late data event (where ondataDidFired reaches TARGET + // after this timeout) from calling done()/t.end() a second time. + received = -1; + done(); + t.fail("RTCAudioSink only fired " + ondataDidFired + " times in 2s"); + t.end(); + } + }, 2000); }); /** diff --git a/test/rtcrtpsender.js b/test/rtcrtpsender.js index 1745ffa6f..7eefd5bd7 100644 --- a/test/rtcrtpsender.js +++ b/test/rtcrtpsender.js @@ -140,9 +140,9 @@ tape( return pc.createOffer().then(function (offer) { t.equal( (offer.sdp.match(/a=msid:/g) || []).length, - 6, - "even duplicates get added", - ); // 3 streams per track and 2 tracks (audio + video) = 6 msid lines + 4, + "duplicates are deduplicated", + ); // 2 unique stream IDs per track × 2 tracks (audio + video) = 4 msid lines pc.close(); t.end(); });