Skip to content

Commit 9877c92

Browse files
committed
Add support for RTCRtpScriptTransform
https://bugs.webkit.org/show_bug.cgi?id=219148 Reviewed by Eric Carlson. Source/WebCore: We introduce RTCRtpScriptTransform which processes encoded frames in a worker for either RTCRtpSender or RTCRtpReceiver. The model follows AudioWorkletNode in the sense that we create a RTCRtpScriptTransform object in main thread that is used with RTCRtp objects. The RTCRtpScriptTransform takes a name and a worker as parameters to create a RTCRtpScriptTransformer counter part in a worker. Before that, RTCRtpScriptTransformer constructors are registered in the worker with a specific name. A message port is shared between RTCRtpScriptTransform and RTCRtpScriptTransformer. RTCRtpScriptTransform keeps a weak pointer to RTCRtpScriptTransformer so that we keep all ref counting of RTCRtpScriptTransformer in the worker thread. To make sure RTCRtpScriptTransformer stays alive for long enough, we set a pending activity when RTCRtpScriptTransform is linked to its RTCRtpScriptTransformer. The pending activity is then removed either at worker closure or RTCRtpScriptTransform being no longer doing processing. We expose individual compressed frames as RTCEncodedAudioFrame and RTCEncodedVideoFrame. Accessor is limited to the raw data but additional getters should be added later on. To implement RTCRtpScriptTransformer, we have to be able to create WritableStream with native sinks. This is why we introduce WritableStreamSink and WritableStream C++ classes. Binding between native frames and streams is done through RTCRtpReadableStreamSource and RTCRtpWritableStreamSink. Test: http/wpt/webrtc/webrtc-transform.html and http/wpt/webrtc/sframe-transform.html. * CMakeLists.txt: * DerivedSources-input.xcfilelist: * DerivedSources-output.xcfilelist: * DerivedSources.make: * Modules/mediastream/RTCEncodedAudioFrame.cpp: Added. (WebCore::RTCEncodedAudioFrame::RTCEncodedAudioFrame): * Modules/mediastream/RTCEncodedAudioFrame.h: Added. (WebCore::RTCEncodedAudioFrame::create): * Modules/mediastream/RTCEncodedAudioFrame.idl: Added. * Modules/mediastream/RTCEncodedFrame.h: Added. * Modules/mediastream/RTCEncodedFrame.cpp: Added. * Modules/mediastream/RTCEncodedVideoFrame.cpp: Added. (WebCore::RTCEncodedVideoFrame::RTCEncodedVideoFrame): * Modules/mediastream/RTCEncodedVideoFrame.h: Added. (WebCore::RTCEncodedVideoFrame::create): * Modules/mediastream/RTCEncodedVideoFrame.idl: Added. * Modules/mediastream/RTCRtpReceiver+Transform.idl: * Modules/mediastream/RTCRtpReceiver.cpp: (WebCore::RTCRtpReceiver::setTransform): (WebCore::RTCRtpReceiver::transform): * Modules/mediastream/RTCRtpReceiver.h: * Modules/mediastream/RTCRtpReceiverWithTransform.h: (WebCore::RTCRtpReceiverWithTransform::transform): (WebCore::RTCRtpReceiverWithTransform::setTransform): * Modules/mediastream/RTCRtpSFrameTransform.cpp: (WebCore::RTCRtpSFrameTransform::initializeTransformer): * Modules/mediastream/RTCRtpSFrameTransform.h: (WebCore::RTCRtpSFrameTransform::isAttached const): * Modules/mediastream/RTCRtpSFrameTransform.idl: * Modules/mediastream/RTCRtpScriptTransform.cpp: Added. (WebCore::RTCRtpScriptTransform::create): (WebCore::RTCRtpScriptTransform::RTCRtpScriptTransform): (WebCore::RTCRtpScriptTransform::~RTCRtpScriptTransform): (WebCore::RTCRtpScriptTransform::setTransformer): (WebCore::RTCRtpScriptTransform::initializeBackendForReceiver): (WebCore::RTCRtpScriptTransform::initializeBackendForSender): (WebCore::RTCRtpScriptTransform::willClearBackend): (WebCore::RTCRtpScriptTransform::initializeTransformer): * Modules/mediastream/RTCRtpScriptTransform.h: Added. * Modules/mediastream/RTCRtpScriptTransform.idl: Added. * Modules/mediastream/RTCRtpScriptTransformProvider.idl: Added. * Modules/mediastream/RTCRtpScriptTransformer.cpp: Added. (WebCore::RTCRtpReadableStreamSource::create): (WebCore::RTCRtpReadableStreamSource::close): (WebCore::RTCRtpReadableStreamSource::enqueue): (WebCore::RTCRtpWritableStreamSink::create): (WebCore::RTCRtpWritableStreamSink::RTCRtpWritableStreamSink): (WebCore::RTCRtpWritableStreamSink::write): (WebCore::RTCRtpScriptTransformer::create): (WebCore::RTCRtpScriptTransformer::RTCRtpScriptTransformer): (WebCore::RTCRtpScriptTransformer::~RTCRtpScriptTransformer): (WebCore::RTCRtpScriptTransformer::start): (WebCore::RTCRtpScriptTransformer::clear): * Modules/mediastream/RTCRtpScriptTransformer.h: Added. (WebCore::RTCRtpScriptTransformer::setCallback): (WebCore::RTCRtpScriptTransformer::port): (WebCore::RTCRtpScriptTransformer::startPendingActivity): (WebCore::RTCRtpScriptTransformer::activeDOMObjectName const): (WebCore::RTCRtpScriptTransformer::stopPendingActivity): * Modules/mediastream/RTCRtpScriptTransformer.idl: Added. * Modules/mediastream/RTCRtpScriptTransformerConstructor.h: Added. * Modules/mediastream/RTCRtpScriptTransformerConstructor.idl: Added. * Modules/mediastream/RTCRtpSender+Transform.idl: * Modules/mediastream/RTCRtpSender.cpp: (WebCore::RTCRtpSender::setTransform): (WebCore::RTCRtpSender::transform): * Modules/mediastream/RTCRtpSender.h: * Modules/mediastream/RTCRtpSenderWithTransform.h: (WebCore::RTCRtpSenderWithTransform::transform): (WebCore::RTCRtpSenderWithTransform::setTransform): * Modules/mediastream/RTCRtpTransform.cpp: (WebCore::RTCRtpTransform::from): (WebCore::RTCRtpTransform::RTCRtpTransform): (WebCore::RTCRtpTransform::~RTCRtpTransform): (WebCore::RTCRtpTransform::isAttached const): (WebCore::RTCRtpTransform::attachToReceiver): (WebCore::RTCRtpTransform::attachToSender): (WebCore::RTCRtpTransform::clearBackend): (WebCore::RTCRtpTransform::detachFromReceiver): (WebCore::RTCRtpTransform::detachFromSender): * Modules/mediastream/RTCRtpTransform.h: (WebCore::RTCRtpTransform::internalTransform): * Modules/mediastream/RTCRtpTransformBackend.h: * Modules/mediastream/RTCRtpTransformableFrame.h: * Modules/mediastream/libwebrtc/LibWebRTCRtpTransformBackend.cpp: (WebCore::LibWebRTCRtpTransformBackend::setInputCallback): (WebCore::LibWebRTCRtpTransformBackend::Transform): * Modules/streams/WritableStreamSink.h: Added. * Modules/streams/WritableStreamSink.idl: Added. * Sources.txt: * WebCore.xcodeproj/project.pbxproj: * bindings/js/ReadableStreamDefaultController.cpp: (WebCore::ReadableStreamDefaultController::enqueue): * bindings/js/ReadableStreamDefaultController.h: * bindings/js/WebCoreBuiltinNames.h: * bindings/js/WritableStream.cpp: Added. (WebCore::WritableStream::create): * bindings/js/WritableStream.h: Added. (WebCore::JSWritableStreamWrapperConverter::toWrapped): (WebCore::WritableStream::WritableStream): (WebCore::toJS): (WebCore::toJSNewlyCreated): * dom/EventTargetFactory.in: * testing/Internals.cpp: * testing/Internals.h: * testing/Internals.idl: * testing/MockRTCRtpTransform.cpp: (WebCore::MockRTCRtpTransformer::transform): * workers/DedicatedWorkerGlobalScope.cpp: (WebCore::DedicatedWorkerGlobalScope::registerRTCRtpScriptTransformer): (WebCore::DedicatedWorkerGlobalScope::createRTCRtpScriptTransformer): * workers/DedicatedWorkerGlobalScope.h: * workers/DedicatedWorkerGlobalScope.idl: * workers/Worker.cpp: (WebCore::Worker::addRTCRtpScriptTransformer): (WebCore::Worker::createRTCRtpScriptTransformer): (WebCore::Worker::postTaskToWorkerGlobalScope): * workers/Worker.h: * workers/WorkerGlobalScopeProxy.h: * workers/WorkerMessagingProxy.cpp: (WebCore::WorkerMessagingProxy::postTaskToWorkerObject): (WebCore::WorkerMessagingProxy::postMessageToWorkerGlobalScope): (WebCore::WorkerMessagingProxy::postTaskToWorkerGlobalScope): * workers/WorkerMessagingProxy.h: * workers/WorkerObjectProxy.h: (WebCore::WorkerObjectProxy::postTaskToWorkerObject): LayoutTests: * http/wpt/webrtc/routines.js: Added. (createConnections): * http/wpt/webrtc/script-transform.js: Added. (MockRTCRtpTransformer): (MockRTCRtpTransformer.prototype.start): (MockRTCRtpTransformer.prototype.process): * http/wpt/webrtc/sframe-transform-expected.txt: Added. * http/wpt/webrtc/sframe-transform.html: Added. * http/wpt/webrtc/webrtc-transform-expected.txt: Renamed from LayoutTests/webrtc/webrtc-transform-expected.txt. * http/wpt/webrtc/webrtc-transform.html: Renamed from LayoutTests/webrtc/webrtc-transform.html. * platform/glib/TestExpectations: * webrtc/script-transform.js: Added. (MockRTCRtpTransformer): (MockRTCRtpTransformer.prototype.start): Canonical link: https://commits.webkit.org/231818@main git-svn-id: https://svn.webkit.org/repository/webkit/trunk@270107 268f45cc-cd09-0410-ab3c-d52691b4dbfc
1 parent 514a713 commit 9877c92

71 files changed

Lines changed: 2265 additions & 341 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

LayoutTests/ChangeLog

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,25 @@
1+
2020-11-20 Youenn Fablet <youenn@apple.com>
2+
3+
Add support for RTCRtpScriptTransform
4+
https://bugs.webkit.org/show_bug.cgi?id=219148
5+
6+
Reviewed by Eric Carlson.
7+
8+
* http/wpt/webrtc/routines.js: Added.
9+
(createConnections):
10+
* http/wpt/webrtc/script-transform.js: Added.
11+
(MockRTCRtpTransformer):
12+
(MockRTCRtpTransformer.prototype.start):
13+
(MockRTCRtpTransformer.prototype.process):
14+
* http/wpt/webrtc/sframe-transform-expected.txt: Added.
15+
* http/wpt/webrtc/sframe-transform.html: Added.
16+
* http/wpt/webrtc/webrtc-transform-expected.txt: Renamed from LayoutTests/webrtc/webrtc-transform-expected.txt.
17+
* http/wpt/webrtc/webrtc-transform.html: Renamed from LayoutTests/webrtc/webrtc-transform.html.
18+
* platform/glib/TestExpectations:
19+
* webrtc/script-transform.js: Added.
20+
(MockRTCRtpTransformer):
21+
(MockRTCRtpTransformer.prototype.start):
22+
123
2020-11-20 Philippe Normand <pnormand@igalia.com>
224

325
[MSE] Infinite loop in sample eviction when duration is NaN
Lines changed: 299 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,299 @@
1+
// Test inspired from https://webrtc.github.io/samples/
2+
var localConnection;
3+
var remoteConnection;
4+
5+
function createConnections(setupLocalConnection, setupRemoteConnection, options = { }) {
6+
localConnection = new RTCPeerConnection();
7+
remoteConnection = new RTCPeerConnection();
8+
remoteConnection.onicecandidate = (event) => { iceCallback2(event, options.filterOutICECandidate) };
9+
10+
localConnection.onicecandidate = (event) => { iceCallback1(event, options.filterOutICECandidate) };
11+
12+
Promise.resolve(setupLocalConnection(localConnection)).then(() => {
13+
return Promise.resolve(setupRemoteConnection(remoteConnection));
14+
}).then(() => {
15+
localConnection.createOffer().then((desc) => gotDescription1(desc, options), onCreateSessionDescriptionError);
16+
});
17+
18+
return [localConnection, remoteConnection]
19+
}
20+
21+
function closeConnections()
22+
{
23+
localConnection.close();
24+
remoteConnection.close();
25+
}
26+
27+
function onCreateSessionDescriptionError(error)
28+
{
29+
assert_unreached();
30+
}
31+
32+
function gotDescription1(desc, options)
33+
{
34+
if (options.observeOffer) {
35+
const result = options.observeOffer(desc);
36+
if (result)
37+
desc = result;
38+
}
39+
40+
localConnection.setLocalDescription(desc);
41+
remoteConnection.setRemoteDescription(desc).then(() => {
42+
remoteConnection.createAnswer().then((desc) => gotDescription2(desc, options), onCreateSessionDescriptionError);
43+
});
44+
}
45+
46+
function gotDescription2(desc, options)
47+
{
48+
if (options.observeAnswer)
49+
options.observeAnswer(desc);
50+
51+
remoteConnection.setLocalDescription(desc);
52+
localConnection.setRemoteDescription(desc);
53+
}
54+
55+
function iceCallback1(event, filterOutICECandidate)
56+
{
57+
if (filterOutICECandidate && filterOutICECandidate(event.candidate))
58+
return;
59+
60+
remoteConnection.addIceCandidate(event.candidate).then(onAddIceCandidateSuccess, onAddIceCandidateError);
61+
}
62+
63+
function iceCallback2(event, filterOutICECandidate)
64+
{
65+
if (filterOutICECandidate && filterOutICECandidate(event.candidate))
66+
return;
67+
68+
localConnection.addIceCandidate(event.candidate).then(onAddIceCandidateSuccess, onAddIceCandidateError);
69+
}
70+
71+
function onAddIceCandidateSuccess()
72+
{
73+
}
74+
75+
function onAddIceCandidateError(error)
76+
{
77+
console.log("addIceCandidate error: " + error)
78+
assert_unreached();
79+
}
80+
81+
async function renegotiate(pc1, pc2)
82+
{
83+
let d = await pc1.createOffer();
84+
await pc1.setLocalDescription(d);
85+
await pc2.setRemoteDescription(d);
86+
d = await pc2.createAnswer();
87+
await pc1.setRemoteDescription(d);
88+
await pc2.setLocalDescription(d);
89+
}
90+
91+
function analyseAudio(stream, duration, context)
92+
{
93+
return new Promise((resolve, reject) => {
94+
var sourceNode = context.createMediaStreamSource(stream);
95+
96+
var analyser = context.createAnalyser();
97+
var gain = context.createGain();
98+
99+
var results = { heardHum: false, heardBip: false, heardBop: false, heardNoise: false };
100+
101+
analyser.fftSize = 2048;
102+
analyser.smoothingTimeConstant = 0;
103+
analyser.minDecibels = -100;
104+
analyser.maxDecibels = 0;
105+
gain.gain.value = 0;
106+
107+
sourceNode.connect(analyser);
108+
analyser.connect(gain);
109+
gain.connect(context.destination);
110+
111+
function analyse() {
112+
var freqDomain = new Uint8Array(analyser.frequencyBinCount);
113+
analyser.getByteFrequencyData(freqDomain);
114+
115+
var hasFrequency = expectedFrequency => {
116+
var bin = Math.floor(expectedFrequency * analyser.fftSize / context.sampleRate);
117+
return bin < freqDomain.length && freqDomain[bin] >= 100;
118+
};
119+
120+
if (!results.heardHum)
121+
results.heardHum = hasFrequency(150);
122+
123+
if (!results.heardBip)
124+
results.heardBip = hasFrequency(1500);
125+
126+
if (!results.heardBop)
127+
results.heardBop = hasFrequency(500);
128+
129+
if (!results.heardNoise)
130+
results.heardNoise = hasFrequency(3000);
131+
132+
if (results.heardHum && results.heardBip && results.heardBop && results.heardNoise)
133+
done();
134+
};
135+
136+
function done() {
137+
clearTimeout(timeout);
138+
clearInterval(interval);
139+
resolve(results);
140+
}
141+
142+
var timeout = setTimeout(done, 3 * duration);
143+
var interval = setInterval(analyse, duration / 30);
144+
analyse();
145+
});
146+
}
147+
148+
function waitFor(duration)
149+
{
150+
return new Promise((resolve) => setTimeout(resolve, duration));
151+
}
152+
153+
function waitForVideoSize(video, width, height, count)
154+
{
155+
if (video.videoWidth === width && video.videoHeight === height)
156+
return Promise.resolve("video has expected size");
157+
158+
if (count === undefined)
159+
count = 0;
160+
if (++count > 20)
161+
return Promise.reject("waitForVideoSize timed out, expected " + width + "x"+ height + " but got " + video.videoWidth + "x" + video.videoHeight);
162+
163+
return waitFor(100).then(() => {
164+
return waitForVideoSize(video, width, height, count);
165+
});
166+
}
167+
168+
async function doHumAnalysis(stream, expected)
169+
{
170+
var context = new AudioContext();
171+
for (var cptr = 0; cptr < 20; cptr++) {
172+
var results = await analyseAudio(stream, 200, context);
173+
if (results.heardHum === expected)
174+
return true;
175+
await waitFor(50);
176+
}
177+
await context.close();
178+
return false;
179+
}
180+
181+
function isVideoBlack(canvas, video, startX, startY, grabbedWidth, grabbedHeight)
182+
{
183+
canvas.width = video.videoWidth;
184+
canvas.height = video.videoHeight;
185+
if (!grabbedHeight) {
186+
startX = 10;
187+
startY = 10;
188+
grabbedWidth = canvas.width - 20;
189+
grabbedHeight = canvas.height - 20;
190+
}
191+
192+
canvas.getContext('2d').drawImage(video, 0, 0, canvas.width, canvas.height);
193+
194+
imageData = canvas.getContext('2d').getImageData(startX, startY, grabbedWidth, grabbedHeight);
195+
data = imageData.data;
196+
for (var cptr = 0; cptr < grabbedWidth * grabbedHeight; ++cptr) {
197+
// Approximatively black pixels.
198+
if (data[4 * cptr] > 30 || data[4 * cptr + 1] > 30 || data[4 * cptr + 2] > 30)
199+
return false;
200+
}
201+
return true;
202+
}
203+
204+
async function checkVideoBlack(expected, canvas, video, errorMessage, counter)
205+
{
206+
if (isVideoBlack(canvas, video) === expected)
207+
return Promise.resolve();
208+
209+
if (counter === undefined)
210+
counter = 0;
211+
if (counter > 400) {
212+
if (!errorMessage)
213+
errorMessage = "checkVideoBlack timed out expecting " + expected;
214+
return Promise.reject(errorMessage);
215+
}
216+
217+
await waitFor(50);
218+
return checkVideoBlack(expected, canvas, video, errorMessage, ++counter);
219+
}
220+
221+
function setCodec(sdp, codec)
222+
{
223+
return sdp.split('\r\n').filter(line => {
224+
return line.indexOf('a=fmtp') === -1 && line.indexOf('a=rtcp-fb') === -1 && (line.indexOf('a=rtpmap') === -1 || line.indexOf(codec) !== -1);
225+
}).join('\r\n');
226+
}
227+
228+
async function getTypedStats(connection, type)
229+
{
230+
const report = await connection.getStats();
231+
var stats;
232+
report.forEach((statItem) => {
233+
if (statItem.type === type)
234+
stats = statItem;
235+
});
236+
return stats;
237+
}
238+
239+
function getReceivedTrackStats(connection)
240+
{
241+
return connection.getStats().then((report) => {
242+
var stats;
243+
report.forEach((statItem) => {
244+
if (statItem.type === "track") {
245+
stats = statItem;
246+
}
247+
});
248+
return stats;
249+
});
250+
}
251+
252+
async function computeFrameRate(stream, video)
253+
{
254+
if (window.internals) {
255+
internals.observeMediaStreamTrack(stream.getVideoTracks()[0]);
256+
await new Promise(resolve => setTimeout(resolve, 1000));
257+
return internals.trackVideoSampleCount;
258+
}
259+
260+
let connection;
261+
video.srcObject = await new Promise((resolve, reject) => {
262+
createConnections((firstConnection) => {
263+
firstConnection.addTrack(stream.getVideoTracks()[0], stream);
264+
}, (secondConnection) => {
265+
connection = secondConnection;
266+
secondConnection.ontrack = (trackEvent) => {
267+
resolve(trackEvent.streams[0]);
268+
};
269+
});
270+
setTimeout(() => reject("Test timed out"), 5000);
271+
});
272+
273+
await video.play();
274+
275+
const stats1 = await getReceivedTrackStats(connection);
276+
await new Promise(resolve => setTimeout(resolve, 1000));
277+
const stats2 = await getReceivedTrackStats(connection);
278+
return (stats2.framesReceived - stats1.framesReceived) * 1000 / (stats2.timestamp - stats1.timestamp);
279+
}
280+
281+
function setH264BaselineCodec(sdp)
282+
{
283+
const lines = sdp.split('\r\n');
284+
const h264Lines = lines.filter(line => line.indexOf("a=fmtp") === 0 && line.indexOf("42e01f") !== -1);
285+
const baselineNumber = h264Lines[0].substring(6).split(' ')[0];
286+
return lines.filter(line => {
287+
return (line.indexOf('a=fmtp') === -1 && line.indexOf('a=rtcp-fb') === -1 && line.indexOf('a=rtpmap') === -1) || line.indexOf(baselineNumber) !== -1;
288+
}).join('\r\n');
289+
}
290+
291+
function setH264HighCodec(sdp)
292+
{
293+
const lines = sdp.split('\r\n');
294+
const h264Lines = lines.filter(line => line.indexOf("a=fmtp") === 0 && line.indexOf("640c1f") !== -1);
295+
const baselineNumber = h264Lines[0].substring(6).split(' ')[0];
296+
return lines.filter(line => {
297+
return (line.indexOf('a=fmtp') === -1 && line.indexOf('a=rtcp-fb') === -1 && line.indexOf('a=rtpmap') === -1) || line.indexOf(baselineNumber) !== -1;
298+
}).join('\r\n');
299+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
class MockRTCRtpTransformer extends RTCRtpScriptTransformer {
2+
constructor() {
3+
super();
4+
this.port.onmessage = (event) => this.port.postMessage(event.data);
5+
}
6+
start(readableStream, writableStream)
7+
{
8+
self.postMessage("started");
9+
this.reader = readableStream.getReader();
10+
this.writer = writableStream.getWriter();
11+
this.process();
12+
}
13+
14+
process()
15+
{
16+
this.reader.read().then(chunk => {
17+
if (chunk.done)
18+
return;
19+
if (chunk.value instanceof RTCEncodedVideoFrame)
20+
self.postMessage("video chunk");
21+
else if (chunk.value instanceof RTCEncodedAudioFrame)
22+
self.postMessage("audio chunk");
23+
this.writer.write(chunk.value);
24+
this.process();
25+
});
26+
}
27+
};
28+
29+
registerRTCRtpScriptTransformer("MockRTCRtpTransform", MockRTCRtpTransformer);
30+
self.postMessage("registered");
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
2+
PASS Cannot reuse attached transforms
3+
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<!doctype html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8">
5+
<script src="/resources/testharness.js"></script>
6+
<script src="/resources/testharnessreport.js"></script>
7+
</head>
8+
<body>
9+
<script>
10+
11+
promise_test(async (test) => {
12+
const pc = new RTCPeerConnection();
13+
const senderTransform = new RTCRtpSFrameTransform();
14+
const receiverTransform = new RTCRtpSFrameTransform();
15+
const sender1 = pc.addTransceiver('audio').sender;
16+
const sender2 = pc.addTransceiver('video').sender;
17+
const receiver1 = pc.getReceivers()[0];
18+
const receiver2 = pc.getReceivers()[1];
19+
20+
sender1.transform = senderTransform;
21+
receiver1.transform = receiverTransform;
22+
assert_throws_dom("InvalidStateError", () => sender2.transform = senderTransform);
23+
assert_throws_dom("InvalidStateError", () => receiver2.transform = receiverTransform);
24+
25+
sender1.transform = senderTransform;
26+
receiver1.transform = receiverTransform;
27+
28+
sender1.transform = null;
29+
receiver1.transform = null;
30+
}, "Cannot reuse attached transforms");
31+
</script>
32+
</body>
33+
</html>

0 commit comments

Comments
 (0)