Commit 18142899 authored by 张宏's avatar 张宏

webrtc接入

parent 35fee5c6
This diff is collapsed.
import 'package:equatable/equatable.dart';
/// 用途:WebRTC 信令地址接口响应模型
/// 涉及页面:首页监护舱 - 监控画面
class WebrtcAddressBO extends Equatable {
/// 错误码
final int errorCode;
/// 错误信息
final String? errorMessage;
/// 错误提示文案
final String? errorTips;
/// SDP 交换 URL 列表(主)
final List<String> urlList;
/// SDP 交换 URL 列表(备用)
final List<String> backupUrlList;
const WebrtcAddressBO({
required this.errorCode,
this.errorMessage,
this.errorTips,
required this.urlList,
required this.backupUrlList,
});
factory WebrtcAddressBO.fromJson(Map<String, dynamic> json) {
final data = json['data'] as Map<String, dynamic>?;
return WebrtcAddressBO(
errorCode: json['error_code'] as int? ?? 0,
errorMessage: json['error_message'] as String?,
errorTips: json['error_tips'] as String?,
urlList: _parseStringList(data?['list']),
backupUrlList: _parseStringList(data?['backup_list']),
);
}
static List<String> _parseStringList(dynamic value) {
if (value is List) {
return value.map((e) => e.toString()).toList();
}
return [];
}
@override
List<Object?> get props => [errorCode, urlList, backupUrlList];
}
import '../utils/http/response_model.dart';
import '../utils/http/dio_request.dart';
import '../models/bo/webrtc_bo.dart';
class WebrtcRepository {
/// 获取 WebRTC SDP 信令交换地址
///
/// [deviceNo] 设备摄像头序列号(cameraSn)
/// [password] 设备 WiFi 密码
Future<ResponseModel<WebrtcAddressBO>> getAddress({
required String deviceNo,
required String password,
}) {
return DioRequest.instance.get<WebrtcAddressBO>(
'/veepai/cloud/webrtc/address',
queryParameters: {'cameraSn': deviceNo, 'password': password},
fromJsonT: (data) =>
WebrtcAddressBO.fromJson(data as Map<String, dynamic>),
);
}
}
This diff is collapsed.
......@@ -37,5 +37,5 @@ class Constants {
// ==================== Mock 数据开关 ====================
/// 接口未提供时使用本地 mock 数据。
/// 设为 false 并调整 models 的 fromJson 即可切回真实接口。
static const bool useMockData = true;
static const bool useMockData = false;
}
import 'package:bloc/bloc.dart';
import 'dart:async';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:laki_icu_app/services/monitoring_service.dart';
import 'package:laki_icu_app/services/webrtc_service.dart';
import 'package:laki_icu_app/utils/constants.dart';
import 'home_index_state.dart';
/// 设备凭据 —— Mock 阶段使用占位值,后续接口接入后替换
/// TODO: 从舱详情接口获取真实的 cameraSn 和 wifiPwd
const _mockDeviceNo = 'MOCK_DEVICE_SN';
const _mockDevicePassword = 'MOCK_WIFI_PWD';
class HomeIndexCubit extends Cubit<HomeIndexState> {
final MonitoringService _monitoringService;
final WebrtcService _webrtcService;
StreamSubscription<WebrtcConnectionState>? _webrtcStateSub;
HomeIndexCubit()
: _monitoringService = MonitoringService(),
_webrtcService = WebrtcService(),
super(const HomeIndexState()) {
_listenWebrtcState();
loadData();
}
/// 监听 WebRTC 连接状态变化并同步到 State
void _listenWebrtcState() {
_webrtcStateSub = _webrtcService.connectionStateStream.listen((state) {
if (isClosed) return;
emit(this.state.copyWith(videoConnectionState: state));
});
}
Future<void> loadData() async {
emit(state.copyWith(isLoading: true, error: null));
try {
......@@ -33,6 +55,9 @@ class HomeIndexCubit extends Cubit<HomeIndexState> {
menuItems: menuItems,
error: null,
));
// 数据加载完成后初始化 WebRTC 视频连接
_initWebrtc();
} catch (e) {
if (isClosed) return;
emit(state.copyWith(
......@@ -43,6 +68,24 @@ class HomeIndexCubit extends Cubit<HomeIndexState> {
}
}
/// 初始化 WebRTC 视频连接
Future<void> _initWebrtc() async {
if (Constants.useMockData) {
// Mock 模式:跳过真实连接,保持断开状态
return;
}
await _webrtcService.connect(
deviceNo: _mockDeviceNo,
password: _mockDevicePassword,
);
}
/// 手动重试视频连接
Future<void> retryWebrtc() async {
await _initWebrtc();
}
Future<void> refreshData() async {
try {
final metrics = await _monitoringService.getMetrics();
......@@ -70,4 +113,14 @@ class HomeIndexCubit extends Cubit<HomeIndexState> {
));
}
}
/// 获取渲染器供 UI 层使用
WebrtcService get webrtcService => _webrtcService;
@override
Future<void> close() {
_webrtcStateSub?.cancel();
_webrtcService.dispose();
return super.close();
}
}
import 'package:equatable/equatable.dart';
import 'package:laki_icu_app/models/bo/monitoring_bo.dart';
import 'package:laki_icu_app/services/webrtc_service.dart';
enum HomeIndexStatus {
initial,
......@@ -17,6 +18,9 @@ class HomeIndexState extends Equatable {
final List<AlertInfoBO> alerts;
final List<MonitoringMenuItemBO> menuItems;
/// WebRTC 视频连接状态
final WebrtcConnectionState videoConnectionState;
const HomeIndexState({
this.status = HomeIndexStatus.initial,
this.isLoading = false,
......@@ -25,6 +29,7 @@ class HomeIndexState extends Equatable {
this.patientInfo,
this.alerts = const [],
this.menuItems = const [],
this.videoConnectionState = WebrtcConnectionState.disconnected,
});
HomeIndexState copyWith({
......@@ -35,6 +40,7 @@ class HomeIndexState extends Equatable {
PatientInfoBO? patientInfo,
List<AlertInfoBO>? alerts,
List<MonitoringMenuItemBO>? menuItems,
WebrtcConnectionState? videoConnectionState,
}) {
return HomeIndexState(
status: status ?? this.status,
......@@ -44,6 +50,8 @@ class HomeIndexState extends Equatable {
patientInfo: patientInfo ?? this.patientInfo,
alerts: alerts ?? this.alerts,
menuItems: menuItems ?? this.menuItems,
videoConnectionState:
videoConnectionState ?? this.videoConnectionState,
);
}
......@@ -56,5 +64,6 @@ class HomeIndexState extends Equatable {
patientInfo,
alerts,
menuItems,
videoConnectionState,
];
}
......@@ -8,9 +8,9 @@ import 'cubit/home_index_cubit.dart';
import 'cubit/home_index_state.dart';
import 'widgets/alert_info_card.dart';
import 'widgets/metric_card.dart';
import 'widgets/monitoring_screen.dart';
import 'widgets/patient_info_card.dart';
import 'widgets/sidebar_menu_item.dart';
import 'widgets/webrtc_video_widget.dart';
@RoutePage()
class HomeIndexView extends StatelessWidget {
......@@ -175,13 +175,11 @@ class _HomeIndexContentState extends State<HomeIndexContent> {
// 监控画面模块(2/3宽度、100%高度)
Expanded(
flex: 2,
child: Text("1234")
// MonitoringScreen(
// onRefresh: () => _homeIndexCubit.refreshData(),
// onSnapshot: () {},
// onRecord: () {},
// ),
child: WebrtcVideoWidget(
renderer: _homeIndexCubit.webrtcService.renderer,
connectionState: state.videoConnectionState,
onRetry: () => _homeIndexCubit.retryWebrtc(),
),
),
SizedBox(width: 24.w),
// 右侧(1/3宽度):患者信息卡片 + 告警信息卡片
......
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_webrtc/flutter_webrtc.dart';
import '../../../../services/webrtc_service.dart';
/// WebRTC 实时视频播放组件
///
/// 职责:
/// - 通过 [RTCVideoView] 渲染远程摄像头实时画面
/// - 依据 [connectionState] 显示对应的连接状态指示器
/// - 连接失败或未连接时显示重试占位
///
/// 涉及页面:首页监护舱 - 监控画面
class WebrtcVideoWidget extends StatelessWidget {
/// 视频渲染器(来自 [WebrtcService])
final RTCVideoRenderer renderer;
/// 当前连接状态
final WebrtcConnectionState connectionState;
/// 连接失败时点击重试回调
final VoidCallback? onRetry;
const WebrtcVideoWidget({
super.key,
required this.renderer,
required this.connectionState,
this.onRetry,
});
@override
Widget build(BuildContext context) {
return ClipRRect(
borderRadius: BorderRadius.circular(20.r),
child: Container(
decoration: BoxDecoration(
gradient: const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0xCC63A0FF), Color(0xCC0024C4)],
),
border: Border.all(color: Colors.white24, width: 1),
),
child: Stack(
fit: StackFit.expand,
children: [
// 视频画面层
_buildVideoLayer(),
// 状态指示层
_buildStatusOverlay(),
],
),
),
);
}
/// 视频画面
Widget _buildVideoLayer() {
if (connectionState == WebrtcConnectionState.connected) {
return RTCVideoView(
renderer,
objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitContain,
);
}
// 非连接状态显示占位
return _buildPlaceholder();
}
/// 状态遮罩层
Widget _buildStatusOverlay() {
switch (connectionState) {
case WebrtcConnectionState.connecting:
case WebrtcConnectionState.reconnecting:
return _buildLoadingOverlay();
case WebrtcConnectionState.failed:
return _buildErrorOverlay();
default:
return const SizedBox.shrink();
}
}
/// 加载中遮罩
Widget _buildLoadingOverlay() {
final isReconnecting =
connectionState == WebrtcConnectionState.reconnecting;
return Container(
color: Colors.black26,
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const CircularProgressIndicator(color: Colors.white70),
SizedBox(height: 16.h),
Text(
isReconnecting ? '正在重新连接...' : '正在连接摄像头...',
style: TextStyle(
fontSize: 18.sp,
color: Colors.white70,
),
),
],
),
),
);
}
/// 错误遮罩
Widget _buildErrorOverlay() {
return Container(
color: Colors.black38,
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.error_outline, size: 48.w, color: Colors.white54),
SizedBox(height: 16.h),
Text(
'视频连接失败',
style: TextStyle(
fontSize: 20.sp,
color: Colors.white70,
fontWeight: FontWeight.w500,
),
),
SizedBox(height: 24.h),
ElevatedButton.icon(
onPressed: onRetry,
icon: const Icon(Icons.refresh),
label: const Text('重新连接'),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF3A87FF),
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20.r),
),
),
),
],
),
),
);
}
/// 未连接时的占位
Widget _buildPlaceholder() {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.videocam_off,
size: 48.w,
color: Colors.white38,
),
SizedBox(height: 16.h),
Text(
'暂无视频画面',
style: TextStyle(
fontSize: 20.sp,
color: Colors.white54,
fontWeight: FontWeight.w500,
),
),
],
),
);
}
}
# Contributing
We love contributions from everyone, whether it's raising an issue, reporting a bug, adding a feature, or helping improve a document.
Maintaining the flutter-webrtc plugin for all platforms is not an easy task, so everything you do is support for the project.
# Pull Request
We recommend that you create a related issue before PR so that others can find the answers they want in the issues.
# End to End Encryption
E2EE is an AES-GCM encryption interface injected before sending the packaged RTP packet and after receiving the RTP packet, ensuring that the data is not eavesdropped when passing through SFU or any public transmission network. It coexists with DTLS-SRTP as two layers of encryption. You can control the key, ratchet and other operations of FrameCryptor yourself to ensure that no third party will monitor your tracks.
## Process of enabling E2EE
1, Prepare the key provider
`ratchetSalt` is used to add to the mixture when ratcheting or deriving AES passwords
`aesKey` aesKey is the plaintext password you entered, which will be used to derive the actual password
```dart
final aesKey = 'you-private-key-here'.codeUnits;
final ratchetSalt = 'flutter-webrtc-ratchet-salt';
var keyProviderOptions = KeyProviderOptions(
sharedKey: true,
ratchetSalt: Uint8List.fromList(ratchetSalt.codeUnits),
ratchetWindowSize: 16,
failureTolerance: -1,
);
var keyProvider = await frameCyrptorFactory.createDefaultKeyProvider(keyProviderOptions);
/// set shared key for all track, default index is 0
/// also you can set multiple keys by different indexes
await keyProvider.setSharedKey(key: aesKey);
```
2, create PeerConnectioin
when you use E2EE on the web, please add `encodedInsertableStreams`,
``` dart
var pc = await createPeerConnection( {
'encodedInsertableStreams': true,
});
```
3, Enable FrameCryptor for RTPSender.
```dart
var stream = await navigator.mediaDevices
.getUserMedia({'audio': true, 'video': false });
var audioTrack = stream.getAudioTracks();
var sender = await pc.addTrack(audioTrack, stream);
var trackId = audioTrack?.id;
var id = 'audio_' + trackId! + '_sender';
var frameCyrptor =
await frameCyrptorFactory.createFrameCryptorForRtpSender(
participantId: id,
sender: sender,
algorithm: Algorithm.kAesGcm,
keyProvider: keyProvider!);
/// print framecyrptor state
frameCyrptor.onFrameCryptorStateChanged = (participantId, state) =>
print('EN onFrameCryptorStateChanged $participantId $state');
/// set currently shared key index
await frameCyrptor.setKeyIndex(0);
/// enable encryption now.
await frameCyrptor.setEnabled(true);
```
4, Enable FrameCryptor for RTPReceiver
```dart
pc.onTrack((RTCTrackEvent event) async {
var receiver = event.receiver;
var trackId = event.track?.id;
var id = event.track.kind + '_' + trackId! + '_receiver';
var frameCyrptor =
await frameCyrptorFactory.createFrameCryptorForRtpReceiver(
participantId: id,
receiver: receiver,
algorithm: Algorithm.kAesGcm,
keyProvider: keyProvider);
frameCyrptor.onFrameCryptorStateChanged = (participantId, state) =>
print('DE onFrameCryptorStateChanged $participantId $state');
/// set currently shared key index
await frameCyrptor.setKeyIndex(0);
/// enable encryption now.
await frameCyrptor.setEnabled(true);
});
```
MIT License
Copyright (c) 2018 湖北捷智云技术有限公司
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
###################################################################################
The following modifications follow Apache License 2.0 from shiguredo.
SimulcastVideoEncoderFactoryWrapper.kt
Apache License 2.0
Copyright 2017, Lyo Kato <lyo.kato at gmail.com> (Original Author)
Copyright 2017-2021, Shiguredo Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
#####################################################################################
react-native-webrtc
https://github.com/react-native-webrtc/react-native-webrtc
The MIT License (MIT)
Copyright (c) 2015 Howard Yang
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
#####################################################################################
\ No newline at end of file
# Flutter-WebRTC
[![Financial Contributors on Open Collective](https://opencollective.com/flutter-webrtc/all/badge.svg?label=financial+contributors)](https://opencollective.com/flutter-webrtc) [![pub package](https://img.shields.io/pub/v/flutter_webrtc.svg)](https://pub.dartlang.org/packages/flutter_webrtc) [![Gitter](https://badges.gitter.im/flutter-webrtc/Lobby.svg)](https://gitter.im/flutter-webrtc/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [![slack](https://img.shields.io/badge/join-us%20on%20slack-gray.svg?longCache=true&logo=slack&colorB=brightgreen)](https://join.slack.com/t/flutterwebrtc/shared_invite/zt-q83o7y1s-FExGLWEvtkPKM8ku_F8cEQ)
WebRTC plugin for Flutter Mobile/Desktop/Web
</br>
<p align="center">
<strong>Sponsored with 💖 &nbsp by</strong><br />
<a href="https://getstream.io/chat/flutter/tutorial/?utm_source=https://github.com/flutter-webrtc/flutter-webrtc&utm_medium=github&utm_content=developer&utm_term=flutter" target="_blank">
<img src="assets/sponsors/stream-logo.png" alt="Stream Chat" style="margin: 8px; width: 350px" />
</a>
<br />
Enterprise Grade APIs for Feeds, Chat, & Video. <a href="https://getstream.io/video/docs/flutter/?utm_source=https://github.com/flutter-webrtc/flutter-webrtc&utm_medium=sponsorship&utm_content=&utm_campaign=webrtcFlutterRepo_July2023_video_klmh22" target="_blank">Try the Flutter Video tutorial</a> 💬
</p>
</br>
<p align="center">
<a href="https://livekit.io/?utm_source=opencollective&utm_medium=github&utm_campaign=flutter-webrtc" target="_blank">
<img src="https://avatars.githubusercontent.com/u/69438833?s=200&v=4" alt="LiveKit" style="margin: 8px; width: 100px" />
</a>
<br />
<a href="https://livekit.io/?utm_source=opencollective&utm_medium=github&utm_campaign=flutter-webrtc" target="_blank">LiveKit</a> - Open source WebRTC and realtime AI infrastructure
<p>
## Functionality
| Feature | Android | iOS | [Web](https://flutter.dev/web) | macOS | Windows | Linux | [Embedded](https://github.com/sony/flutter-elinux) | [Fuchsia](https://fuchsia.dev/) |
| :-------------: | :-------------:| :-----: | :-----: | :-----: | :-----: | :-----: | :-----: | :-----: |
| Audio/Video | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | |
| Data Channel | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | |
| Screen Capture | :heavy_check_mark: | [:heavy_check_mark:(*)](https://github.com/flutter-webrtc/flutter-webrtc/wiki/iOS-Screen-Sharing) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | |
| Unified-Plan | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | |
| Simulcast | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | |
| MediaRecorder | :warning: | :warning: | :heavy_check_mark: | | | | | |
| End to End Encryption | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | |
| Insertable Streams | | | | | | | | |
Additional platform/OS support from the other community
- flutter-tizen: <https://github.com/flutter-tizen/plugins/tree/master/packages/flutter_webrtc>
- flutter-elinux(WIP): <https://github.com/sony/flutter-elinux-plugins/issues/7>
Add `flutter_webrtc` as a [dependency in your pubspec.yaml file](https://flutter.io/using-packages/).
### iOS
Add the following entry to your _Info.plist_ file, located in `<project root>/ios/Runner/Info.plist`:
```xml
<key>NSCameraUsageDescription</key>
<string>$(PRODUCT_NAME) Camera Usage!</string>
<key>NSMicrophoneUsageDescription</key>
<string>$(PRODUCT_NAME) Microphone Usage!</string>
```
This entry allows your app to access camera and microphone.
### Note for iOS
The WebRTC.xframework compiled after the m104 release no longer supports iOS arm devices, so need to add the `config.build_settings['ONLY_ACTIVE_ARCH'] = 'YES'` to your ios/Podfile in your project
ios/Podfile
```ruby
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
target.build_configurations.each do |config|
# Workaround for https://github.com/flutter/flutter/issues/64502
config.build_settings['ONLY_ACTIVE_ARCH'] = 'YES' # <= this line
end
end
end
```
### Android
Ensure the following permission is present in your Android Manifest file, located in `<project root>/android/app/src/main/AndroidManifest.xml`:
```xml
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
```
If you need to use a Bluetooth device, please add:
```xml
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
```
The Flutter project template adds it, so it may already be there.
Also you will need to set your build settings to Java 8, because official WebRTC jar now uses static methods in `EglBase` interface. Just add this to your app level `build.gradle`:
```groovy
android {
//...
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
```
If necessary, in the same `build.gradle` you will need to increase `minSdkVersion` of `defaultConfig` up to `23` (currently default Flutter generator set it to `16`).
### Important reminder
When you compile the release apk, you need to add the following operations,
[Setup Proguard Rules](https://github.com/flutter-webrtc/flutter-webrtc/blob/main/android/proguard-rules.pro)
## Contributing
The project is inseparable from the contributors of the community.
- [CloudWebRTC](https://github.com/cloudwebrtc) - Original Author
- [RainwayApp](https://github.com/rainwayapp) - Sponsor
- [亢少军](https://github.com/kangshaojun) - Sponsor
- [ION](https://github.com/pion/ion) - Sponsor
- [reSipWebRTC](https://github.com/reSipWebRTC) - Sponsor
- [沃德米科技](https://github.com/woodemi)-[36记手写板](https://www.36notes.com) - Sponsor
- [阿斯特网络科技有限公司](https://www.astgo.net/) - Sponsor
### Example
For more examples, please refer to [flutter-webrtc-demo](https://github.com/cloudwebrtc/flutter-webrtc-demo/).
## Contributors
### Code Contributors
This project exists thanks to all the people who contribute. [[Contribute](CONTRIBUTING.md)].
<a href="https://github.com/cloudwebrtc/flutter-webrtc/graphs/contributors"><img src="https://opencollective.com/flutter-webrtc/contributors.svg?width=890&button=false" /></a>
### Financial Contributors
Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/flutter-webrtc/contribute)]
#### Individuals
<a href="https://opencollective.com/flutter-webrtc"><img src="https://opencollective.com/flutter-webrtc/individuals.svg?width=890"></a>
#### Organizations
Support this project with your organization. Your logo will show up here with a link to your website. [[Contribute](https://opencollective.com/flutter-webrtc/contribute)]
<a href="https://opencollective.com/flutter-webrtc/organization/0/website"><img src="https://opencollective.com/flutter-webrtc/organization/0/avatar.svg"></a>
<a href="https://opencollective.com/flutter-webrtc/organization/1/website"><img src="https://opencollective.com/flutter-webrtc/organization/1/avatar.svg"></a>
<a href="https://opencollective.com/flutter-webrtc/organization/2/website"><img src="https://opencollective.com/flutter-webrtc/organization/2/avatar.svg"></a>
<a href="https://opencollective.com/flutter-webrtc/organization/3/website"><img src="https://opencollective.com/flutter-webrtc/organization/3/avatar.svg"></a>
<a href="https://opencollective.com/flutter-webrtc/organization/4/website"><img src="https://opencollective.com/flutter-webrtc/organization/4/avatar.svg"></a>
<a href="https://opencollective.com/flutter-webrtc/organization/5/website"><img src="https://opencollective.com/flutter-webrtc/organization/5/avatar.svg"></a>
<a href="https://opencollective.com/flutter-webrtc/organization/6/website"><img src="https://opencollective.com/flutter-webrtc/organization/6/avatar.svg"></a>
<a href="https://opencollective.com/flutter-webrtc/organization/7/website"><img src="https://opencollective.com/flutter-webrtc/organization/7/avatar.svg"></a>
<a href="https://opencollective.com/flutter-webrtc/organization/8/website"><img src="https://opencollective.com/flutter-webrtc/organization/8/avatar.svg"></a>
<a href="https://opencollective.com/flutter-webrtc/organization/9/website"><img src="https://opencollective.com/flutter-webrtc/organization/9/avatar.svg"></a>
include: package:lints/recommended.yaml
linter:
rules:
- always_declare_return_types
- avoid_empty_else
- await_only_futures
- avoid_returning_null_for_void
- camel_case_extensions
- camel_case_types
- cancel_subscriptions
- flutter_style_todos
- sort_constructors_first
- sort_unnamed_constructors_first
- sort_pub_dependencies
- type_init_formals
- unnecessary_brace_in_string_interps
- unnecessary_const
- unnecessary_new
- unnecessary_getters_setters
- unnecessary_null_aware_assignments
- unnecessary_null_in_if_null_operators
- unnecessary_overrides
- unnecessary_parenthesis
- unnecessary_statements
- unnecessary_string_interpolations
- unnecessary_this
- unrelated_type_equality_checks
- use_rethrow_when_possible
- valid_regexps
- void_checks
analyzer:
errors:
# treat missing required parameters as a warning (not a hint)
missing_required_param: warning
# treat missing returns as a warning (not a hint)
missing_return: warning
# allow having TODOs in the code
todo: ignore
# allow self-reference to deprecated members (we do this because otherwise we have
# to annotate every member in every test, assert, etc, when we deprecate something)
deprecated_member_use_from_same_package: ignore
# Conflict with import_sorter
directives_ordering: ignore
constant_identifier_names: ignore
deprecated_member_use: ignore
implementation_imports: ignore
group 'com.cloudwebrtc.webrtc'
version '1.0-SNAPSHOT'
buildscript {
ext.kotlin_version = '1.8.10'
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.1.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
rootProject.allprojects {
repositories {
google()
mavenCentral()
maven { url 'https://jitpack.io' }
}
}
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
android {
if (project.android.hasProperty("namespace")) {
namespace 'com.cloudwebrtc.webrtc'
}
compileSdkVersion 36
defaultConfig {
minSdkVersion 21
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
consumerProguardFiles 'proguard-rules.pro'
}
lintOptions {
disable 'InvalidPackage'
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8
}
}
dependencies {
implementation 'io.github.webrtc-sdk:android:144.7559.01'
implementation 'com.github.davidliu:audioswitch:89582c47c9a04c62f90aa5e57251af4800a62c9a'
implementation 'androidx.annotation:annotation:1.1.0'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
}
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
android.enableJetifier=true
# Flutter WebRTC
-keep class com.cloudwebrtc.webrtc.** { *; }
-keep class org.webrtc.** { *; }
-keep class org.jni_zero.** { *; }
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.cloudwebrtc.webrtc">
</manifest>
package com.cloudwebrtc.webrtc;
import android.util.Log;
import org.webrtc.CameraVideoCapturer;
class CameraEventsHandler implements CameraVideoCapturer.CameraEventsHandler {
public enum CameraState {
NEW,
OPENING,
OPENED,
CLOSED,
DISCONNECTED,
ERROR,
FREEZED
}
private final static String TAG = FlutterWebRTCPlugin.TAG;
private CameraState state = CameraState.NEW;
public void waitForCameraOpen() {
Log.d(TAG, "CameraEventsHandler.waitForCameraOpen");
while (state != CameraState.OPENED && state != CameraState.ERROR) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void waitForCameraClosed() {
Log.d(TAG, "CameraEventsHandler.waitForCameraClosed");
while (state != CameraState.CLOSED && state != CameraState.ERROR) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// Camera error handler - invoked when camera can not be opened
// or any camera exception happens on camera thread.
@Override
public void onCameraError(String errorDescription) {
Log.d(TAG, String.format("CameraEventsHandler.onCameraError: errorDescription=%s", errorDescription));
state = CameraState.ERROR;
}
// Called when camera is disconnected.
@Override
public void onCameraDisconnected() {
Log.d(TAG, "CameraEventsHandler.onCameraDisconnected");
state = CameraState.DISCONNECTED;
}
// Invoked when camera stops receiving frames
@Override
public void onCameraFreezed(String errorDescription) {
Log.d(TAG, String.format("CameraEventsHandler.onCameraFreezed: errorDescription=%s", errorDescription));
state = CameraState.FREEZED;
}
// Callback invoked when camera is opening.
@Override
public void onCameraOpening(String cameraName) {
Log.d(TAG, String.format("CameraEventsHandler.onCameraOpening: cameraName=%s", cameraName));
state = CameraState.OPENING;
}
// Callback invoked when first camera frame is available after camera is opened.
@Override
public void onFirstFrameAvailable() {
Log.d(TAG, "CameraEventsHandler.onFirstFrameAvailable");
state = CameraState.OPENED;
}
// Callback invoked when camera closed.
@Override
public void onCameraClosed() {
Log.d(TAG, "CameraEventsHandler.onFirstFrameAvailable");
state = CameraState.CLOSED;
}
}
package com.cloudwebrtc.webrtc;
import com.cloudwebrtc.webrtc.utils.AnyThreadSink;
import com.cloudwebrtc.webrtc.utils.ConstraintsMap;
import org.webrtc.DataChannel;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import io.flutter.plugin.common.BinaryMessenger;
import io.flutter.plugin.common.EventChannel;
class DataChannelObserver implements DataChannel.Observer, EventChannel.StreamHandler {
private final String flutterId;
private final DataChannel dataChannel;
private final EventChannel eventChannel;
private EventChannel.EventSink eventSink;
private final ArrayList eventQueue = new ArrayList();
DataChannelObserver(BinaryMessenger messenger, String peerConnectionId, String flutterId,
DataChannel dataChannel) {
this.flutterId = flutterId;
this.dataChannel = dataChannel;
eventChannel =
new EventChannel(messenger, "FlutterWebRTC/dataChannelEvent" + peerConnectionId + flutterId);
eventChannel.setStreamHandler(this);
}
private String dataChannelStateString(DataChannel.State dataChannelState) {
switch (dataChannelState) {
case CONNECTING:
return "connecting";
case OPEN:
return "open";
case CLOSING:
return "closing";
case CLOSED:
return "closed";
}
return "";
}
@Override
public void onListen(Object o, EventChannel.EventSink sink) {
eventSink = new AnyThreadSink(sink);
for(Object event : eventQueue) {
eventSink.success(event);
}
eventQueue.clear();
}
@Override
public void onCancel(Object o) {
eventSink = null;
}
@Override
public void onBufferedAmountChange(long amount) {
ConstraintsMap params = new ConstraintsMap();
params.putString("event", "dataChannelBufferedAmountChange");
params.putInt("id", dataChannel.id());
params.putLong("bufferedAmount", dataChannel.bufferedAmount());
params.putLong("changedAmount", amount);
sendEvent(params);
}
@Override
public void onStateChange() {
ConstraintsMap params = new ConstraintsMap();
params.putString("event", "dataChannelStateChanged");
params.putInt("id", dataChannel.id());
params.putString("state", dataChannelStateString(dataChannel.state()));
sendEvent(params);
}
@Override
public void onMessage(DataChannel.Buffer buffer) {
ConstraintsMap params = new ConstraintsMap();
params.putString("event", "dataChannelReceiveMessage");
params.putInt("id", dataChannel.id());
byte[] bytes;
if (buffer.data.hasArray()) {
bytes = buffer.data.array();
} else {
bytes = new byte[buffer.data.remaining()];
buffer.data.get(bytes);
}
if (buffer.binary) {
params.putString("type", "binary");
params.putByte("data", bytes);
} else {
params.putString("type", "text");
params.putString("data", new String(bytes, StandardCharsets.UTF_8));
}
sendEvent(params);
}
private void sendEvent(ConstraintsMap params) {
if (eventSink != null) {
eventSink.success(params.toMap());
} else {
eventQueue.add(params.toMap());
}
}
}
package com.cloudwebrtc.webrtc;
import androidx.annotation.NonNull;
import com.cloudwebrtc.webrtc.utils.ConstraintsMap;
import org.webrtc.DataPacketCryptor;
import org.webrtc.DataPacketCryptorFactory;
import org.webrtc.FrameCryptor;
import org.webrtc.FrameCryptorAlgorithm;
import org.webrtc.FrameCryptorKeyProvider;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
class FlutterDataPacketCryptor {
private static final String TAG = "FlutterDataPacketCryptor";
private final Map<String, DataPacketCryptor> dataCryptos = new HashMap<>();
private final FlutterRTCFrameCryptor frameCryptor;
public FlutterDataPacketCryptor(FlutterRTCFrameCryptor frameCryptor) {
this.frameCryptor = frameCryptor;
}
public boolean handleMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
String method_name = call.method;
Map<String, Object> params = (Map<String, Object>) call.arguments;
if (method_name.equals("createDataPacketCryptor")) {
createDataPacketCryptor(params, result);
} else if(method_name.equals("dataPacketCryptorEncrypt")) {
dataPacketCryptorEncrypt(params, result);
} else if(method_name.equals("dataPacketCryptorDecrypt")) {
dataPacketCryptorDecrypt(params, result);
} else if(method_name.equals("dataPacketCryptorDispose")) {
dataPacketCryptorDispose(params, result);
} else {
return false;
}
return true;
}
private void createDataPacketCryptor(@NonNull Map<String, Object> params, @NonNull MethodChannel.Result result) {
String keyProviderId = (String) params.get("keyProviderId");
FrameCryptorKeyProvider keyProvider = frameCryptor.getKeyProvider(keyProviderId);
if (keyProvider == null) {
result.error("createDataPacketCryptorFailed", "keyProvider not found", null);
return;
}
if(params.get("algorithm") == null) {
result.error("createDataPacketCryptorFailed", "algorithm is null", null);
return;
}
int algorithm = (int) params.get("algorithm");
DataPacketCryptor dataPacketCryptor = DataPacketCryptorFactory.createDataPacketCryptor(
frameCryptor.frameCryptorAlgorithmFromInt(algorithm),
keyProvider);
if(dataPacketCryptor == null) {
result.error("createDataPacketCryptorFailed", "createDataPacketCryptor failed", null);
return;
}
String dataCryptorId = UUID.randomUUID().toString();
dataCryptos.put(dataCryptorId, dataPacketCryptor);
ConstraintsMap paramsResult = new ConstraintsMap();
paramsResult.putString("dataCryptorId", dataCryptorId);
result.success(paramsResult.toMap());
}
private void dataPacketCryptorEncrypt(@NonNull Map<String, Object> params, @NonNull MethodChannel.Result result) {
String dataCryptorId = (String) params.get("dataCryptorId");
if (dataCryptorId == null) {
result.error("dataPacketCryptorEncryptFailed", "dataCryptorId is null", null);
return;
}
DataPacketCryptor dataPacketCryptor = dataCryptos.get(dataCryptorId);
if(dataPacketCryptor == null) {
result.error("dataPacketCryptorEncryptFailed", "dataPacketCryptor not found", null);
return;
}
String participantId = (String) params.get("participantId");
if (participantId == null) {
result.error("dataPacketCryptorEncryptFailed", "participantId is null", null);
return;
}
byte[] data = (byte[]) params.get("data");
if (data == null) {
result.error("dataPacketCryptorEncryptFailed", "data is null", null);
return;
}
int keyIndex = (int) params.get("keyIndex");
if( keyIndex < 0 ) {
result.error("dataPacketCryptorEncryptFailed", "keyIndex is invalid", null);
return;
}
DataPacketCryptor.EncryptedPacket packet = dataPacketCryptor.encrypt(participantId, keyIndex, data);
ConstraintsMap paramsResult = new ConstraintsMap();
paramsResult.putInt("keyIndex", packet.keyIndex);
paramsResult.putByte("data", packet.payload);
paramsResult.putByte("iv", packet.iv);
result.success(paramsResult.toMap());
}
private void dataPacketCryptorDecrypt(@NonNull Map<String, Object> params, @NonNull MethodChannel.Result result) {
String dataCryptorId = (String) params.get("dataCryptorId");
if (dataCryptorId == null) {
result.error("dataPacketCryptorEncryptFailed", "dataCryptorId is null", null);
return;
}
DataPacketCryptor dataPacketCryptor = dataCryptos.get(dataCryptorId);
if(dataPacketCryptor == null) {
result.error("dataPacketCryptorEncryptFailed", "dataPacketCryptor not found", null);
return;
}
String participantId = (String) params.get("participantId");
if (participantId == null) {
result.error("dataPacketCryptorEncryptFailed", "participantId is null", null);
return;
}
byte[] data = (byte[]) params.get("data");
if (data == null) {
result.error("dataPacketCryptorEncryptFailed", "data is null", null);
return;
}
byte[] iv = (byte[]) params.get("iv");
if (iv == null) {
result.error("dataPacketCryptorEncryptFailed", "iv is null", null);
return;
}
int keyIndex = (int) params.get("keyIndex");
if( keyIndex < 0 ) {
result.error("dataPacketCryptorEncryptFailed", "keyIndex is invalid", null);
return;
}
DataPacketCryptor.EncryptedPacket encryptedPacket = new DataPacketCryptor.EncryptedPacket(data, iv, keyIndex);
byte[] decrypted = dataPacketCryptor.decrypt(participantId, encryptedPacket);
if(decrypted == null) {
result.error("dataPacketCryptorDecryptFailed", "decrypt failed", null);
return;
}
ConstraintsMap paramsResult = new ConstraintsMap();
paramsResult.putByte("data", decrypted);
result.success(paramsResult.toMap());
}
private void dataPacketCryptorDispose(@NonNull Map<String, Object> params, @NonNull MethodChannel.Result result) {
String dataCryptorId = (String) params.get("dataCryptorId");
if (dataCryptorId == null) {
result.error("dataPacketCryptorDisposeFailed", "dataCryptorId is null", null);
return;
}
DataPacketCryptor dataPacketCryptor = dataCryptos.remove(dataCryptorId);
if(dataPacketCryptor == null) {
result.error("dataPacketCryptorDisposeFailed", "dataPacketCryptor not found", null);
return;
}
if (dataPacketCryptor != null) {
dataPacketCryptor.dispose();
}
result.success(null);
}
}
package com.cloudwebrtc.webrtc;
import android.util.Log;
import android.graphics.SurfaceTexture;
import android.view.Surface;
import com.cloudwebrtc.webrtc.utils.AnyThreadSink;
import com.cloudwebrtc.webrtc.utils.ConstraintsMap;
import com.cloudwebrtc.webrtc.utils.EglUtils;
import java.util.List;
import org.webrtc.EglBase;
import org.webrtc.MediaStream;
import org.webrtc.RendererCommon.RendererEvents;
import org.webrtc.VideoTrack;
import io.flutter.plugin.common.EventChannel;
import io.flutter.view.TextureRegistry;
public class FlutterRTCVideoRenderer implements EventChannel.StreamHandler {
private static final String TAG = FlutterWebRTCPlugin.TAG;
private final TextureRegistry.SurfaceProducer producer;
private int id = -1;
private MediaStream mediaStream;
private String ownerTag;
public void Dispose() {
//destroy
if (surfaceTextureRenderer != null) {
surfaceTextureRenderer.release();
}
if (eventChannel != null)
eventChannel.setStreamHandler(null);
eventSink = null;
producer.release();
}
/**
* The {@code RendererEvents} which listens to rendering events reported by
* {@link #surfaceTextureRenderer}.
*/
private RendererEvents rendererEvents;
private void listenRendererEvents() {
rendererEvents = new RendererEvents() {
private int _rotation = -1;
private int _width = 0, _height = 0;
@Override
public void onFirstFrameRendered() {
ConstraintsMap params = new ConstraintsMap();
params.putString("event", "didFirstFrameRendered");
params.putInt("id", id);
if (eventSink != null) {
eventSink.success(params.toMap());
}
}
@Override
public void onFrameResolutionChanged(
int videoWidth, int videoHeight,
int rotation) {
if (eventSink != null) {
if (_width != videoWidth || _height != videoHeight) {
ConstraintsMap params = new ConstraintsMap();
params.putString("event", "didTextureChangeVideoSize");
params.putInt("id", id);
params.putDouble("width", (double) videoWidth);
params.putDouble("height", (double) videoHeight);
_width = videoWidth;
_height = videoHeight;
eventSink.success(params.toMap());
}
if (_rotation != rotation) {
ConstraintsMap params2 = new ConstraintsMap();
params2.putString("event", "didTextureChangeRotation");
params2.putInt("id", id);
params2.putInt("rotation", rotation);
_rotation = rotation;
eventSink.success(params2.toMap());
}
}
}
};
}
private final SurfaceTextureRenderer surfaceTextureRenderer;
/**
* The {@code VideoTrack}, if any, rendered by this {@code FlutterRTCVideoRenderer}.
*/
private VideoTrack videoTrack;
EventChannel eventChannel;
EventChannel.EventSink eventSink;
public FlutterRTCVideoRenderer(TextureRegistry.SurfaceProducer producer) {
this.surfaceTextureRenderer = new SurfaceTextureRenderer("");
listenRendererEvents();
surfaceTextureRenderer.init(EglUtils.getRootEglBaseContext(), rendererEvents);
surfaceTextureRenderer.surfaceCreated(producer);
this.eventSink = null;
this.producer = producer;
this.ownerTag = null;
}
public void setEventChannel(EventChannel eventChannel) {
this.eventChannel = eventChannel;
}
public void setId(int id) {
this.id = id;
}
@Override
public void onListen(Object o, EventChannel.EventSink sink) {
eventSink = new AnyThreadSink(sink);
}
@Override
public void onCancel(Object o) {
eventSink = null;
}
/**
* Stops rendering {@link #videoTrack} and releases the associated acquired
* resources (if rendering is in progress).
*/
private void removeRendererFromVideoTrack() {
videoTrack.removeSink(surfaceTextureRenderer);
}
/**
* Sets the {@code MediaStream} to be rendered by this {@code FlutterRTCVideoRenderer}.
* The implementation renders the first {@link VideoTrack}, if any, of the
* specified {@code mediaStream}.
*
* @param mediaStream The {@code MediaStream} to be rendered by this
* {@code FlutterRTCVideoRenderer} or {@code null}.
*/
public void setStream(MediaStream mediaStream, String ownerTag) {
VideoTrack videoTrack;
this.mediaStream = mediaStream;
this.ownerTag = ownerTag;
if (mediaStream == null) {
videoTrack = null;
} else {
List<VideoTrack> videoTracks = mediaStream.videoTracks;
videoTrack = videoTracks.isEmpty() ? null : videoTracks.get(0);
}
setVideoTrack(videoTrack);
}
/**
* Sets the {@code MediaStream} to be rendered by this {@code FlutterRTCVideoRenderer}.
* The implementation renders the first {@link VideoTrack}, if any, of the
* specified trackId
*
* @param mediaStream The {@code MediaStream} to be rendered by this
* {@code FlutterRTCVideoRenderer} or {@code null}.
* @param trackId The {@code trackId} to be rendered by this
* {@code FlutterRTCVideoRenderer} or {@code null}.
*/
public void setStream(MediaStream mediaStream,String trackId, String ownerTag) {
VideoTrack videoTrack;
this.mediaStream = mediaStream;
this.ownerTag = ownerTag;
if (mediaStream == null) {
videoTrack = null;
} else {
List<VideoTrack> videoTracks = mediaStream.videoTracks;
videoTrack = videoTracks.isEmpty() ? null : videoTracks.get(0);
for (VideoTrack track : videoTracks){
if (track.id().equals(trackId)){
videoTrack = track;
}
}
}
setVideoTrack(videoTrack);
}
/**
* Sets the {@code VideoTrack} to be rendered by this {@code FlutterRTCVideoRenderer}.
*
* @param videoTrack The {@code VideoTrack} to be rendered by this
* {@code FlutterRTCVideoRenderer} or {@code null}.
*/
public void setVideoTrack(VideoTrack videoTrack) {
VideoTrack oldValue = this.videoTrack;
if (oldValue != videoTrack) {
if (oldValue != null) {
removeRendererFromVideoTrack();
}
this.videoTrack = videoTrack;
if (videoTrack != null) {
try {
Log.w(TAG, "FlutterRTCVideoRenderer.setVideoTrack, set video track to " + videoTrack.id());
tryAddRendererToVideoTrack();
} catch (Exception e) {
Log.e(TAG, "tryAddRendererToVideoTrack " + e);
}
} else {
Log.w(TAG, "FlutterRTCVideoRenderer.setVideoTrack, set video track to null");
}
}
}
/**
* Starts rendering {@link #videoTrack} if rendering is not in progress and
* all preconditions for the start of rendering are met.
*/
private void tryAddRendererToVideoTrack() throws Exception {
if (videoTrack != null) {
EglBase.Context sharedContext = EglUtils.getRootEglBaseContext();
if (sharedContext == null) {
// If SurfaceViewRenderer#init() is invoked, it will throw a
// RuntimeException which will very likely kill the application.
Log.e(TAG, "Failed to render a VideoTrack!");
return;
}
surfaceTextureRenderer.release();
listenRendererEvents();
surfaceTextureRenderer.init(sharedContext, rendererEvents);
surfaceTextureRenderer.surfaceCreated(producer);
videoTrack.addSink(surfaceTextureRenderer);
}
}
public boolean checkMediaStream(String id, String ownerTag) {
if (null == id || null == mediaStream || ownerTag == null || !ownerTag.equals(this.ownerTag)) {
return false;
}
return id.equals(mediaStream.getId());
}
public boolean checkVideoTrack(String id, String ownerTag) {
if (null == id || null == videoTrack || ownerTag == null || !ownerTag.equals(this.ownerTag)) {
return false;
}
return id.equals(videoTrack.id());
}
}
package com.cloudwebrtc.webrtc;
import android.app.Activity;
import android.app.Application;
import android.content.Context;
import android.os.Bundle;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleOwner;
import com.cloudwebrtc.webrtc.audio.AudioProcessingController;
import com.cloudwebrtc.webrtc.audio.AudioSwitchManager;
import com.cloudwebrtc.webrtc.utils.AnyThreadSink;
import com.cloudwebrtc.webrtc.utils.ConstraintsMap;
import org.webrtc.ExternalAudioProcessingFactory;
import org.webrtc.MediaStreamTrack;
import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.embedding.engine.plugins.activity.ActivityAware;
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
import io.flutter.embedding.engine.plugins.lifecycle.HiddenLifecycleReference;
import io.flutter.plugin.common.BinaryMessenger;
import io.flutter.plugin.common.EventChannel;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.view.TextureRegistry;
/**
* FlutterWebRTCPlugin
*/
public class FlutterWebRTCPlugin implements FlutterPlugin, ActivityAware, EventChannel.StreamHandler {
static public final String TAG = "FlutterWebRTCPlugin";
private static Application application;
private MethodChannel methodChannel;
private MethodCallHandlerImpl methodCallHandler;
private LifeCycleObserver observer;
private Lifecycle lifecycle;
private EventChannel eventChannel;
// eventSink is static because FlutterWebRTCPlugin can be instantiated multiple times
// but the onListen(Object, EventChannel.EventSink) event only fires once for the first
// FlutterWebRTCPlugin instance, so for the next instances eventSink will be == null
public static EventChannel.EventSink eventSink;
public FlutterWebRTCPlugin() {
sharedSingleton = this;
}
public static FlutterWebRTCPlugin sharedSingleton;
public AudioProcessingController getAudioProcessingController() {
return methodCallHandler.audioProcessingController;
}
public MediaStreamTrack getTrackForId(String trackId, String peerConnectionId) {
return methodCallHandler.getTrackForId(trackId, peerConnectionId);
}
public LocalTrack getLocalTrack(String trackId) {
return methodCallHandler.getLocalTrack(trackId);
}
public MediaStreamTrack getRemoteTrack(String trackId) {
return methodCallHandler.getRemoteTrack(trackId);
}
@Override
public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) {
startListening(binding.getApplicationContext(), binding.getBinaryMessenger(),
binding.getTextureRegistry());
}
@Override
public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
stopListening();
}
@Override
public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) {
methodCallHandler.setActivity(binding.getActivity());
this.observer = new LifeCycleObserver();
this.lifecycle = ((HiddenLifecycleReference) binding.getLifecycle()).getLifecycle();
this.lifecycle.addObserver(this.observer);
}
@Override
public void onDetachedFromActivityForConfigChanges() {
methodCallHandler.setActivity(null);
}
@Override
public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding binding) {
methodCallHandler.setActivity(binding.getActivity());
}
@Override
public void onDetachedFromActivity() {
methodCallHandler.setActivity(null);
if (this.observer != null) {
this.lifecycle.removeObserver(this.observer);
if (application!=null) {
application.unregisterActivityLifecycleCallbacks(this.observer);
}
}
this.lifecycle = null;
}
private void startListening(final Context context, BinaryMessenger messenger,
TextureRegistry textureRegistry) {
AudioSwitchManager.instance = new AudioSwitchManager(context);
methodCallHandler = new MethodCallHandlerImpl(context, messenger, textureRegistry);
methodChannel = new MethodChannel(messenger, "FlutterWebRTC.Method");
methodChannel.setMethodCallHandler(methodCallHandler);
eventChannel = new EventChannel( messenger,"FlutterWebRTC.Event");
eventChannel.setStreamHandler(this);
AudioSwitchManager.instance.audioDeviceChangeListener = (devices, currentDevice) -> {
Log.w(TAG, "audioFocusChangeListener " + devices+ " " + currentDevice);
ConstraintsMap params = new ConstraintsMap();
params.putString("event", "onDeviceChange");
sendEvent(params.toMap());
return null;
};
}
private void stopListening() {
methodCallHandler.dispose();
methodCallHandler = null;
methodChannel.setMethodCallHandler(null);
eventChannel.setStreamHandler(null);
if (AudioSwitchManager.instance != null) {
Log.d(TAG, "Stopping the audio manager...");
AudioSwitchManager.instance.stop();
}
}
@Override
public void onListen(Object arguments, EventChannel.EventSink events) {
eventSink = new AnyThreadSink(events);
}
@Override
public void onCancel(Object arguments) {
eventSink = null;
}
public void sendEvent(Object event) {
if(eventSink != null) {
eventSink.success(event);
}
}
private class LifeCycleObserver implements Application.ActivityLifecycleCallbacks, DefaultLifecycleObserver {
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
}
@Override
public void onActivityStarted(Activity activity) {
}
@Override
public void onActivityResumed(Activity activity) {
if (null != methodCallHandler) {
methodCallHandler.reStartCamera();
}
}
@Override
public void onResume(LifecycleOwner owner) {
if (null != methodCallHandler) {
methodCallHandler.reStartCamera();
}
}
@Override
public void onActivityPaused(Activity activity) {
}
@Override
public void onActivityStopped(Activity activity) {
}
@Override
public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
}
@Override
public void onActivityDestroyed(Activity activity) {
}
}
}
package com.cloudwebrtc.webrtc;
import org.webrtc.MediaStreamTrack;
public class LocalTrack {
public LocalTrack(MediaStreamTrack track) {
this.track = track;
}
public MediaStreamTrack track;
public void dispose() {
track.dispose();
}
public boolean enabled() {
return track.enabled();
}
public void setEnabled(boolean enabled) {
track.setEnabled(enabled);
}
public String id() {
return track.id();
}
public String kind() {
return track.kind();
}
}
package com.cloudwebrtc.webrtc;
import org.webrtc.SurfaceTextureHelper;
import org.webrtc.CapturerObserver;
import org.webrtc.ThreadUtils;
import org.webrtc.VideoCapturer;
import org.webrtc.VideoFrame;
import org.webrtc.VideoSink;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.Intent;
import android.media.projection.MediaProjection;
import android.view.Surface;
import android.view.WindowManager;
import android.app.Activity;
import android.hardware.display.DisplayManager;
import android.util.DisplayMetrics;
import android.hardware.display.VirtualDisplay;
import android.media.projection.MediaProjectionManager;
import android.os.Looper;
import android.os.Handler;
import android.os.Build;
import android.view.Display;
/**
* An copy of ScreenCapturerAndroid to capture the screen content while being aware of device orientation
*/
@TargetApi(21)
public class OrientationAwareScreenCapturer implements VideoCapturer, VideoSink {
private static final int DISPLAY_FLAGS =
DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC | DisplayManager.VIRTUAL_DISPLAY_FLAG_PRESENTATION;
// DPI for VirtualDisplay, does not seem to matter for us.
private static final int VIRTUAL_DISPLAY_DPI = 400;
private final Intent mediaProjectionPermissionResultData;
private final MediaProjection.Callback mediaProjectionCallback;
private int width;
private int height;
private int oldWidth;
private int oldHeight;
private VirtualDisplay virtualDisplay;
private SurfaceTextureHelper surfaceTextureHelper;
private CapturerObserver capturerObserver;
private long numCapturedFrames = 0;
private MediaProjection mediaProjection;
private boolean isDisposed = false;
private MediaProjectionManager mediaProjectionManager;
private WindowManager windowManager;
private boolean isPortrait;
/**
* Constructs a new Screen Capturer.
*
* @param mediaProjectionPermissionResultData the result data of MediaProjection permission
* activity; the calling app must validate that result code is Activity.RESULT_OK before
* calling this method.
* @param mediaProjectionCallback MediaProjection callback to implement application specific
* logic in events such as when the user revokes a previously granted capture permission.
**/
public OrientationAwareScreenCapturer(Intent mediaProjectionPermissionResultData,
MediaProjection.Callback mediaProjectionCallback) {
this.mediaProjectionPermissionResultData = mediaProjectionPermissionResultData;
this.mediaProjectionCallback = mediaProjectionCallback;
}
public void onFrame(VideoFrame frame) {
checkNotDisposed();
this.isPortrait = isDeviceOrientationPortrait();
final int max = Math.max(this.height, this.width);
final int min = Math.min(this.height, this.width);
if (this.isPortrait) {
changeCaptureFormat(min, max, 15);
} else {
changeCaptureFormat(max, min, 15);
}
capturerObserver.onFrameCaptured(frame);
}
private boolean isDeviceOrientationPortrait() {
final Display display = windowManager.getDefaultDisplay();
final DisplayMetrics metrics = new DisplayMetrics();
display.getRealMetrics(metrics);
return metrics.heightPixels > metrics.widthPixels;
}
private void checkNotDisposed() {
if (isDisposed) {
throw new RuntimeException("capturer is disposed.");
}
}
public synchronized void initialize(final SurfaceTextureHelper surfaceTextureHelper,
final Context applicationContext, final CapturerObserver capturerObserver) {
checkNotDisposed();
if (capturerObserver == null) {
throw new RuntimeException("capturerObserver not set.");
}
this.capturerObserver = capturerObserver;
if (surfaceTextureHelper == null) {
throw new RuntimeException("surfaceTextureHelper not set.");
}
this.surfaceTextureHelper = surfaceTextureHelper;
this.windowManager = (WindowManager) applicationContext.getSystemService(
Context.WINDOW_SERVICE);
this.mediaProjectionManager = (MediaProjectionManager) applicationContext.getSystemService(
Context.MEDIA_PROJECTION_SERVICE);
}
@Override
public synchronized void startCapture(
final int width, final int height, final int ignoredFramerate) {
//checkNotDisposed();
this.isPortrait = isDeviceOrientationPortrait();
if (this.isPortrait) {
this.width = width;
this.height = height;
} else {
this.height = width;
this.width = height;
}
mediaProjection = mediaProjectionManager.getMediaProjection(
Activity.RESULT_OK, mediaProjectionPermissionResultData);
// Let MediaProjection callback use the SurfaceTextureHelper thread.
mediaProjection.registerCallback(mediaProjectionCallback, surfaceTextureHelper.getHandler());
createVirtualDisplay();
capturerObserver.onCapturerStarted(true);
surfaceTextureHelper.startListening(this);
}
@Override
public synchronized void stopCapture() {
checkNotDisposed();
ThreadUtils.invokeAtFrontUninterruptibly(surfaceTextureHelper.getHandler(), new Runnable() {
@Override
public void run() {
surfaceTextureHelper.stopListening();
capturerObserver.onCapturerStopped();
if (virtualDisplay != null) {
virtualDisplay.release();
virtualDisplay = null;
}
if (mediaProjection != null) {
// Unregister the callback before stopping, otherwise the callback recursively
// calls this method.
mediaProjection.unregisterCallback(mediaProjectionCallback);
mediaProjection.stop();
mediaProjection = null;
}
}
});
}
@Override
public synchronized void dispose() {
isDisposed = true;
}
/**
* Changes output video format. This method can be used to scale the output
* video, or to change orientation when the captured screen is rotated for example.
*
* @param width new output video width
* @param height new output video height
* @param ignoredFramerate ignored
*/
@Override
public synchronized void changeCaptureFormat(
final int width, final int height, final int ignoredFramerate) {
checkNotDisposed();
if (this.oldWidth != width || this.oldHeight != height) {
this.oldWidth = width;
this.oldHeight = height;
if (oldHeight > oldWidth) {
ThreadUtils.invokeAtFrontUninterruptibly(surfaceTextureHelper.getHandler(), new Runnable() {
@Override
public void run() {
if (virtualDisplay != null && surfaceTextureHelper != null) {
virtualDisplay.setSurface(new Surface(surfaceTextureHelper.getSurfaceTexture()));
surfaceTextureHelper.setTextureSize(oldWidth, oldHeight);
virtualDisplay.resize(oldWidth, oldHeight, VIRTUAL_DISPLAY_DPI);
}
}
});
}
if (oldWidth > oldHeight) {
surfaceTextureHelper.setTextureSize(oldWidth, oldHeight);
virtualDisplay.setSurface(new Surface(surfaceTextureHelper.getSurfaceTexture()));
final Handler handler = new Handler(Looper.getMainLooper());
handler.postDelayed(new Runnable() {
@Override
public void run() {
ThreadUtils.invokeAtFrontUninterruptibly(surfaceTextureHelper.getHandler(), new Runnable() {
@Override
public void run() {
if (virtualDisplay != null && surfaceTextureHelper != null) {
virtualDisplay.resize(oldWidth, oldHeight, VIRTUAL_DISPLAY_DPI);
}
}
});
}
}, 700);
}
}
}
private void createVirtualDisplay() {
surfaceTextureHelper.setTextureSize(width, height);
surfaceTextureHelper.getSurfaceTexture().setDefaultBufferSize(width, height);
virtualDisplay = mediaProjection.createVirtualDisplay("WebRTC_ScreenCapture", width, height,
VIRTUAL_DISPLAY_DPI, DISPLAY_FLAGS, new Surface(surfaceTextureHelper.getSurfaceTexture()),
null /* callback */, null /* callback handler */);
}
@Override
public boolean isScreencast() {
return true;
}
public long getNumCapturedFrames() {
return numCapturedFrames;
}
}
\ No newline at end of file
package com.cloudwebrtc.webrtc;
import android.app.Activity;
import android.content.Context;
import androidx.annotation.Nullable;
import java.util.Map;
import org.webrtc.MediaStream;
import org.webrtc.MediaStreamTrack;
import org.webrtc.PeerConnectionFactory;
import io.flutter.plugin.common.BinaryMessenger;
/**
* Provides interested components with access to the current application state.
*
* It is encouraged to use this class instead of a component directly.
*/
public interface StateProvider {
boolean putLocalStream(String streamId, MediaStream stream);
boolean putLocalTrack(String trackId, LocalTrack track);
LocalTrack getLocalTrack(String trackId);
String getNextStreamUUID();
String getNextTrackUUID();
PeerConnectionFactory getPeerConnectionFactory();
PeerConnectionObserver getPeerConnectionObserver(String peerConnectionId);
@Nullable
Activity getActivity();
@Nullable
Context getApplicationContext();
BinaryMessenger getMessenger();
}
package com.cloudwebrtc.webrtc;
import android.graphics.SurfaceTexture;
import android.view.Surface;
import org.webrtc.EglBase;
import org.webrtc.EglRenderer;
import org.webrtc.GlRectDrawer;
import org.webrtc.RendererCommon;
import org.webrtc.ThreadUtils;
import org.webrtc.VideoFrame;
import java.util.concurrent.CountDownLatch;
import io.flutter.view.TextureRegistry;
/**
* Display the video stream on a Surface.
* renderFrame() is asynchronous to avoid blocking the calling thread.
* This class is thread safe and handles access from potentially three different threads:
* Interaction from the main app in init, release and setMirror.
* Interaction from C++ rtc::VideoSinkInterface in renderFrame.
* Interaction from SurfaceHolder lifecycle in surfaceCreated, surfaceChanged, and surfaceDestroyed.
*/
public class SurfaceTextureRenderer extends EglRenderer {
// Callback for reporting renderer events. Read-only after initilization so no lock required.
private RendererCommon.RendererEvents rendererEvents;
private final Object layoutLock = new Object();
private boolean isRenderingPaused;
private boolean isFirstFrameRendered;
private int rotatedFrameWidth;
private int rotatedFrameHeight;
private int frameRotation;
/**
* In order to render something, you must first call init().
*/
public SurfaceTextureRenderer(String name) {
super(name);
}
public void init(final EglBase.Context sharedContext,
RendererCommon.RendererEvents rendererEvents) {
init(sharedContext, rendererEvents, EglBase.CONFIG_PLAIN, new GlRectDrawer());
}
/**
* Initialize this class, sharing resources with |sharedContext|. The custom |drawer| will be used
* for drawing frames on the EGLSurface. This class is responsible for calling release() on
* |drawer|. It is allowed to call init() to reinitialize the renderer after a previous
* init()/release() cycle.
*/
public void init(final EglBase.Context sharedContext,
RendererCommon.RendererEvents rendererEvents, final int[] configAttributes,
RendererCommon.GlDrawer drawer) {
ThreadUtils.checkIsOnMainThread();
this.rendererEvents = rendererEvents;
synchronized (layoutLock) {
isFirstFrameRendered = false;
rotatedFrameWidth = 0;
rotatedFrameHeight = 0;
frameRotation = -1;
}
super.init(sharedContext, configAttributes, drawer);
}
@Override
public void init(final EglBase.Context sharedContext, final int[] configAttributes,
RendererCommon.GlDrawer drawer) {
init(sharedContext, null /* rendererEvents */, configAttributes, drawer);
}
/**
* Limit render framerate.
*
* @param fps Limit render framerate to this value, or use Float.POSITIVE_INFINITY to disable fps
* reduction.
*/
@Override
public void setFpsReduction(float fps) {
synchronized (layoutLock) {
isRenderingPaused = fps == 0f;
}
super.setFpsReduction(fps);
}
@Override
public void disableFpsReduction() {
synchronized (layoutLock) {
isRenderingPaused = false;
}
super.disableFpsReduction();
}
@Override
public void pauseVideo() {
synchronized (layoutLock) {
isRenderingPaused = true;
}
super.pauseVideo();
}
// VideoSink interface.
@Override
public void onFrame(VideoFrame frame) {
if(surface == null) {
producer.setSize(frame.getRotatedWidth(),frame.getRotatedHeight());
surface = producer.getSurface();
createEglSurface(surface);
}
updateFrameDimensionsAndReportEvents(frame);
super.onFrame(frame);
}
private Surface surface = null;
private TextureRegistry.SurfaceProducer producer;
public void surfaceCreated(final TextureRegistry.SurfaceProducer producer) {
ThreadUtils.checkIsOnMainThread();
this.producer = producer;
this.producer.setCallback(
new TextureRegistry.SurfaceProducer.Callback() {
@Override
public void onSurfaceAvailable() {
// Do surface initialization here, and draw the current frame.
}
@Override
public void onSurfaceCleanup() {
surfaceDestroyed();
}
}
);
}
public void surfaceDestroyed() {
ThreadUtils.checkIsOnMainThread();
final CountDownLatch completionLatch = new CountDownLatch(1);
releaseEglSurface(completionLatch::countDown);
ThreadUtils.awaitUninterruptibly(completionLatch);
surface = null;
}
// Update frame dimensions and report any changes to |rendererEvents|.
private void updateFrameDimensionsAndReportEvents(VideoFrame frame) {
synchronized (layoutLock) {
if (isRenderingPaused) {
return;
}
if (!isFirstFrameRendered) {
isFirstFrameRendered = true;
if (rendererEvents != null) {
rendererEvents.onFirstFrameRendered();
}
}
if (rotatedFrameWidth != frame.getRotatedWidth()
|| rotatedFrameHeight != frame.getRotatedHeight()
|| frameRotation != frame.getRotation()) {
if (rendererEvents != null) {
rendererEvents.onFrameResolutionChanged(
frame.getBuffer().getWidth(), frame.getBuffer().getHeight(), frame.getRotation());
}
rotatedFrameWidth = frame.getRotatedWidth();
rotatedFrameHeight = frame.getRotatedHeight();
producer.setSize(rotatedFrameWidth, rotatedFrameHeight);
frameRotation = frame.getRotation();
}
}
}
}
package com.cloudwebrtc.webrtc.audio;
import androidx.annotation.Nullable;
import com.twilio.audioswitch.AudioDevice;
public enum AudioDeviceKind {
BLUETOOTH("bluetooth", AudioDevice.BluetoothHeadset.class),
WIRED_HEADSET("wired-headset", AudioDevice.WiredHeadset.class),
SPEAKER("speaker", AudioDevice.Speakerphone.class),
EARPIECE("earpiece", AudioDevice.Earpiece.class);
public final String typeName;
public final Class<? extends AudioDevice> audioDeviceClass;
AudioDeviceKind(String typeName, Class<? extends AudioDevice> audioDeviceClass) {
this.typeName = typeName;
this.audioDeviceClass = audioDeviceClass;
}
@Nullable
public static AudioDeviceKind fromAudioDevice(AudioDevice audioDevice) {
for (AudioDeviceKind kind : values()) {
if (kind.audioDeviceClass.equals(audioDevice.getClass())) {
return kind;
}
}
return null;
}
@Nullable
public static AudioDeviceKind fromTypeName(String typeName) {
for (AudioDeviceKind kind : values()) {
if (kind.typeName.equals(typeName)) {
return kind;
}
}
return null;
}
}
package com.cloudwebrtc.webrtc.audio;
import org.webrtc.ExternalAudioProcessingFactory;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
public class AudioProcessingAdapter implements ExternalAudioProcessingFactory.AudioProcessing {
public interface ExternalAudioFrameProcessing {
void initialize(int sampleRateHz, int numChannels);
void reset(int newRate);
void process(int numBands, int numFrames, ByteBuffer buffer);
}
public AudioProcessingAdapter() {}
List<ExternalAudioFrameProcessing> audioProcessors = new ArrayList<>();
public void addProcessor(ExternalAudioFrameProcessing audioProcessor) {
synchronized (audioProcessors) {
audioProcessors.add(audioProcessor);
}
}
public void removeProcessor(ExternalAudioFrameProcessing audioProcessor) {
synchronized (audioProcessors) {
audioProcessors.remove(audioProcessor);
}
}
@Override
public void initialize(int sampleRateHz, int numChannels) {
synchronized (audioProcessors) {
for (ExternalAudioFrameProcessing audioProcessor : audioProcessors) {
audioProcessor.initialize(sampleRateHz, numChannels);
}
}
}
@Override
public void reset(int newRate) {
synchronized (audioProcessors) {
for (ExternalAudioFrameProcessing audioProcessor : audioProcessors) {
audioProcessor.reset(newRate);
}
}
}
@Override
public void process(int numBands, int numFrames, ByteBuffer buffer) {
synchronized (audioProcessors) {
for (ExternalAudioFrameProcessing audioProcessor : audioProcessors) {
audioProcessor.process(numBands, numFrames, buffer);
}
}
}
}
package com.cloudwebrtc.webrtc.audio;
import org.webrtc.ExternalAudioProcessingFactory;
public class AudioProcessingController {
/**
* This is the audio processing module that will be applied to the audio stream after it is captured from the microphone.
* This is useful for adding echo cancellation, noise suppression, etc.
*/
public final AudioProcessingAdapter capturePostProcessing = new AudioProcessingAdapter();
/**
* This is the audio processing module that will be applied to the audio stream before it is rendered to the speaker.
*/
public final AudioProcessingAdapter renderPreProcessing = new AudioProcessingAdapter();
public ExternalAudioProcessingFactory externalAudioProcessingFactory;
public AudioProcessingController() {
this.externalAudioProcessingFactory = new ExternalAudioProcessingFactory();
this.externalAudioProcessingFactory.setCapturePostProcessing(capturePostProcessing);
this.externalAudioProcessingFactory.setRenderPreProcessing(renderPreProcessing);
}
}
package com.cloudwebrtc.webrtc.audio;
import android.media.AudioAttributes;
import android.media.AudioDeviceInfo;
import android.media.AudioManager;
import android.os.Build;
import android.util.Log;
import androidx.annotation.Nullable;
public class AudioUtils {
private static final String TAG = "AudioUtils";
@Nullable
public static Integer getAudioModeForString(@Nullable String audioModeString) {
if (audioModeString == null) {
return null;
}
Integer audioMode = null;
switch (audioModeString) {
case "normal":
audioMode = AudioManager.MODE_NORMAL;
break;
case "callScreening":
audioMode = AudioManager.MODE_CALL_SCREENING;
break;
case "inCall":
audioMode = AudioManager.MODE_IN_CALL;
break;
case "inCommunication":
audioMode = AudioManager.MODE_IN_COMMUNICATION;
break;
case "ringtone":
audioMode = AudioManager.MODE_RINGTONE;
break;
default:
Log.w(TAG, "Unknown audio mode: " + audioModeString);
break;
}
return audioMode;
}
@Nullable
public static Integer getFocusModeForString(@Nullable String focusModeString) {
if (focusModeString == null) {
return null;
}
Integer focusMode = null;
switch (focusModeString) {
case "gain":
focusMode = AudioManager.AUDIOFOCUS_GAIN;
break;
case "gainTransient":
focusMode = AudioManager.AUDIOFOCUS_GAIN_TRANSIENT;
break;
case "gainTransientExclusive":
focusMode = AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE;
break;
case "gainTransientMayDuck":
focusMode = AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK;
break;
case "loss":
focusMode = AudioManager.AUDIOFOCUS_LOSS;
break;
default:
Log.w(TAG, "Unknown audio focus mode: " + focusModeString);
break;
}
return focusMode;
}
@Nullable
public static Integer getStreamTypeForString(@Nullable String streamTypeString) {
if (streamTypeString == null) {
return null;
}
Integer streamType = null;
switch (streamTypeString) {
case "accessibility":
streamType = AudioManager.STREAM_ACCESSIBILITY;
break;
case "alarm":
streamType = AudioManager.STREAM_ALARM;
break;
case "dtmf":
streamType = AudioManager.STREAM_DTMF;
break;
case "music":
streamType = AudioManager.STREAM_MUSIC;
break;
case "notification":
streamType = AudioManager.STREAM_NOTIFICATION;
break;
case "ring":
streamType = AudioManager.STREAM_RING;
break;
case "system":
streamType = AudioManager.STREAM_SYSTEM;
break;
case "voiceCall":
streamType = AudioManager.STREAM_VOICE_CALL;
break;
default:
Log.w(TAG, "Unknown audio stream type: " + streamTypeString);
break;
}
return streamType;
}
@Nullable
public static Integer getAudioAttributesUsageTypeForString(@Nullable String usageTypeString) {
if (usageTypeString == null) {
return null;
}
Integer usageType = null;
switch (usageTypeString) {
case "alarm":
usageType = AudioAttributes.USAGE_ALARM;
break;
case "assistanceAccessibility":
usageType = AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY;
break;
case "assistanceNavigationGuidance":
usageType = AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE;
break;
case "assistanceSonification":
usageType = AudioAttributes.USAGE_ASSISTANCE_SONIFICATION;
break;
case "assistant":
usageType = AudioAttributes.USAGE_ASSISTANT;
break;
case "game":
usageType = AudioAttributes.USAGE_GAME;
break;
case "media":
usageType = AudioAttributes.USAGE_MEDIA;
break;
case "notification":
usageType = AudioAttributes.USAGE_NOTIFICATION;
break;
case "notificationEvent":
usageType = AudioAttributes.USAGE_NOTIFICATION_EVENT;
break;
case "notificationRingtone":
usageType = AudioAttributes.USAGE_NOTIFICATION_RINGTONE;
break;
case "unknown":
usageType = AudioAttributes.USAGE_UNKNOWN;
break;
case "voiceCommunication":
usageType = AudioAttributes.USAGE_VOICE_COMMUNICATION;
break;
case "voiceCommunicationSignalling":
usageType = AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING;
break;
default:
Log.w(TAG, "Unknown audio attributes usage type: " + usageTypeString);
break;
}
return usageType;
}
@Nullable
public static Integer getAudioAttributesContentTypeFromString(@Nullable String contentTypeString) {
if (contentTypeString == null) {
return null;
}
Integer contentType = null;
switch (contentTypeString) {
case "movie":
contentType = AudioAttributes.CONTENT_TYPE_MOVIE;
break;
case "music":
contentType = AudioAttributes.CONTENT_TYPE_MUSIC;
break;
case "sonification":
contentType = AudioAttributes.CONTENT_TYPE_SONIFICATION;
break;
case "speech":
contentType = AudioAttributes.CONTENT_TYPE_SPEECH;
break;
case "unknown":
contentType = AudioAttributes.CONTENT_TYPE_UNKNOWN;
break;
default:
Log.w(TAG, "Unknown audio attributes content type:" + contentTypeString);
break;
}
return contentType;
}
static public String getAudioDeviceId(AudioDeviceInfo device) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
return "audio-1";
} else {
String address = Build.VERSION.SDK_INT < Build.VERSION_CODES.P ? "" : device.getAddress();
String deviceId = "" + device.getId();
if (device.getType() == AudioDeviceInfo.TYPE_BUILTIN_MIC) {
deviceId = "microphone-" + address;
}
if (device.getType() == AudioDeviceInfo.TYPE_WIRED_HEADSET) {
deviceId = "wired-headset";
}
if (device.getType() == AudioDeviceInfo.TYPE_BLUETOOTH_SCO) {
deviceId = "bluetooth";
}
return deviceId;
}
}
static public String getAudioGroupId(AudioDeviceInfo device) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
return "microphone";
} else {
String groupId = "" + device.getType();
if (device.getType() == AudioDeviceInfo.TYPE_BUILTIN_MIC) {
groupId = "microphone";
}
if (device.getType() == AudioDeviceInfo.TYPE_WIRED_HEADSET) {
groupId = "wired-headset";
}
if (device.getType() == AudioDeviceInfo.TYPE_BLUETOOTH_SCO) {
groupId = "bluetooth";
}
return groupId;
}
}
static public String getAudioDeviceLabel(AudioDeviceInfo device) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
return "Audio";
} else {
String address = Build.VERSION.SDK_INT < Build.VERSION_CODES.P ? "" : device.getAddress();
String label = device.getProductName().toString();
if (device.getType() == AudioDeviceInfo.TYPE_BUILTIN_MIC) {
label = "Built-in Microphone (" + address + ")";
}
if (device.getType() == AudioDeviceInfo.TYPE_WIRED_HEADSET) {
label = "Wired Headset Microphone";
}
if (device.getType() == AudioDeviceInfo.TYPE_BLUETOOTH_SCO) {
label = device.getProductName().toString();
}
return label;
}
}
}
\ No newline at end of file
package com.cloudwebrtc.webrtc.audio;
import android.media.AudioFormat;
import android.os.SystemClock;
import com.cloudwebrtc.webrtc.LocalTrack;
import org.webrtc.AudioTrack;
import org.webrtc.AudioTrackSink;
import org.webrtc.audio.JavaAudioDeviceModule;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
/**
* LocalAudioTrack represents an audio track that is sourced from local audio capture.
*/
public class LocalAudioTrack
extends LocalTrack implements JavaAudioDeviceModule.SamplesReadyCallback {
public LocalAudioTrack(AudioTrack audioTrack) {
super(audioTrack);
}
final List<AudioTrackSink> sinks = new ArrayList<>();
/**
* Add a sink to receive audio data from this track.
*/
public void addSink(AudioTrackSink sink) {
synchronized (sinks) {
sinks.add(sink);
}
}
/**
* Remove a sink for this track.
*/
public void removeSink(AudioTrackSink sink) {
synchronized (sinks) {
sinks.remove(sink);
}
}
private int getBytesPerSample(int audioFormat) {
switch (audioFormat) {
case AudioFormat.ENCODING_PCM_8BIT:
return 1;
case AudioFormat.ENCODING_PCM_16BIT:
case AudioFormat.ENCODING_IEC61937:
case AudioFormat.ENCODING_DEFAULT:
return 2;
case AudioFormat.ENCODING_PCM_FLOAT:
return 4;
default:
throw new IllegalArgumentException("Bad audio format " + audioFormat);
}
}
@Override
public void onWebRtcAudioRecordSamplesReady(JavaAudioDeviceModule.AudioSamples audioSamples) {
int bitsPerSample = getBytesPerSample(audioSamples.getAudioFormat()) * 8;
int numFrames = audioSamples.getSampleRate() / 100;
long timestamp = SystemClock.elapsedRealtime();
synchronized (sinks) {
for (AudioTrackSink sink : sinks) {
ByteBuffer byteBuffer = ByteBuffer.wrap(audioSamples.getData());
sink.onData(byteBuffer, bitsPerSample, audioSamples.getSampleRate(),
audioSamples.getChannelCount(), numFrames, timestamp);
}
}
}
}
package com.cloudwebrtc.webrtc.audio;
import org.webrtc.audio.JavaAudioDeviceModule;
import java.util.ArrayList;
import java.util.List;
public class PlaybackSamplesReadyCallbackAdapter
implements JavaAudioDeviceModule.PlaybackSamplesReadyCallback {
public PlaybackSamplesReadyCallbackAdapter() {}
List<JavaAudioDeviceModule.PlaybackSamplesReadyCallback> callbacks = new ArrayList<>();
public void addCallback(JavaAudioDeviceModule.PlaybackSamplesReadyCallback callback) {
synchronized (callbacks) {
callbacks.add(callback);
}
}
public void removeCallback(JavaAudioDeviceModule.PlaybackSamplesReadyCallback callback) {
synchronized (callbacks) {
callbacks.remove(callback);
}
}
@Override
public void onWebRtcAudioTrackSamplesReady(JavaAudioDeviceModule.AudioSamples audioSamples) {
for (JavaAudioDeviceModule.PlaybackSamplesReadyCallback callback : callbacks) {
callback.onWebRtcAudioTrackSamplesReady(audioSamples);
}
}
}
package com.cloudwebrtc.webrtc.audio;
import org.webrtc.audio.JavaAudioDeviceModule;
import java.util.ArrayList;
import java.util.List;
public class RecordSamplesReadyCallbackAdapter
implements JavaAudioDeviceModule.SamplesReadyCallback {
public RecordSamplesReadyCallbackAdapter() {}
List<JavaAudioDeviceModule.SamplesReadyCallback> callbacks = new ArrayList<>();
public void addCallback(JavaAudioDeviceModule.SamplesReadyCallback callback) {
synchronized (callbacks) {
callbacks.add(callback);
}
}
public void removeCallback(JavaAudioDeviceModule.SamplesReadyCallback callback) {
synchronized (callbacks) {
callbacks.remove(callback);
}
}
@Override
public void onWebRtcAudioRecordSamplesReady(JavaAudioDeviceModule.AudioSamples audioSamples) {
synchronized (callbacks) {
for (JavaAudioDeviceModule.SamplesReadyCallback callback : callbacks) {
callback.onWebRtcAudioRecordSamplesReady(audioSamples);
}
}
}
}
package com.cloudwebrtc.webrtc.record;
public enum AudioChannel {
INPUT,
OUTPUT
}
\ No newline at end of file
package com.cloudwebrtc.webrtc.record;
import android.annotation.SuppressLint;
import org.webrtc.audio.JavaAudioDeviceModule.SamplesReadyCallback;
import org.webrtc.audio.JavaAudioDeviceModule.AudioSamples;
import java.util.HashMap;
/** JavaAudioDeviceModule allows attaching samples callback only on building
* We don't want to instantiate VideoFileRenderer and codecs at this step
* It's simple dummy class, it does nothing until samples are necessary */
@SuppressWarnings("WeakerAccess")
public class AudioSamplesInterceptor implements SamplesReadyCallback {
@SuppressLint("UseSparseArrays")
protected final HashMap<Integer, SamplesReadyCallback> callbacks = new HashMap<>();
@Override
public void onWebRtcAudioRecordSamplesReady(AudioSamples audioSamples) {
for (SamplesReadyCallback callback : callbacks.values()) {
callback.onWebRtcAudioRecordSamplesReady(audioSamples);
}
}
public void attachCallback(Integer id, SamplesReadyCallback callback) throws Exception {
callbacks.put(id, callback);
}
public void detachCallback(Integer id) {
callbacks.remove(id);
}
}
package com.cloudwebrtc.webrtc.record;
import android.annotation.TargetApi;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioTrack;
import android.os.Build;
import org.webrtc.audio.JavaAudioDeviceModule.AudioSamples;
import org.webrtc.audio.JavaAudioDeviceModule.SamplesReadyCallback;
import java.nio.ByteBuffer;
import androidx.annotation.NonNull;
/**
* Wrapper around audio track
* Intercepts write calls and passes it to callback
* **/
public final class AudioTrackInterceptor extends AudioTrack {
final public AudioTrack originalTrack;
final private SamplesReadyCallback callback;
public AudioTrackInterceptor(@NonNull AudioTrack originalTrack, @NonNull SamplesReadyCallback callback) {
// That just random params, we don't care about object that will be created
super(
AudioManager.STREAM_VOICE_CALL,
44200,
AudioFormat.CHANNEL_OUT_MONO,
AudioFormat.ENCODING_PCM_16BIT,
128,
AudioTrack.MODE_STREAM
);
this.originalTrack = originalTrack;
this.callback = callback;
}
@Override
public int write(@NonNull byte[] audioData, int offsetInBytes, int sizeInBytes) {
callback.onWebRtcAudioRecordSamplesReady(new AudioSamples(
originalTrack.getAudioFormat(),
originalTrack.getChannelCount(),
originalTrack.getSampleRate(),
audioData
));
return originalTrack.write(audioData, offsetInBytes, sizeInBytes);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public int write(@NonNull ByteBuffer audioData, int sizeInBytes, int writeMode) {
byte[] trimmed = new byte[sizeInBytes];
int position = audioData.position();
audioData.get(trimmed, 0, sizeInBytes);
audioData.position(position);
callback.onWebRtcAudioRecordSamplesReady(new AudioSamples(
originalTrack.getAudioFormat(),
originalTrack.getChannelCount(),
originalTrack.getSampleRate(),
trimmed
));
return originalTrack.write(audioData, sizeInBytes, writeMode);
}
/**
* Override all required calls to mimic original track
* https://webrtc.googlesource.com/src/+/master/sdk/android/src/java/org/webrtc/audio/WebRtcAudioTrack.java
* **/
@Override
public int getPlayState() {
return originalTrack.getPlayState();
}
@Override
public void play() throws IllegalStateException {
originalTrack.play();
}
@Override
public void stop() throws IllegalStateException {
originalTrack.stop();
}
@TargetApi(Build.VERSION_CODES.N)
@Override
public int getUnderrunCount() {
return originalTrack.getUnderrunCount();
}
@TargetApi(Build.VERSION_CODES.N)
@Override
public int getBufferCapacityInFrames() {
return originalTrack.getBufferCapacityInFrames();
}
@TargetApi(Build.VERSION_CODES.M)
@Override
public int getBufferSizeInFrames() {
return originalTrack.getBufferSizeInFrames();
}
@Override
public void release() {
originalTrack.release();
}
@Override
public int getPlaybackHeadPosition() {
return originalTrack.getPlaybackHeadPosition();
}
}
package com.cloudwebrtc.webrtc.record;
class EncoderConfig {
final int width;
final int height;
final int bitrate;
final int profile;
EncoderConfig(int width, int height, int bitrate, int profile) {
this.width = width;
this.height = height;
this.bitrate = bitrate;
this.profile = profile;
}
@Override
public String toString() {
return width + "x" + height + ", bitrate: " + bitrate + ", profile: " + profile;
}
}
\ No newline at end of file
package com.cloudwebrtc.webrtc.record;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.ImageFormat;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.graphics.YuvImage;
import android.os.Handler;
import android.os.Looper;
import org.webrtc.VideoFrame;
import org.webrtc.VideoSink;
import org.webrtc.VideoTrack;
import org.webrtc.YuvHelper;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Arrays;
import io.flutter.plugin.common.MethodChannel;
public class FrameCapturer implements VideoSink {
private final VideoTrack videoTrack;
private File file;
private final MethodChannel.Result callback;
private boolean gotFrame = false;
public FrameCapturer(VideoTrack track, File file, MethodChannel.Result callback) {
videoTrack = track;
this.file = file;
this.callback = callback;
track.addSink(this);
}
@Override
public void onFrame(VideoFrame videoFrame) {
if (gotFrame)
return;
gotFrame = true;
videoFrame.retain();
VideoFrame.Buffer buffer = videoFrame.getBuffer();
VideoFrame.I420Buffer i420Buffer = buffer.toI420();
ByteBuffer y = i420Buffer.getDataY();
ByteBuffer u = i420Buffer.getDataU();
ByteBuffer v = i420Buffer.getDataV();
int width = i420Buffer.getWidth();
int height = i420Buffer.getHeight();
int[] strides = new int[] {
i420Buffer.getStrideY(),
i420Buffer.getStrideU(),
i420Buffer.getStrideV()
};
final int chromaWidth = (width + 1) / 2;
final int chromaHeight = (height + 1) / 2;
final int minSize = width * height + chromaWidth * chromaHeight * 2;
ByteBuffer yuvBuffer = ByteBuffer.allocateDirect(minSize);
// NV21 is the same as NV12, only that V and U are stored in the reverse oder
// NV21 (YYYYYYYYY:VUVU)
// NV12 (YYYYYYYYY:UVUV)
// Therefore we can use the NV12 helper, but swap the U and V input buffers
YuvHelper.I420ToNV12(y, strides[0], v, strides[2], u, strides[1], yuvBuffer, width, height);
// For some reason the ByteBuffer may have leading 0. We remove them as
// otherwise the
// image will be shifted
byte[] cleanedArray = Arrays.copyOfRange(yuvBuffer.array(), yuvBuffer.arrayOffset(), minSize);
YuvImage yuvImage = new YuvImage(
cleanedArray,
ImageFormat.NV21,
width,
height,
// We omit the strides here. If they were included, the resulting image would
// have its colors offset.
null);
i420Buffer.release();
videoFrame.release();
new Handler(Looper.getMainLooper()).post(() -> {
videoTrack.removeSink(this);
});
try {
if (!file.exists()) {
//noinspection ResultOfMethodCallIgnored
file.getParentFile().mkdirs();
//noinspection ResultOfMethodCallIgnored
file.createNewFile();
}
} catch (IOException io) {
callback.error("IOException", io.getLocalizedMessage(), io);
return;
}
try (FileOutputStream outputStream = new FileOutputStream(file)) {
yuvImage.compressToJpeg(
new Rect(0, 0, width, height),
100,
outputStream
);
switch (videoFrame.getRotation()) {
case 0:
break;
case 90:
case 180:
case 270:
Bitmap original = BitmapFactory.decodeFile(file.toString());
Matrix matrix = new Matrix();
matrix.postRotate(videoFrame.getRotation());
Bitmap rotated = Bitmap.createBitmap(original, 0, 0, original.getWidth(), original.getHeight(), matrix, true);
FileOutputStream rotatedOutputStream = new FileOutputStream(file);
rotated.compress(Bitmap.CompressFormat.JPEG, 100, rotatedOutputStream);
break;
default:
// Rotation is checked to always be 0, 90, 180 or 270 by VideoFrame
throw new RuntimeException("Invalid rotation");
}
callback.success(null);
} catch (IOException io) {
callback.error("IOException", io.getLocalizedMessage(), io);
} catch (IllegalArgumentException iae) {
callback.error("IllegalArgumentException", iae.getLocalizedMessage(), iae);
} finally {
file = null;
}
}
}
package com.cloudwebrtc.webrtc.record;
import androidx.annotation.Nullable;
import android.util.Log;
import com.cloudwebrtc.webrtc.utils.EglUtils;
import org.webrtc.VideoTrack;
import java.io.File;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class MediaRecorderImpl {
private final Integer id;
private final VideoTrack videoTrack;
private final AudioSamplesInterceptor audioInterceptor;
private VideoFileRenderer videoFileRenderer;
private AudioFileRenderer audioFileRenderer;
private boolean isRunning = false;
private File recordFile;
public MediaRecorderImpl(Integer id, @Nullable VideoTrack videoTrack,
@Nullable AudioSamplesInterceptor audioInterceptor) {
this.id = id;
this.videoTrack = videoTrack;
this.audioInterceptor = audioInterceptor;
}
public void startRecording(File file) throws Exception {
recordFile = file;
if (isRunning)
return;
isRunning = true;
// noinspection ResultOfMethodCallIgnored
file.getParentFile().mkdirs();
if (videoTrack != null) {
videoFileRenderer = new VideoFileRenderer(
file.getAbsolutePath(),
EglUtils.getRootEglBaseContext(),
audioInterceptor != null);
videoTrack.addSink(videoFileRenderer);
if (audioInterceptor != null)
audioInterceptor.attachCallback(id, videoFileRenderer);
} else {
Log.d(TAG, "Video track is null - checking for audio-only recording");
if (audioInterceptor != null) {
// Audio-only recording implementation
audioFileRenderer = new AudioFileRenderer(file.getAbsolutePath());
audioInterceptor.attachCallback(id, audioFileRenderer);
} else {
throw new Exception("Both video track and audio interceptor are null - cannot record");
}
}
}
public File getRecordFile() {
return recordFile;
}
private final ExecutorService releaseExecutor = Executors.newSingleThreadExecutor();
public void stopRecording(Runnable onStopped) {
isRunning = false;
if (audioInterceptor != null)
audioInterceptor.detachCallback(id);
if (videoTrack != null && videoFileRenderer != null) {
videoTrack.removeSink(videoFileRenderer);
releaseExecutor.submit(() -> {
videoFileRenderer.release();
videoFileRenderer = null;
if (onStopped != null)
onStopped.run();
releaseExecutor.shutdown(); // libera o executor
});
} else {
if (onStopped != null)
onStopped.run();
releaseExecutor.shutdown();
}
if (audioFileRenderer != null) {
audioFileRenderer.release();
audioFileRenderer = null;
}
}
private static final String TAG = "MediaRecorderImpl";
}
package com.cloudwebrtc.webrtc.record;
import org.webrtc.audio.JavaAudioDeviceModule;
import org.webrtc.audio.WebRtcAudioTrackUtils;
public class OutputAudioSamplesInterceptor extends AudioSamplesInterceptor {
private final JavaAudioDeviceModule audioDeviceModule;
public OutputAudioSamplesInterceptor(JavaAudioDeviceModule audioDeviceModule) {
super();
this.audioDeviceModule = audioDeviceModule;
}
@Override
public void attachCallback(Integer id, JavaAudioDeviceModule.SamplesReadyCallback callback) throws Exception {
if (callbacks.isEmpty())
WebRtcAudioTrackUtils.attachOutputCallback(this, audioDeviceModule);
super.attachCallback(id, callback);
}
@Override
public void detachCallback(Integer id) {
super.detachCallback(id);
if (callbacks.isEmpty())
WebRtcAudioTrackUtils.detachOutputCallback(audioDeviceModule);
}
}
package com.cloudwebrtc.webrtc.utils;
import android.os.Looper;
import android.os.Handler;
import io.flutter.plugin.common.MethodChannel;
public final class AnyThreadResult implements MethodChannel.Result {
final private MethodChannel.Result result;
final private Handler handler = new Handler(Looper.getMainLooper());
public AnyThreadResult(MethodChannel.Result result) {
this.result = result;
}
@Override
public void success(Object o) {
post(()->result.success(o));
}
@Override
public void error(String s, String s1, Object o) {
post(()->result.error(s, s1, o));
}
@Override
public void notImplemented() {
post(result::notImplemented);
}
private void post(Runnable r) {
if(Looper.getMainLooper() == Looper.myLooper()){
r.run();
}else{
handler.post(r);
}
}
}
package com.cloudwebrtc.webrtc.utils;
import android.os.Handler;
import android.os.Looper;
import io.flutter.plugin.common.EventChannel;
public final class AnyThreadSink implements EventChannel.EventSink {
final private EventChannel.EventSink eventSink;
final private Handler handler = new Handler(Looper.getMainLooper());
public AnyThreadSink(EventChannel.EventSink eventSink) {
this.eventSink = eventSink;
}
@Override
public void success(Object o) {
post(()->eventSink.success(o));
}
@Override
public void error(String s, String s1, Object o) {
post(()->eventSink.error(s, s1, o));
}
@Override
public void endOfStream() {
post(eventSink::endOfStream);
}
private void post(Runnable r) {
if(Looper.getMainLooper() == Looper.myLooper()){
r.run();
}else{
handler.post(r);
}
}
}
package com.cloudwebrtc.webrtc.utils;
public interface Callback {
void invoke(Object... args);
}
package com.cloudwebrtc.webrtc.utils;
import java.util.ArrayList;
import java.util.Map;
public class ConstraintsArray {
final private ArrayList<Object> mArray;
public ConstraintsArray(){
this.mArray = new ArrayList<>();
}
public ConstraintsArray(ArrayList<Object> array){
this.mArray = array;
}
public int size(){
return mArray.size();
}
public boolean isNull(int index){
return mArray.get(index) == null;
}
public boolean getBoolean(int index){
return (Boolean) mArray.get(index);
}
public double getDouble(int index){
return (double) mArray.get(index);
}
public int getInt(int index){
return (int) mArray.get(index);
}
public String getString(int index){
return (String) mArray.get(index);
}
public Byte[] getByte(int index){
return (Byte[]) mArray.get(index);
}
public ConstraintsArray getArray(int index){
return new ConstraintsArray((ArrayList<Object>)mArray.get(index));
}
public ConstraintsMap getMap(int index){
return new ConstraintsMap((Map<String, Object>) mArray.get(index));
}
public ObjectType getType(int index) {
Object object = mArray.get(index);
if (object == null) {
return ObjectType.Null;
} else if (object instanceof Boolean) {
return ObjectType.Boolean;
} else if (object instanceof Double ||
object instanceof Float ||
object instanceof Integer) {
return ObjectType.Number;
} else if (object instanceof String) {
return ObjectType.String;
} else if (object instanceof ArrayList) {
return ObjectType.Array;
} else if (object instanceof Map) {
return ObjectType.Map;
} else if (object instanceof Byte) {
return ObjectType.Byte;
}
return ObjectType.Null;
}
public ArrayList<Object> toArrayList(){
return mArray;
}
public void pushNull(){
mArray.add(null);
}
public void pushBoolean(boolean value){
mArray.add(value);
}
public void pushDouble(double value){
mArray.add(value);
}
public void pushInt(int value){
mArray.add(value);
}
public void pushString(String value){
mArray.add(value);
}
public void pushArray(ConstraintsArray array){
mArray.add(array.toArrayList());
}
public void pushByte(byte[] value){
mArray.add(value);
}
public void pushMap(ConstraintsMap map){
mArray.add(map.toMap());
}
}
package com.cloudwebrtc.webrtc.utils;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
public class ConstraintsMap {
private final Map<String, Object> mMap;
public ConstraintsMap(){
mMap = new HashMap<String,Object>();
}
public ConstraintsMap(Map<String, Object> map){
this.mMap = map;
}
public Map<String, Object> toMap() {
return mMap;
}
public boolean hasKey(String name){
return this.mMap.containsKey(name);
}
public boolean isNull(String name){
return mMap.get(name) == null;
}
public boolean getBoolean(String name){
return (boolean) mMap.get(name);
}
public double getDouble(String name){
return (double) mMap.get(name);
}
public int getInt(String name) {
if(getType(name) == ObjectType.String) {
return Integer.parseInt(((String)mMap.get(name)));
}
return (int) mMap.get(name);
}
public String getString(String name){
return (String) mMap.get(name);
}
public ConstraintsMap getMap(String name){
Object value = mMap.get(name);
if (value == null) {
return null;
}
return new ConstraintsMap((Map<String, Object>) value);
}
public ObjectType getType(String name) {
Object value = mMap.get(name);
if (value == null) {
return ObjectType.Null;
} else if (value instanceof Number) {
return ObjectType.Number;
} else if (value instanceof String) {
return ObjectType.String;
} else if (value instanceof Boolean) {
return ObjectType.Boolean;
} else if (value instanceof Map) {
return ObjectType.Map;
} else if (value instanceof ArrayList) {
return ObjectType.Array;
} else if (value instanceof Byte) {
return ObjectType.Byte;
} else {
throw new IllegalArgumentException("Invalid value " + value + " for key " + name +
"contained in ConstraintsMap");
}
}
public void putBoolean(String key, boolean value) {
mMap.put(key, value);
}
public void putDouble(String key, double value) {
mMap.put(key, value);
}
public void putInt(String key, int value) {
mMap.put(key, value);
}
public void putLong(String key, long value) {
mMap.put(key, value);
}
public void putString(String key, String value) {
mMap.put(key, value);
}
public void putByte(String key, byte[] value) {
mMap.put(key, value);
}
public void putNull(String key) {
mMap.put(key, null);
}
public void putMap(String key, Map<String, Object> value) {
mMap.put(key, value);
}
public void merge(Map<String, Object> value) {
mMap.putAll(value);
}
public void putArray(String key, ArrayList<Object> value) {
mMap.put(key, value);
}
public ConstraintsArray getArray(String name){
Object value = mMap.get(name);
if (value == null) {
return null;
}
return new ConstraintsArray((ArrayList<Object>) value);
}
public ArrayList<Object> getListArray(String name){
return (ArrayList<Object>) mMap.get(name);
}
@Override
public String toString() {
return "ConstraintsMap{" +
"mMap=" + mMap +
'}';
}
}
package com.cloudwebrtc.webrtc.utils;
import android.os.Build;
import org.webrtc.EglBase;
public class EglUtils {
/**
* The root {@link EglBase} instance shared by the entire application for
* the sake of reducing the utilization of system resources (such as EGL
* contexts).
*/
private static EglBase rootEglBase;
/**
* Lazily creates and returns the one and only {@link EglBase} which will
* serve as the root for all contexts that are needed.
*/
public static synchronized EglBase getRootEglBase() {
if (rootEglBase == null) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
rootEglBase = EglBase.createEgl10(EglBase.CONFIG_PLAIN);
else
rootEglBase = EglBase.create();
}
return rootEglBase;
}
public static EglBase.Context getRootEglBaseContext() {
EglBase eglBase = getRootEglBase();
return eglBase == null ? null : eglBase.getEglBaseContext();
}
}
package com.cloudwebrtc.webrtc.utils;
import android.util.Log;
import java.util.List;
import java.util.Map.Entry;
import org.webrtc.MediaConstraints;
import org.webrtc.MediaConstraints.KeyValuePair;
public class MediaConstraintsUtils {
static public final String TAG = "MediaConstraintsUtils";
/**
* Parses mandatory and optional "GUM" constraints described by a specific
* <tt>ConstraintsMap</tt>.
*
* @param constraints A <tt>ConstraintsMap</tt> which represents a JavaScript object specifying
* the constraints to be parsed into a
* <tt>MediaConstraints</tt> instance.
* @return A new <tt>MediaConstraints</tt> instance initialized with the mandatory and optional
* constraint keys and values specified by
* <tt>constraints</tt>.
*/
public static MediaConstraints parseMediaConstraints(ConstraintsMap constraints) {
MediaConstraints mediaConstraints = new MediaConstraints();
// TODO: change getUserMedia constraints format to support new syntax
// constraint format seems changed, and there is no mandatory any more.
// and has a new syntax/attrs to specify resolution
// should change `parseConstraints()` according
// see: https://www.w3.org/TR/mediacapture-streams/#idl-def-MediaTrackConstraints
if (constraints.hasKey("mandatory")
&& constraints.getType("mandatory") == ObjectType.Map) {
parseConstraints(constraints.getMap("mandatory"),
mediaConstraints.mandatory);
} else {
Log.d(TAG, "mandatory constraints are not a map");
}
if (constraints.hasKey("optional")
&& constraints.getType("optional") == ObjectType.Array) {
ConstraintsArray optional = constraints.getArray("optional");
for (int i = 0, size = optional.size(); i < size; i++) {
if (optional.getType(i) == ObjectType.Map) {
parseConstraints(
optional.getMap(i),
mediaConstraints.optional);
}
}
} else {
Log.d(TAG, "optional constraints are not an array");
}
return mediaConstraints;
}
/**
* Parses a constraint set specified in the form of a JavaScript object into a specific
* <tt>List</tt> of <tt>MediaConstraints.KeyValuePair</tt>s.
*
* @param src The constraint set in the form of a JavaScript object to parse.
* @param dst The <tt>List</tt> of <tt>MediaConstraints.KeyValuePair</tt>s into which the
* specified <tt>src</tt> is to be parsed.
*/
private static void parseConstraints(
ConstraintsMap src,
List<KeyValuePair> dst) {
for (Entry<String, Object> entry : src.toMap().entrySet()) {
String key = entry.getKey();
String value = getMapStrValue(src, entry.getKey());
dst.add(new KeyValuePair(key, value));
}
}
private static String getMapStrValue(ConstraintsMap map, String key) {
if (!map.hasKey(key)) {
return null;
}
ObjectType type = map.getType(key);
switch (type) {
case Boolean:
return String.valueOf(map.getBoolean(key));
case Number:
// Don't know how to distinguish between Int and Double from
// ReadableType.Number. 'getInt' will fail on double value,
// while 'getDouble' works for both.
// return String.valueOf(map.getInt(key));
return String.valueOf(map.getDouble(key));
case String:
return map.getString(key);
default:
return null;
}
}
}
package com.cloudwebrtc.webrtc.utils;
public enum ObjectType {
Null,
Boolean,
Number,
String,
Map,
Array,
Byte
}
package com.cloudwebrtc.webrtc.utils;
import android.app.Activity;
import android.app.Fragment;
import android.app.FragmentTransaction;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.ResultReceiver;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.core.app.ActivityCompat;
import java.util.ArrayList;
/** Helper module for dealing with dynamic permissions, introduced in Android M (API level 23). */
public class PermissionUtils {
/**
* Constants for internal fields in the <tt>Bundle</tt> exchanged between the activity requesting
* the permissions and the auxiliary activity we spawn for this purpose.
*/
private static final String GRANT_RESULTS = "GRANT_RESULT";
private static final String PERMISSIONS = "PERMISSION";
private static final String REQUEST_CODE = "REQUEST_CODE";
private static final String RESULT_RECEIVER = "RESULT_RECEIVER";
/** Incrementing counter for permission requests. Each request must have a unique numeric code. */
private static int requestCode;
private static void requestPermissions(
Context context, Activity activity, String[] permissions, ResultReceiver resultReceiver) {
// Ask the Context whether we have already been granted the requested
// permissions.
int size = permissions.length;
int[] grantResults = new int[size];
boolean permissionsGranted = true;
for (int i = 0; i < size; ++i) {
int grantResult;
// No need to ask for permission on pre-Marshmallow
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M)
grantResult = PackageManager.PERMISSION_GRANTED;
else if (activity != null){
grantResult = activity.checkSelfPermission(permissions[i]);
} else {
grantResult = ActivityCompat.checkSelfPermission(context, permissions[i]);
}
grantResults[i] = grantResult;
if (grantResult != PackageManager.PERMISSION_GRANTED) {
permissionsGranted = false;
}
}
// Obviously, if the requested permissions have already been granted,
// there is nothing to ask the user about. On the other hand, if there
// is no Activity or the runtime permissions are not supported, there is
// no way to ask the user to grant us the denied permissions.
int requestCode = ++PermissionUtils.requestCode;
if (permissionsGranted
// Here we test for the target SDK version with which *the app*
// was compiled. If we use Build.VERSION.SDK_INT that would give
// us the API version of the device itself, not the version the
// app was compiled for. When compiled for API level < 23 we
// must still use old permissions model, regardless of the
// Android version on the device.
|| Build.VERSION.SDK_INT < Build.VERSION_CODES.M
|| context.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.M) {
send(resultReceiver, requestCode, permissions, grantResults);
return;
}
Bundle args = new Bundle();
args.putInt(REQUEST_CODE, requestCode);
args.putParcelable(RESULT_RECEIVER, resultReceiver);
args.putStringArray(PERMISSIONS, permissions);
RequestPermissionsFragment fragment = new RequestPermissionsFragment();
fragment.setArguments(args);
if(activity != null){
FragmentTransaction transaction =
activity
.getFragmentManager()
.beginTransaction()
.add(fragment, fragment.getClass().getName() + "-" + requestCode);
try {
transaction.commit();
} catch (IllegalStateException ise) {
// Context is a Plugin, just send result back.
send(resultReceiver, requestCode, permissions, grantResults);
}
}
}
public static void requestPermissions(
final Context context,
final Activity activity,
final String[] permissions,
final Callback callback) {
requestPermissions(
context,
activity,
permissions,
new ResultReceiver(new Handler(Looper.getMainLooper())) {
@Override
protected void onReceiveResult(int resultCode, Bundle resultData) {
callback.invoke(
resultData.getStringArray(PERMISSIONS), resultData.getIntArray(GRANT_RESULTS));
}
});
}
private static void send(
ResultReceiver resultReceiver, int requestCode, String[] permissions, int[] grantResults) {
Bundle resultData = new Bundle();
resultData.putStringArray(PERMISSIONS, permissions);
resultData.putIntArray(GRANT_RESULTS, grantResults);
resultReceiver.send(requestCode, resultData);
}
public interface Callback {
void invoke(String[] permissions, int[] grantResults);
}
/**
* Helper activity for requesting permissions. Android only allows requesting permissions from an
* activity and the result is reported in the <tt>onRequestPermissionsResult</tt> method. Since
* this package is a library we create an auxiliary activity and communicate back the results
* using a <tt>ResultReceiver</tt>.
*/
@RequiresApi(api = VERSION_CODES.M)
public static class RequestPermissionsFragment extends Fragment {
private void checkSelfPermissions(boolean requestPermissions) {
// Figure out which of the requested permissions are actually denied
// because we do not want to ask about the granted permissions
// (which Android supports).
Bundle args = getArguments();
String[] permissions = args.getStringArray(PERMISSIONS);
int size = permissions.length;
Activity activity = getActivity();
int[] grantResults = new int[size];
ArrayList<String> deniedPermissions = new ArrayList<>();
for (int i = 0; i < size; ++i) {
String permission = permissions[i];
int grantResult;
// No need to ask for permission on pre-Marshmallow
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M)
grantResult = PackageManager.PERMISSION_GRANTED;
else grantResult = activity.checkSelfPermission(permission);
grantResults[i] = grantResult;
if (grantResult != PackageManager.PERMISSION_GRANTED) {
deniedPermissions.add(permission);
}
}
int requestCode = args.getInt(REQUEST_CODE, 0);
if (deniedPermissions.isEmpty() || !requestPermissions) {
// All permissions have already been granted or we cannot ask
// the user about the denied ones.
finish();
send(args.getParcelable(RESULT_RECEIVER), requestCode, permissions, grantResults);
} else {
// Ask the user about the denied permissions.
requestPermissions(
deniedPermissions.toArray(new String[deniedPermissions.size()]), requestCode);
}
}
private void finish() {
Activity activity = getActivity();
if (activity != null) {
activity.getFragmentManager().beginTransaction().remove(this).commitAllowingStateLoss();
}
}
@Override
public void onRequestPermissionsResult(
int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
Bundle args = getArguments();
if (args.getInt(REQUEST_CODE, 0) != requestCode) {
return;
}
// XXX The super's documentation says: It is possible that the
// permissions request interaction with the user is interrupted. In
// this case you will receive empty permissions and results arrays
// which should be treated as a cancellation.
if (permissions.length == 0 || grantResults.length == 0) {
// The getUserMedia algorithm does not define a way to cancel
// the invocation so we have to redo the permission request.
finish();
PermissionUtils.requestPermissions(
getContext(),
getActivity(),
args.getStringArray(PERMISSIONS),
(ResultReceiver) args.getParcelable(RESULT_RECEIVER));
} else {
// We did not ask for all requested permissions, just the denied
// ones. But when we send the result, we have to answer about
// all requested permissions.
checkSelfPermissions(/* requestPermissions */ false);
}
}
@Override
public void onResume() {
super.onResume();
checkSelfPermissions(/* requestPermissions */ true);
}
}
}
package com.cloudwebrtc.webrtc.utils;
import androidx.annotation.Nullable;
import org.webrtc.PeerConnection;
public class Utils {
@Nullable
static public String iceConnectionStateString(PeerConnection.IceConnectionState iceConnectionState) {
switch (iceConnectionState) {
case NEW:
return "new";
case CHECKING:
return "checking";
case CONNECTED:
return "connected";
case COMPLETED:
return "completed";
case FAILED:
return "failed";
case DISCONNECTED:
return "disconnected";
case CLOSED:
return "closed";
}
return null;
}
@Nullable
static public String iceGatheringStateString(PeerConnection.IceGatheringState iceGatheringState) {
switch (iceGatheringState) {
case NEW:
return "new";
case GATHERING:
return "gathering";
case COMPLETE:
return "complete";
}
return null;
}
@Nullable
static public String signalingStateString(PeerConnection.SignalingState signalingState) {
switch (signalingState) {
case STABLE:
return "stable";
case HAVE_LOCAL_OFFER:
return "have-local-offer";
case HAVE_LOCAL_PRANSWER:
return "have-local-pranswer";
case HAVE_REMOTE_OFFER:
return "have-remote-offer";
case HAVE_REMOTE_PRANSWER:
return "have-remote-pranswer";
case CLOSED:
return "closed";
}
return null;
}
@Nullable
static public String connectionStateString(PeerConnection.PeerConnectionState connectionState) {
switch (connectionState) {
case NEW:
return "new";
case CONNECTING:
return "connecting";
case CONNECTED:
return "connected";
case DISCONNECTED:
return "disconnected";
case FAILED:
return "failed";
case CLOSED:
return "closed";
}
return null;
}
}
\ No newline at end of file
package com.cloudwebrtc.webrtc.video;
import androidx.annotation.Nullable;
import com.cloudwebrtc.webrtc.LocalTrack;
import org.webrtc.VideoFrame;
import org.webrtc.VideoProcessor;
import org.webrtc.VideoSink;
import org.webrtc.VideoTrack;
import java.util.ArrayList;
import java.util.List;
public class LocalVideoTrack extends LocalTrack implements VideoProcessor {
public interface ExternalVideoFrameProcessing {
/**
* Process a video frame.
* @param frame
* @return The processed video frame.
*/
public abstract VideoFrame onFrame(VideoFrame frame);
}
public LocalVideoTrack(VideoTrack videoTrack) {
super(videoTrack);
}
List<ExternalVideoFrameProcessing> processors = new ArrayList<>();
public void addProcessor(ExternalVideoFrameProcessing processor) {
synchronized (processors) {
processors.add(processor);
}
}
public void removeProcessor(ExternalVideoFrameProcessing processor) {
synchronized (processors) {
processors.remove(processor);
}
}
private VideoSink sink = null;
@Override
public void setSink(@Nullable VideoSink videoSink) {
sink = videoSink;
}
@Override
public void onCapturerStarted(boolean b) {}
@Override
public void onCapturerStopped() {}
@Override
public void onFrameCaptured(VideoFrame videoFrame) {
if (sink != null) {
synchronized (processors) {
for (ExternalVideoFrameProcessing processor : processors) {
videoFrame = processor.onFrame(videoFrame);
}
}
sink.onFrame(videoFrame);
}
}
}
package com.cloudwebrtc.webrtc.video;
import org.webrtc.VideoCapturer;
public class VideoCapturerInfo {
public VideoCapturer capturer;
public int width;
public int height;
public int fps;
public boolean isScreenCapture = false;
public String cameraName;
}
\ No newline at end of file
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package com.cloudwebrtc.webrtc.video.camera;
import android.annotation.TargetApi;
import android.graphics.Rect;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.params.MeteringRectangle;
import android.os.Build;
import android.util.Size;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import io.flutter.embedding.engine.systemchannels.PlatformChannel;
import java.util.Arrays;
/**
* Utility class offering functions to calculate values regarding the camera boundaries.
*
* <p>The functions are used to calculate focus and exposure settings.
*/
public final class CameraRegionUtils {
@NonNull
public static Size getCameraBoundaries(
@NonNull CameraCharacteristics cameraCharacteristics, @NonNull CaptureRequest.Builder requestBuilder) {
if (SdkCapabilityChecker.supportsDistortionCorrection()
&& supportsDistortionCorrection(cameraCharacteristics)) {
// Get the current distortion correction mode.
Integer distortionCorrectionMode =
requestBuilder.get(CaptureRequest.DISTORTION_CORRECTION_MODE);
// Return the correct boundaries depending on the mode.
android.graphics.Rect rect;
if (distortionCorrectionMode == null
|| distortionCorrectionMode == CaptureRequest.DISTORTION_CORRECTION_MODE_OFF) {
rect = getSensorInfoPreCorrectionActiveArraySize(cameraCharacteristics);
} else {
rect = getSensorInfoActiveArraySize(cameraCharacteristics);
}
return SizeFactory.create(rect.width(), rect.height());
} else {
// No distortion correction support.
return getSensorInfoPixelArraySize(cameraCharacteristics);
}
}
@TargetApi(Build.VERSION_CODES.P)
private static boolean supportsDistortionCorrection(CameraCharacteristics cameraCharacteristics) {
int[] availableDistortionCorrectionModes = getDistortionCorrectionAvailableModes(cameraCharacteristics);
if (availableDistortionCorrectionModes == null) {
availableDistortionCorrectionModes = new int[0];
}
long nonOffModesSupported =
Arrays.stream(availableDistortionCorrectionModes)
.filter((value) -> value != CaptureRequest.DISTORTION_CORRECTION_MODE_OFF)
.count();
return nonOffModesSupported > 0;
}
static public int[] getDistortionCorrectionAvailableModes(CameraCharacteristics cameraCharacteristics) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
return cameraCharacteristics.get(CameraCharacteristics.DISTORTION_CORRECTION_AVAILABLE_MODES);
}
return null;
}
public static Rect getSensorInfoActiveArraySize(CameraCharacteristics cameraCharacteristics) {
return cameraCharacteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE);
}
public static Size getSensorInfoPixelArraySize(CameraCharacteristics cameraCharacteristics) {
return cameraCharacteristics.get(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE);
}
@NonNull
public static Rect getSensorInfoPreCorrectionActiveArraySize(CameraCharacteristics cameraCharacteristics) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return cameraCharacteristics.get(
CameraCharacteristics.SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE);
}
return getSensorInfoActiveArraySize(cameraCharacteristics);
}
public static Integer getControlMaxRegionsAutoExposure(CameraCharacteristics cameraCharacteristics) {
return cameraCharacteristics.get(CameraCharacteristics.CONTROL_MAX_REGIONS_AE);
}
/**
* Converts a point into a {@link MeteringRectangle} with the supplied coordinates as the center
* point.
*
* <p>Since the Camera API (due to cross-platform constraints) only accepts a point when
* configuring a specific focus or exposure area and Android requires a rectangle to configure
* these settings there is a need to convert the point into a rectangle. This method will create
* the required rectangle with an arbitrarily size that is a 10th of the current viewport and the
* coordinates as the center point.
*
* @param boundaries - The camera boundaries to calculate the metering rectangle for.
* @param x x - 1 >= coordinate >= 0.
* @param y y - 1 >= coordinate >= 0.
* @return The dimensions of the metering rectangle based on the supplied coordinates and
* boundaries.
*/
@NonNull
public static MeteringRectangle convertPointToMeteringRectangle(
@NonNull Size boundaries,
double x,
double y,
@NonNull PlatformChannel.DeviceOrientation orientation) {
assert (boundaries.getWidth() > 0 && boundaries.getHeight() > 0);
assert (x >= 0 && x <= 1);
assert (y >= 0 && y <= 1);
// Rotate the coordinates to match the device orientation.
double oldX = x, oldY = y;
switch (orientation) {
case PORTRAIT_UP: // 90 ccw.
y = 1 - oldX;
x = oldY;
break;
case PORTRAIT_DOWN: // 90 cw.
x = 1 - oldY;
y = oldX;
break;
case LANDSCAPE_LEFT:
// No rotation required.
break;
case LANDSCAPE_RIGHT: // 180.
x = 1 - x;
y = 1 - y;
break;
}
// Interpolate the target coordinate.
int targetX = (int) Math.round(x * ((double) (boundaries.getWidth() - 1)));
int targetY = (int) Math.round(y * ((double) (boundaries.getHeight() - 1)));
// Determine the dimensions of the metering rectangle (10th of the viewport).
int targetWidth = (int) Math.round(((double) boundaries.getWidth()) / 10d);
int targetHeight = (int) Math.round(((double) boundaries.getHeight()) / 10d);
// Adjust target coordinate to represent top-left corner of metering rectangle.
targetX -= targetWidth / 2;
targetY -= targetHeight / 2;
// Adjust target coordinate as to not fall out of bounds.
if (targetX < 0) {
targetX = 0;
}
if (targetY < 0) {
targetY = 0;
}
int maxTargetX = boundaries.getWidth() - 1 - targetWidth;
int maxTargetY = boundaries.getHeight() - 1 - targetHeight;
if (targetX > maxTargetX) {
targetX = maxTargetX;
}
if (targetY > maxTargetY) {
targetY = maxTargetY;
}
// Build the metering rectangle.
return MeteringRectangleFactory.create(targetX, targetY, targetWidth, targetHeight, 1);
}
/** Factory class that assists in creating a {@link MeteringRectangle} instance. */
static class MeteringRectangleFactory {
/**
* Creates a new instance of the {@link MeteringRectangle} class.
*
* <p>This method is visible for testing purposes only and should never be used outside this *
* class.
*
* @param x coordinate >= 0.
* @param y coordinate >= 0.
* @param width width >= 0.
* @param height height >= 0.
* @param meteringWeight weight between {@value MeteringRectangle#METERING_WEIGHT_MIN} and
* {@value MeteringRectangle#METERING_WEIGHT_MAX} inclusively.
* @return new instance of the {@link MeteringRectangle} class.
* @throws IllegalArgumentException if any of the parameters were negative.
*/
@VisibleForTesting
public static MeteringRectangle create(
int x, int y, int width, int height, int meteringWeight) {
return new MeteringRectangle(x, y, width, height, meteringWeight);
}
}
/** Factory class that assists in creating a {@link Size} instance. */
static class SizeFactory {
/**
* Creates a new instance of the {@link Size} class.
*
* <p>This method is visible for testing purposes only and should never be used outside this *
* class.
*
* @param width width >= 0.
* @param height height >= 0.
* @return new instance of the {@link Size} class.
*/
@VisibleForTesting
public static Size create(int width, int height) {
return new Size(width, height);
}
}
}
package com.cloudwebrtc.webrtc.video.camera;
import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.res.Configuration;
import android.view.Display;
import android.view.Surface;
import android.view.WindowManager;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import io.flutter.embedding.engine.systemchannels.PlatformChannel;
import io.flutter.embedding.engine.systemchannels.PlatformChannel.DeviceOrientation;
/**
* Support class to help to determine the media orientation based on the orientation of the device.
*/
public class DeviceOrientationManager {
private static final IntentFilter orientationIntentFilter =
new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED);
private final Activity activity;
private final int sensorOrientation;
private PlatformChannel.DeviceOrientation lastOrientation;
private BroadcastReceiver broadcastReceiver;
/** Factory method to create a device orientation manager. */
@NonNull
public static DeviceOrientationManager create(
@NonNull Activity activity,
int sensorOrientation) {
return new DeviceOrientationManager(activity, sensorOrientation);
}
DeviceOrientationManager(
@NonNull Activity activity,
int sensorOrientation) {
this.activity = activity;
this.sensorOrientation = sensorOrientation;
}
public void start() {
if (broadcastReceiver != null) {
return;
}
broadcastReceiver =
new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
handleUIOrientationChange();
}
};
activity.registerReceiver(broadcastReceiver, orientationIntentFilter);
broadcastReceiver.onReceive(activity, null);
}
/** Stops listening for orientation updates. */
public void stop() {
if (broadcastReceiver == null) {
return;
}
activity.unregisterReceiver(broadcastReceiver);
broadcastReceiver = null;
}
/** @return the last received UI orientation. */
@Nullable
public PlatformChannel.DeviceOrientation getLastUIOrientation() {
return this.lastOrientation;
}
/**
* Handles orientation changes based on change events triggered by the OrientationIntentFilter.
*
* <p>This method is visible for testing purposes only and should never be used outside this
* class.
*/
@VisibleForTesting
void handleUIOrientationChange() {
PlatformChannel.DeviceOrientation orientation = getUIOrientation();
handleOrientationChange(orientation, lastOrientation);
lastOrientation = orientation;
}
@VisibleForTesting
static void handleOrientationChange(
DeviceOrientation newOrientation,
DeviceOrientation previousOrientation) {
}
@SuppressWarnings("deprecation")
@VisibleForTesting
PlatformChannel.DeviceOrientation getUIOrientation() {
final int rotation = getDisplay().getRotation();
final int orientation = activity.getResources().getConfiguration().orientation;
switch (orientation) {
case Configuration.ORIENTATION_PORTRAIT:
if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_90) {
return PlatformChannel.DeviceOrientation.PORTRAIT_UP;
} else {
return PlatformChannel.DeviceOrientation.PORTRAIT_DOWN;
}
case Configuration.ORIENTATION_LANDSCAPE:
if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_90) {
return PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT;
} else {
return PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT;
}
case Configuration.ORIENTATION_SQUARE:
case Configuration.ORIENTATION_UNDEFINED:
default:
return PlatformChannel.DeviceOrientation.PORTRAIT_UP;
}
}
/**
* Calculates the sensor orientation based on the supplied angle.
*
* <p>This method is visible for testing purposes only and should never be used outside this
* class.
*
* @param angle Orientation angle.
* @return The sensor orientation based on the supplied angle.
*/
@VisibleForTesting
PlatformChannel.DeviceOrientation calculateSensorOrientation(int angle) {
final int tolerance = 45;
angle += tolerance;
// Orientation is 0 in the default orientation mode. This is portrait-mode for phones
// and landscape for tablets. We have to compensate for this by calculating the default
// orientation, and apply an offset accordingly.
int defaultDeviceOrientation = getDeviceDefaultOrientation();
if (defaultDeviceOrientation == Configuration.ORIENTATION_LANDSCAPE) {
angle += 90;
}
// Determine the orientation
angle = angle % 360;
return new PlatformChannel.DeviceOrientation[] {
PlatformChannel.DeviceOrientation.PORTRAIT_UP,
PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT,
PlatformChannel.DeviceOrientation.PORTRAIT_DOWN,
PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT,
}
[angle / 90];
}
/**
* Gets the default orientation of the device.
*
* <p>This method is visible for testing purposes only and should never be used outside this
* class.
*
* @return The default orientation of the device.
*/
@VisibleForTesting
int getDeviceDefaultOrientation() {
Configuration config = activity.getResources().getConfiguration();
int rotation = getDisplay().getRotation();
if (((rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180)
&& config.orientation == Configuration.ORIENTATION_LANDSCAPE)
|| ((rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270)
&& config.orientation == Configuration.ORIENTATION_PORTRAIT)) {
return Configuration.ORIENTATION_LANDSCAPE;
} else {
return Configuration.ORIENTATION_PORTRAIT;
}
}
/**
* Gets an instance of the Android {@link android.view.Display}.
*
* <p>This method is visible for testing purposes only and should never be used outside this
* class.
*
* @return An instance of the Android {@link android.view.Display}.
*/
@SuppressWarnings("deprecation")
@VisibleForTesting
Display getDisplay() {
return ((WindowManager) activity.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
}
}
package com.cloudwebrtc.webrtc.video.camera;
import androidx.annotation.Nullable;
/** Represents a point on an x/y axis. */
public class Point {
public final Double x;
public final Double y;
public Point(@Nullable Double x, @Nullable Double y) {
this.x = x;
this.y = y;
}
}
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package com.cloudwebrtc.webrtc.video.camera;
import android.annotation.SuppressLint;
import android.os.Build;
import androidx.annotation.ChecksSdkIntAtLeast;
import androidx.annotation.VisibleForTesting;
/** Abstracts SDK version checks, and allows overriding them in unit tests. */
public class SdkCapabilityChecker {
/** The current SDK version, overridable for testing. */
@SuppressLint("AnnotateVersionCheck")
@VisibleForTesting
public static int SDK_VERSION = Build.VERSION.SDK_INT;
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.P)
public static boolean supportsDistortionCorrection() {
// See https://developer.android.com/reference/android/hardware/camera2/CameraCharacteristics#DISTORTION_CORRECTION_AVAILABLE_MODES
return SDK_VERSION >= Build.VERSION_CODES.P;
}
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.O)
public static boolean supportsEglRecordableAndroid() {
// See https://developer.android.com/reference/android/opengl/EGLExt#EGL_RECORDABLE_ANDROID
return SDK_VERSION >= Build.VERSION_CODES.O;
}
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.S)
public static boolean supportsEncoderProfiles() {
// See https://developer.android.com/reference/android/media/EncoderProfiles
return SDK_VERSION >= Build.VERSION_CODES.S;
}
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.M)
public static boolean supportsMarshmallowNoiseReductionModes() {
// See https://developer.android.com/reference/android/hardware/camera2/CameraCharacteristics#NOISE_REDUCTION_AVAILABLE_NOISE_REDUCTION_MODES
return SDK_VERSION >= Build.VERSION_CODES.M;
}
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.P)
public static boolean supportsSessionConfiguration() {
// See https://developer.android.com/reference/android/hardware/camera2/params/SessionConfiguration
return SDK_VERSION >= Build.VERSION_CODES.P;
}
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.N)
public static boolean supportsVideoPause() {
// See https://developer.android.com/reference/androidx/camera/video/VideoRecordEvent.Pause
return SDK_VERSION >= Build.VERSION_CODES.N;
}
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.R)
public static boolean supportsZoomRatio() {
// See https://developer.android.com/reference/android/hardware/camera2/CaptureRequest#CONTROL_ZOOM_RATIO
return SDK_VERSION >= Build.VERSION_CODES.R;
}
}
/*
* Copyright 2023-2024 LiveKit, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.webrtc;
import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;
/**
* A helper to access package-protected methods used in [Camera2Session]
* <p>
* Note: cameraId as used in the Camera1XXX classes refers to the index within the list of cameras.
*
* @suppress
*/
public class Camera1Helper {
public static int getCameraId(String deviceName) {
return Camera1Enumerator.getCameraIndex(deviceName);
}
@Nullable
public static List<CameraEnumerationAndroid.CaptureFormat> getSupportedFormats(int cameraId) {
return Camera1Enumerator.getSupportedFormats(cameraId);
}
public static Size findClosestCaptureFormat(int cameraId, int width, int height) {
List<CameraEnumerationAndroid.CaptureFormat> formats = getSupportedFormats(cameraId);
List<Size> sizes = new ArrayList<>();
if (formats != null) {
for (CameraEnumerationAndroid.CaptureFormat format : formats) {
sizes.add(new Size(format.width, format.height));
}
}
return CameraEnumerationAndroid.getClosestSupportedSize(sizes, width, height);
}
}
/*
* Copyright 2023-2024 LiveKit, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.webrtc;
import android.hardware.camera2.CameraManager;
import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;
/**
* A helper to access package-protected methods used in [Camera2Session]
* <p>
* Note: cameraId as used in the Camera2XXX classes refers to the id returned
* by [CameraManager.getCameraIdList].
*/
public class Camera2Helper {
@Nullable
public static List<CameraEnumerationAndroid.CaptureFormat> getSupportedFormats(CameraManager cameraManager, @Nullable String cameraId) {
return Camera2Enumerator.getSupportedFormats(cameraManager, cameraId);
}
public static Size findClosestCaptureFormat(CameraManager cameraManager, @Nullable String cameraId, int width, int height) {
List<CameraEnumerationAndroid.CaptureFormat> formats = getSupportedFormats(cameraManager, cameraId);
List<Size> sizes = new ArrayList<>();
if (formats != null) {
for (CameraEnumerationAndroid.CaptureFormat format : formats) {
sizes.add(new Size(format.width, format.height));
}
}
return CameraEnumerationAndroid.getClosestSupportedSize(sizes, width, height);
}
}
package org.webrtc.audio;
import android.media.AudioTrack;
import android.util.Log;
import com.cloudwebrtc.webrtc.record.AudioTrackInterceptor;
import org.webrtc.audio.JavaAudioDeviceModule.SamplesReadyCallback;
import java.lang.reflect.Field;
/**
* Awful hack
* It must be in this package, because WebRtcAudioTrack is package-private
* **/
public abstract class WebRtcAudioTrackUtils {
static private final String TAG = "WebRtcAudioTrackUtils";
public static void attachOutputCallback(
SamplesReadyCallback callback,
JavaAudioDeviceModule audioDeviceModule
) throws NoSuchFieldException, IllegalAccessException, NullPointerException {
Field audioOutputField = audioDeviceModule.getClass().getDeclaredField("audioOutput");
audioOutputField.setAccessible(true);
WebRtcAudioTrack audioOutput = (WebRtcAudioTrack) audioOutputField.get(audioDeviceModule);
Log.w(TAG, "Here is a little hedgehog 🦔");
Field audioTrackField = audioOutput.getClass().getDeclaredField("audioTrack");
audioTrackField.setAccessible(true);
AudioTrack audioTrack = (AudioTrack) audioTrackField.get(audioOutput);
Log.w(TAG, "He is hiding in a forest 🌲🦔🌲");
AudioTrackInterceptor interceptor = new AudioTrackInterceptor(audioTrack, callback);
audioTrackField.set(audioOutput, interceptor);
Log.w(TAG, "Little hedgie in the forest 🌲🌲🌲 but you can't see him");
}
public static void detachOutputCallback(JavaAudioDeviceModule audioDeviceModule) {
try {
Log.w(TAG, "Where did the hedgie gone? Let's find him");
Field audioOutputField = audioDeviceModule.getClass().getDeclaredField("audioOutput");
audioOutputField.setAccessible(true);
WebRtcAudioTrack audioOutput = (WebRtcAudioTrack) audioOutputField.get(audioDeviceModule);
Field audioTrackField = audioOutput.getClass().getDeclaredField("audioTrack");
audioTrackField.setAccessible(true);
AudioTrack audioTrack = (AudioTrack) audioTrackField.get(audioOutput);
if (audioTrack instanceof AudioTrackInterceptor) {
AudioTrackInterceptor interceptor = (AudioTrackInterceptor) audioTrack;
audioTrackField.set(audioOutput, interceptor.originalTrack);
Log.w(TAG, "Here he is 🦔");
} else {
Log.w(TAG, "Hedgie is lost 😢");
}
} catch (Exception e) {
Log.w(TAG, "Failed to detach callback", e);
}
}
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This source diff could not be displayed because it is too large. You can view the blob instead.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This source diff could not be displayed because it is too large. You can view the blob instead.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This source diff could not be displayed because it is too large. You can view the blob instead.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment