diff --git a/packages/flutter_webrtc/CHANGELOG.md b/packages/flutter_webrtc/CHANGELOG.md index 7b2d34763..2c84c7d7c 100644 --- a/packages/flutter_webrtc/CHANGELOG.md +++ b/packages/flutter_webrtc/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.2.3 + +* Fix `RTCPeerConnection.close()` to be idempotent; calling it more than once no longer throws an error. + ## 0.2.2 * Support x86_64 diff --git a/packages/flutter_webrtc/README.md b/packages/flutter_webrtc/README.md index 06d6fa665..ad86713ce 100644 --- a/packages/flutter_webrtc/README.md +++ b/packages/flutter_webrtc/README.md @@ -41,7 +41,7 @@ For other Tizen devices : ```yaml dependencies: flutter_webrtc: ^1.4.1 - flutter_webrtc_tizen: ^0.2.2 + flutter_webrtc_tizen: ^0.2.3 ``` ## Functionality diff --git a/packages/flutter_webrtc/example/flutter_webrtc_example/integration_test/flutter_webrtc_test.dart b/packages/flutter_webrtc/example/flutter_webrtc_example/integration_test/flutter_webrtc_test.dart new file mode 100644 index 000000000..429f54a3c --- /dev/null +++ b/packages/flutter_webrtc/example/flutter_webrtc_example/integration_test/flutter_webrtc_test.dart @@ -0,0 +1,224 @@ +// Copyright 2024 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_webrtc/flutter_webrtc.dart'; +import 'package:integration_test/integration_test.dart'; + +const Map _kConfig = { + 'iceServers': >[], +}; + +Future _negotiate( + RTCPeerConnection pc1, + RTCPeerConnection pc2, +) async { + pc1.onIceCandidate = (c) => pc2.addCandidate(c); + pc2.onIceCandidate = (c) => pc1.addCandidate(c); + + final offer = await pc1.createOffer(); + await pc1.setLocalDescription(offer); + await pc2.setRemoteDescription(offer); + final answer = await pc2.createAnswer(); + await pc2.setLocalDescription(answer); + await pc1.setRemoteDescription(answer); +} + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('RTCPeerConnection', () { + testWidgets('creates with empty ice servers', (WidgetTester tester) async { + final pc = await createPeerConnection(_kConfig); + expect(pc, isNotNull); + await pc.close(); + }, timeout: const Timeout(Duration(seconds: 10))); + + testWidgets('createOffer returns valid SDP', (WidgetTester tester) async { + final pc = await createPeerConnection(_kConfig); + final offer = await pc.createOffer(); + expect(offer.type, equals('offer')); + expect(offer.sdp, isNotEmpty); + await pc.close(); + }, timeout: const Timeout(Duration(seconds: 10))); + + testWidgets('setLocalDescription succeeds', (WidgetTester tester) async { + final pc = await createPeerConnection(_kConfig); + final offer = await pc.createOffer(); + await expectLater(pc.setLocalDescription(offer), completes); + await pc.close(); + }, timeout: const Timeout(Duration(seconds: 10))); + + testWidgets('createAnswer returns valid SDP', (WidgetTester tester) async { + final pc1 = await createPeerConnection(_kConfig); + final pc2 = await createPeerConnection(_kConfig); + final offer = await pc1.createOffer(); + await pc2.setRemoteDescription(offer); + final answer = await pc2.createAnswer(); + expect(answer.type, equals('answer')); + expect(answer.sdp, isNotEmpty); + await pc1.close(); + await pc2.close(); + }, timeout: const Timeout(Duration(seconds: 10))); + + testWidgets('close is idempotent', (WidgetTester tester) async { + final pc = await createPeerConnection(_kConfig); + await pc.close(); + await expectLater(pc.close(), completes); + }, timeout: const Timeout(Duration(seconds: 10))); + }); + + group('RTCDataChannel', () { + testWidgets('createDataChannel returns channel with correct label', + (WidgetTester tester) async { + final pc = await createPeerConnection(_kConfig); + final dc = await pc.createDataChannel('test-label', RTCDataChannelInit()); + expect(dc.label, equals('test-label')); + await dc.close(); + await pc.close(); + }, timeout: const Timeout(Duration(seconds: 10))); + + testWidgets('loopback text message exchange', (WidgetTester tester) async { + final pc1 = await createPeerConnection(_kConfig); + final pc2 = await createPeerConnection(_kConfig); + + final dc2Completer = Completer(); + pc2.onDataChannel = (channel) { + if (!dc2Completer.isCompleted) dc2Completer.complete(channel); + }; + + final dc1 = await pc1.createDataChannel( + 'loopback', + RTCDataChannelInit()..ordered = true, + ); + + await _negotiate(pc1, pc2); + + final dc2 = + await dc2Completer.future.timeout(const Duration(seconds: 10)); + + final dc1Open = Completer(); + final dc2Open = Completer(); + if (dc1.state == RTCDataChannelState.RTCDataChannelOpen) { + dc1Open.complete(); + } else { + dc1.onDataChannelState = (s) { + if (s == RTCDataChannelState.RTCDataChannelOpen && + !dc1Open.isCompleted) { + dc1Open.complete(); + } + }; + } + if (dc2.state == RTCDataChannelState.RTCDataChannelOpen) { + dc2Open.complete(); + } else { + dc2.onDataChannelState = (s) { + if (s == RTCDataChannelState.RTCDataChannelOpen && + !dc2Open.isCompleted) { + dc2Open.complete(); + } + }; + } + + await Future.wait([ + dc1Open.future.timeout(const Duration(seconds: 10)), + dc2Open.future.timeout(const Duration(seconds: 10)), + ]); + + final dc2Received = Completer(); + dc2.onMessage = (msg) { + if (!dc2Received.isCompleted) dc2Received.complete(msg.text); + }; + await dc1.send(RTCDataChannelMessage('hello from dc1')); + expect( + await dc2Received.future.timeout(const Duration(seconds: 5)), + equals('hello from dc1'), + ); + + final dc1Received = Completer(); + dc1.onMessage = (msg) { + if (!dc1Received.isCompleted) dc1Received.complete(msg.text); + }; + await dc2.send(RTCDataChannelMessage('hello from dc2')); + expect( + await dc1Received.future.timeout(const Duration(seconds: 5)), + equals('hello from dc2'), + ); + + await dc1.close(); + await dc2.close(); + await pc1.close(); + await pc2.close(); + }, timeout: const Timeout(Duration(seconds: 60))); + + testWidgets('loopback binary message exchange', + (WidgetTester tester) async { + final pc1 = await createPeerConnection(_kConfig); + final pc2 = await createPeerConnection(_kConfig); + + final dc2Completer = Completer(); + pc2.onDataChannel = (channel) { + if (!dc2Completer.isCompleted) dc2Completer.complete(channel); + }; + + final dc1 = await pc1.createDataChannel( + 'binary', + RTCDataChannelInit()..ordered = true, + ); + + await _negotiate(pc1, pc2); + + final dc2 = + await dc2Completer.future.timeout(const Duration(seconds: 10)); + + final dc1Open = Completer(); + final dc2Open = Completer(); + if (dc1.state == RTCDataChannelState.RTCDataChannelOpen) { + dc1Open.complete(); + } else { + dc1.onDataChannelState = (s) { + if (s == RTCDataChannelState.RTCDataChannelOpen && + !dc1Open.isCompleted) { + dc1Open.complete(); + } + }; + } + if (dc2.state == RTCDataChannelState.RTCDataChannelOpen) { + dc2Open.complete(); + } else { + dc2.onDataChannelState = (s) { + if (s == RTCDataChannelState.RTCDataChannelOpen && + !dc2Open.isCompleted) { + dc2Open.complete(); + } + }; + } + + await Future.wait([ + dc1Open.future.timeout(const Duration(seconds: 10)), + dc2Open.future.timeout(const Duration(seconds: 10)), + ]); + + final payload = Uint8List.fromList([1, 2, 3, 4, 5]); + + final dc2Received = Completer(); + dc2.onMessage = (msg) { + if (!dc2Received.isCompleted) dc2Received.complete(msg.binary); + }; + await dc1.send(RTCDataChannelMessage.fromBinary(payload)); + expect( + await dc2Received.future.timeout(const Duration(seconds: 5)), + equals(payload), + ); + + await dc1.close(); + await dc2.close(); + await pc1.close(); + await pc2.close(); + }, timeout: const Timeout(Duration(seconds: 60))); + }); +} diff --git a/packages/flutter_webrtc/example/flutter_webrtc_example/pubspec.yaml b/packages/flutter_webrtc/example/flutter_webrtc_example/pubspec.yaml index d0ca5cc62..39c78381f 100644 --- a/packages/flutter_webrtc/example/flutter_webrtc_example/pubspec.yaml +++ b/packages/flutter_webrtc/example/flutter_webrtc_example/pubspec.yaml @@ -23,8 +23,14 @@ dependencies: web: ^1.0.0 dev_dependencies: + flutter_driver: + sdk: flutter flutter_test: sdk: flutter + integration_test: + sdk: flutter + integration_test_tizen: + path: ../../../integration_test/ pedantic: ^1.11.0 flutter: diff --git a/packages/flutter_webrtc/example/flutter_webrtc_example/test_driver/integration_test.dart b/packages/flutter_webrtc/example/flutter_webrtc_example/test_driver/integration_test.dart new file mode 100644 index 000000000..b38629cca --- /dev/null +++ b/packages/flutter_webrtc/example/flutter_webrtc_example/test_driver/integration_test.dart @@ -0,0 +1,3 @@ +import 'package:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver(); diff --git a/packages/flutter_webrtc/pubspec.yaml b/packages/flutter_webrtc/pubspec.yaml index eac495d2e..81e6a5af7 100644 --- a/packages/flutter_webrtc/pubspec.yaml +++ b/packages/flutter_webrtc/pubspec.yaml @@ -2,7 +2,7 @@ name: flutter_webrtc_tizen homepage: https://github.com/flutter-tizen/plugins description: Flutter WebRTC plugin for Tizen, based on GoogleWebRTC. repository: https://github.com/flutter-tizen/plugins/tree/master/packages/flutter_webrtc -version: 0.2.2 +version: 0.2.3 environment: sdk: ">=3.3.0 <4.0.0" diff --git a/packages/flutter_webrtc/tizen/src/flutter_webrtc.cc b/packages/flutter_webrtc/tizen/src/flutter_webrtc.cc index 6eeac0737..c8b1d3919 100644 --- a/packages/flutter_webrtc/tizen/src/flutter_webrtc.cc +++ b/packages/flutter_webrtc/tizen/src/flutter_webrtc.cc @@ -465,8 +465,7 @@ void FlutterWebRTC::HandleMethodCall( const std::string peerConnectionId = findString(params, "peerConnectionId"); RTCPeerConnection* pc = PeerConnectionForId(peerConnectionId); if (pc == nullptr) { - result->Error("peerConnectionCloseFailed", - "peerConnectionClose() peerConnection is null"); + result->Success(); return; } RTCPeerConnectionClose(pc, peerConnectionId, std::move(result));