我目前正在向我的应用程序添加视频通话(前端使用 FLutter,后端使用 Asp.NET Core Web + SignalR),我正在使用 flutter_webrtc 库。无论我多么努力,我都成功地使发起呼叫的客户端接收远程流,而接听呼叫的客户端看不到发起呼叫的客户端的流
现在基本上对于想要拨打电话的人来说,我将单击另一个页面上的按钮,该按钮将在没有 roomId 的情况下实例化此页面,并且该过程在设置功能中继续,最后通过中心。 对于接收方,他将从另一个集线器(应用程序始终连接到该集线器)接收回调,该集线器将推送带有 roomId 的 PhoneCallPage,并且他必须按下一个按钮来触发 answerCall 函数,该函数将尝试获取报价等等
我的扑动页面:
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_webrtc/flutter_webrtc.dart';
import 'package:getwidget/components/appbar/gf_appbar.dart';
import 'package:scobius/runtime_data.dart';
import 'package:signalr_netcore/signalr_client.dart';
class PhoneCallPage extends StatefulWidget{
final String interlocutor;
final String? roomId;
const PhoneCallPage({required this.interlocutor, this.roomId, super.key});
call(){
}
@override
// ignore: no_logic_in_create_state
State<PhoneCallPage> createState() => PhoneCallPageState(interlocutor, roomId);
}
class PhoneCallPageState extends State<PhoneCallPage>{
String? interlocutor;
String? roomId;
bool callStart = false;
bool isCaller = false;
bool requestSended = false;
PhoneCallPageState(this.interlocutor, this.roomId);
HubConnection commHub = HubConnectionBuilder()
.withUrl('${RuntimeData.serverBaseAddress}/call',
options: HttpConnectionOptions(
accessTokenFactory: () async => await Future.value(RuntimeData.sessionInformations?.token),
transport: HttpTransportType.WebSockets,
))
.withAutomaticReconnect(retryDelays: [2000, 5000, 10000, 20000])
.build();
RTCPeerConnection? pc;
final RTCVideoRenderer _localRenderer = RTCVideoRenderer();
final RTCVideoRenderer _remoteRenderer = RTCVideoRenderer();
MediaStream? localStream;
MediaStream? remoteStream;
late RTCSessionDescription _offer;
final Map<String, dynamic> _configuration = {
'iceServers': [
{
'urls': [
'stun:stun1.l.google.com:19302',
'stun:stun2.l.google.com:19302',
],
},
],
};
@override
void initState() {
setup();
super.initState();
}
@override
void dispose() {
_localRenderer.dispose();
_remoteRenderer.dispose();
localStream!.dispose();
remoteStream!.dispose();
pc?.dispose();
super.dispose();
}
void setup() async{
setState(() {
isCaller = roomId == null;
});
await _localRenderer.initialize();
await _remoteRenderer.initialize();
await _initializeHubConnection();
var stream = await navigator.mediaDevices.getUserMedia(
{'video': true, 'audio': true},
);
setState((){
localStream = stream;
_localRenderer.srcObject = localStream;
});
if(isCaller) await _createRoom();
}
Future<void> _initializeHubConnection() async{
commHub.on('ReceiveOffer', _handleReceiveOffer);
commHub.on('ReceiveAnswer', _handleReceiveAnswer);
commHub.on('ReceiveIceCandidate', _handleReceiveIceCandidate);
commHub.on('ReceiveIceCandidates', _handleReceiveIceCandidates);
commHub.on('ReceiveRoomId', _handleReceiveRoomId);
await commHub.start();
}
Future<void> _createRoom() async{
pc = await createPeerConnection(_configuration);
var offer = await pc?.createOffer();
setState(() {
_offer = offer!;
});
await commHub.invoke('CreateRoom', args : [offer!.sdp!]);
}
void _registerPeerConnectionListeners() {
localStream!.getTracks().forEach((track) async{
await pc?.addTrack(track, localStream!);
});
pc?.onIceCandidate = (candidate) async{
await commHub.invoke('SendIceCandidate', args: [roomId! , candidate.toMap()]);
};
pc?.onAddStream = (stream) {
remoteStream = stream;
};
pc?.onTrack = (event) {
event.streams[0].getTracks().forEach(
(track) => remoteStream?.addTrack(track)
);
};
}
void answerCall() async{
await commHub.invoke('GetOffer', args: [roomId!]);
}
void endCall() async{
Navigator.pop(context);
}
@override
Widget build(BuildContext context) {
if (localStream != null || _localRenderer.srcObject != localStream) {
_localRenderer.srcObject = localStream!;
}
if (remoteStream != null || _remoteRenderer.srcObject != remoteStream) {
_remoteRenderer.srcObject = remoteStream!;
}
return Scaffold(
appBar: GFAppBar(
leading: Container(),
title: Text(interlocutor!),
),
body: Column(
children: [
(
Expanded(
child: callStart ?
Stack(
alignment: Alignment.bottomRight,
children: [
Container(
decoration: const BoxDecoration(color: Colors.black),
child: RTCVideoView(_remoteRenderer)
),
Container(
width: 100,
height: 250,
decoration: const BoxDecoration(color: Colors.black),
child: RTCVideoView(_localRenderer, objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitContain,)
),
])
: const Center(
child: CircleAvatar(
backgroundColor: Colors.grey,
foregroundColor: Colors.white,
radius: 50,
child: Icon(Icons.person, size: 40),
),
)
)),
Padding(
padding: const EdgeInsets.all(12),
child: callStart ? Flex(
direction: Axis.horizontal,
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
onPressed: (){},
icon: const Icon(Icons.video_call)
),
IconButton(
onPressed: (){},
icon: const Icon(Icons.mic_off)
),
IconButton(
onPressed: endCall,
icon: const Icon(Icons.call_end, color: Colors.red,)
),
],
) : Flex(
direction: Axis.horizontal,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
onPressed: answerCall,
icon: const Icon(Icons.call)
),
IconButton(
onPressed: endCall,
icon: const Icon(Icons.call_end, color: Colors.red,)
),
],
),
),
],
),
);
}
void _handleReceiveRoomId(List<Object?>? arguments) async{
roomId = arguments![0] as String;
_registerPeerConnectionListeners();
await pc?.setLocalDescription(_offer);
}
Future<void> _handleReceiveIceCandidate(List<Object?>? arguments) async {
var candidate = arguments![0] as dynamic;
await pc?.addCandidate(RTCIceCandidate(
candidate['candidate'] as String,
candidate['sdpMid'] as String,
candidate['sdpMLineIndex'] as int,
));
if(isCaller && !requestSended) {
setState(() {
requestSended = true;
});
await commHub.invoke('SendOffer', args: [interlocutor!,roomId!]);
}
}
Future<void> _handleReceiveIceCandidates(List<Object?>? arguments) async {
var candidates = arguments![0] as List<dynamic>;
for (var candidate in candidates) {
await pc?.addCandidate(RTCIceCandidate(
candidate['candidate'] as String,
candidate['sdpMid'] as String,
candidate['sdpMLineIndex'] as int,
));
}
}
void _handleReceiveAnswer(List<Object?>? arguments) async{
String sdp = arguments![0] as String;
await pc?.setRemoteDescription(RTCSessionDescription(sdp, 'answer'));
await commHub.invoke('GetIceCandiates', args: [roomId!]);
setState(() {
callStart = true;
});
}
void _handleReceiveOffer(List<Object?>? arguments) async{
String sdp = arguments![0] as String;
pc = await createPeerConnection(_configuration);
_registerPeerConnectionListeners();
await pc?.setRemoteDescription(RTCSessionDescription(sdp, 'offer'));
final answer = await pc?.createAnswer();
await pc?.setLocalDescription(answer!);
setState(() {
callStart = true;
});
await commHub.invoke('GetIceCandiates', args: [roomId!]);
await commHub.invoke('SendAnswer', args: [interlocutor!, answer!.sdp!]);
}
}
以及管理通信的 SignalR 中心:
using Microsoft.AspNetCore.SignalR;
using System.Text.Json;
using ScobiusLibrary.DTOs;
namespace ScobiusServer.Hubs;
public class CommunicationHub(IHubContext<ServerHub> serverHub) : Hub
{
public static List<CallRoom> CallRooms = new();
public async Task CreateRoom(string sdp){
string roomId = $"CallingRoom{CallRooms.Count}";
CallRoom room = new CallRoom{
RoomCreator = Context.UserIdentifier!,
RoomId = roomId,
Offer = sdp
};
CallRooms.Add(room);
Console.WriteLine(JsonSerializer.Serialize(room));
await Clients.Caller.SendAsync("ReceiveRoomId", roomId);
await Groups.AddToGroupAsync(Context.ConnectionId, roomId);
}
public async Task GetOffer(string roomId){
var room = CallRooms.FirstOrDefault(r => r.RoomId == roomId);
if(room == null) return;
await Clients.Caller.SendAsync("ReceiveOffer", room.Offer, room.IceCandidates);
await Groups.AddToGroupAsync(Context.ConnectionId, room.RoomId);
}
public async Task SendOffer(string receiverId, string roomId)
{
var room = CallRooms.First(r => r.RoomId == roomId);
await serverHub.Clients.User(receiverId).SendAsync("ReceiveCallRequest", roomId, room.RoomCreator);
}
public async Task SendAnswer(string roomCreator, string answer)
{
await Clients.User(roomCreator).SendAsync("ReceiveAnswer", answer);
}
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
public async Task SendIceCandidate(string roomId, IceCandidate candidate)
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
{
CallRoom room = CallRooms.First(r => r.RoomId == roomId);
room.IceCandidates.Add(candidate);
Console.WriteLine(JsonSerializer.Serialize(room));
await Clients.Group(roomId).SendAsync("ReceiveIceCandidate", candidate);
}
public async Task GetIceCandiates(string roomId){
CallRoom room = CallRooms.First(r => r.RoomId == roomId);
var candidates = room.IceCandidates;
await Clients.Caller.SendAsync("ReceiveIceCandidates", candidates);
}
public override Task OnDisconnectedAsync(Exception? exception)
{
var room = CallRooms.FirstOrDefault(r => r.RoomCreator == Context.ConnectionId);
if(room != null) CallRooms.Remove(room);
return base.OnDisconnectedAsync(exception);
}
}
我希望我已经提供了足够的信息。顺便说一句,我使用手机和模拟器作为设备
您可以提供任何错误消息吗?