This commit is contained in:
zhutao
2025-11-21 18:21:47 +08:00
parent 9c94ee31fd
commit 5784a0a5d4
32 changed files with 734 additions and 441 deletions

View File

@@ -26,14 +26,10 @@ class HomeViewModel extends ChangeNotifier {
///计算会议时间
int get roomMinutes {
if (roomInfo == null) return 0;
final start = parseTime(roomInfo!.startTime);
final end = parseTime(roomInfo!.endTime);
final start = roomInfo!.startTime;
final end = roomInfo!.endTime;
final s = DateTime.parse('2000-01-01 $start:00');
final e = DateTime.parse('2000-01-01 $end:00');
return e.difference(s).inMinutes;
return end.difference(start).inMinutes;
}
///能否进入房间

View File

@@ -28,13 +28,7 @@ class _TodayCardState extends State<TodayCard> {
permissions: [Permission.microphone, Permission.camera],
onGranted: () {
final vm = context.read<HomeViewModel>();
context.push(
RoutePaths.tRoom,
extra: {
"roomId": vm.roomInfo!.id,
"startTime": vm.roomInfo!.startTime,
},
);
context.push(RoutePaths.tRoom,extra: vm.roomInfo);
},
onDenied: () {
EasyLoading.showError("请开启权限");

View File

@@ -1,17 +1,16 @@
import 'package:app/request/dto/room/room_info_dto.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'controls/top_bar.dart';
import 'widgets/status_view.dart';
import 'viewmodel/students_view_model.dart';
import 'viewmodel/tch_room_vm.dart';
class TRoomPage extends StatefulWidget {
final int roomId;
final String startTime;
final RoomInfoDto roomInfo;
const TRoomPage({
super.key,
required this.roomId,
required this.startTime,
required this.roomInfo,
});
@override
@@ -21,11 +20,10 @@ class TRoomPage extends StatefulWidget {
class _TRoomPageState extends State<TRoomPage> {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider<StudentsViewModel>(
return ChangeNotifierProvider<TchRoomVM>(
create: (BuildContext context) {
return StudentsViewModel(
roomId: widget.roomId,
start: widget.startTime,
return TchRoomVM(
roomInfo: widget.roomInfo,
);
},
child: Scaffold(

View File

@@ -1,3 +1,6 @@
import 'dart:async';
import 'package:app/request/dto/room/room_info_dto.dart';
import 'package:app/request/dto/room/room_user_dto.dart';
import 'package:app/request/dto/room/rtc_token_dto.dart';
import 'package:app/request/websocket/room_protocol.dart';
@@ -7,18 +10,16 @@ import 'package:flutter/cupertino.dart';
import 'type.dart';
class StudentsViewModel extends ChangeNotifier {
StudentsViewModel({required this.roomId, String? start}) {
startTime = parseTime(start!);
class TchRoomVM extends ChangeNotifier {
TchRoomVM({required this.roomInfo, String? start}) {
_startRoom();
}
///
List<RoomUserDto> _students = [];
///id
final int roomId;
late final DateTime startTime;
///
final RoomInfoDto roomInfo;
///id
int activeSId = 0;
@@ -28,7 +29,7 @@ class StudentsViewModel extends ChangeNotifier {
///
bool get canEnterRoom {
final now = DateTime.now();
if (now.isAfter(startTime)) {
if (now.isAfter(parseTime(roomInfo.startTime))) {
return true;
}
return false;
@@ -36,6 +37,8 @@ class StudentsViewModel extends ChangeNotifier {
///websocket管理
final RoomWebSocket _ws = RoomWebSocket();
bool wsConnected = false; // socket连接状态
StreamSubscription<RoomMessage>? _sub;
RtcTokenDto? get rtcToken => _ws.rtcToken;
@@ -43,18 +46,16 @@ class StudentsViewModel extends ChangeNotifier {
void _startRoom() async {
//socket的token没有
if (_ws.wsToken.isEmpty) {
await _ws.initToken(roomId);
await _ws.initToken(roomInfo.id);
}
//
await _ws.connect();
//
_ws.send(RoomCommand.joinRoom);
wsConnected = true;
//ws事件
_ws.stream.listen((msg) {
_sub = _ws.stream.listen((msg) {
//
if (msg.event == RoomEvent.changeUser) {
final list = msg.data['user_list'].map((x) => RoomUserDto.fromJson(x)).toList();
final list = RoomUserDto.listFromJson(msg.data['user_list']);
onStudentChange(list);
} else if ([
RoomEvent.openSpeaker,
@@ -65,8 +66,7 @@ class StudentsViewModel extends ChangeNotifier {
RoomEvent.closeCamera,
RoomEvent.handUp,
].contains(msg.event)) {
onSyncStudentStatus();
//TODO
onSyncStudentItem(RoomUserDto.fromJson(msg.data));
}
});
notifyListeners();
@@ -91,29 +91,31 @@ class StudentsViewModel extends ChangeNotifier {
///
/// - [uId]: id
/// - [action]:
void closeStudentSpeaker({
void closeStudentAction({
required int uId,
required StudentAction action,
}) {
final student = _students.firstWhere((t) => t.userId == uId);
Map<String, int> data = {
Map<String, dynamic> data = {
'target_user_id': uId,
"mute_type": action.value,
};
//
if (action == StudentAction.speaker) {
student.speekerStatus = student.speekerStatus == 0 ? 1 : 0;
data['speeker'] = student.speekerStatus;
bool isOpen = student.speekerStatus == 1;
student.speekerStatus = isOpen ? 0 : 1;
data['is_mute'] = isOpen ? 1 : 0;
} else if (action == StudentAction.camera) {
//
if (student.cameraStatus == 0) return;
student.cameraStatus = 0;
data['camera'] = 0;
data['is_mute'] = 1;
} else if (action == StudentAction.microphone) {
//
if (student.microphoneStatus == 0) return;
student.microphoneStatus = 0;
data['microphone'] = 0;
data['is_mute'] = 1;
}
notifyListeners();
_ws.send(RoomCommand.switchStudentCamera, data);
@@ -143,13 +145,21 @@ class StudentsViewModel extends ChangeNotifier {
notifyListeners();
}
//TODO
void onSyncStudentStatus() {}
///
void onSyncStudentItem(RoomUserDto userInfo) {
final index = _students.indexWhere((t) => t.userId == userInfo.userId);
print(userInfo.toString());
if (index != -1) {
_students[index] = userInfo;
notifyListeners();
}
}
//
@override
void dispose() {
super.dispose();
_sub?.cancel();
_ws.dispose();
}
}

View File

@@ -1,11 +1,15 @@
///老师操作学生的状态、摄像头、扬声器、麦克风
enum StudentAction {
///摄像头
camera,
camera("camera"),
///麦克风
microphone,
microphone("microphone"),
///扬声器
speaker,
speaker("speeker");
final String value;
const StudentAction(this.value);
}

View File

@@ -3,7 +3,7 @@ import 'package:app/config/config.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../viewmodel/students_view_model.dart';
import '../viewmodel/tch_room_vm.dart';
import 'student_item.dart';
class ContentView extends StatefulWidget {
@@ -14,19 +14,24 @@ class ContentView extends StatefulWidget {
}
class _ContentViewState extends State<ContentView> {
// bool isLoading = true;
//声网数据
RtcEngine? _engine;
@override
void initState() {
super.initState();
// _initRtc();
}
void _initRtc() async {
final vm = context.read<StudentsViewModel>();
final vm = context.read<TchRoomVM>();
_engine = createAgoraRtcEngine();
//初始化 RtcEngine设置频道场景为 channelProfileLiveBroadcasting直播场景
await _engine!.initialize(
RtcEngineContext(
appId: Config.swAppId,
channelProfile: ChannelProfileType.channelProfileLiveBroadcasting,
channelProfile: ChannelProfileType.channelProfileCommunication,
),
);
//添加回调
@@ -39,7 +44,8 @@ class _ContentViewState extends State<ContentView> {
// 远端用户或主播加入当前频道回调
onUserJoined: (RtcConnection connection, int remoteUid, int elapsed) {},
// 远端用户或主播离开当前频道回调
onUserOffline: (RtcConnection connection, int remoteUid, UserOfflineReasonType reason) {},
onUserOffline:
(RtcConnection connection, int remoteUid, UserOfflineReasonType reason) {},
),
);
//启动视频模块
@@ -48,7 +54,7 @@ class _ContentViewState extends State<ContentView> {
await _engine!.joinChannel(
token: vm.rtcToken!.token,
channelId: vm.rtcToken!.channel,
uid: int.parse(vm.rtcToken!.uid),
uid: vm.rtcToken!.uid,
options: ChannelMediaOptions(
// 自动订阅所有视频流
autoSubscribeVideo: true,
@@ -64,22 +70,13 @@ class _ContentViewState extends State<ContentView> {
);
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
final vm = context.read<StudentsViewModel>();
if (_engine == null && vm.students.isNotEmpty) {
_initRtc();
}
}
@override
Widget build(BuildContext context) {
return Consumer<StudentsViewModel>(
return Consumer<TchRoomVM>(
builder: (context, vm, _) {
if (vm.students.isEmpty) {
return Center(
child: Text('无学生在场,请通知学生入场'),
child: Text('准备中'),
);
}
//选中的学生
@@ -96,6 +93,7 @@ class _ContentViewState extends State<ContentView> {
Expanded(
child: StudentItem(
user: activeStudent,
engine: _engine,
),
),
SizedBox(
@@ -107,6 +105,7 @@ class _ContentViewState extends State<ContentView> {
height: 250,
child: StudentItem(
user: item,
engine: _engine,
),
);
},

View File

@@ -5,7 +5,7 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'content_view.dart';
import '../viewmodel/students_view_model.dart';
import '../viewmodel/tch_room_vm.dart';
class StatusView extends StatefulWidget {
const StatusView({super.key});
@@ -26,6 +26,8 @@ class _StatusViewState extends State<StatusView> {
void initState() {
super.initState();
_init();
final vm = context.read<TchRoomVM>();
vm.addListener(openRoom);
}
@override
@@ -36,10 +38,11 @@ class _StatusViewState extends State<StatusView> {
}
void _init() {
final vm = context.read<StudentsViewModel>();
//如果房间可以开始
final vm = context.read<TchRoomVM>();
//如果房间到点可以开始
if (vm.canEnterRoom) {
status = RoomStatus.start;
// openRoom();
} else {
status = RoomStatus.waiting;
startCountDown();
@@ -48,12 +51,12 @@ class _StatusViewState extends State<StatusView> {
///开始倒计时
void startCountDown() {
final vm = context.read<StudentsViewModel>();
final vm = context.read<TchRoomVM>();
//当前时间
DateTime now = DateTime.now();
//远端时间
setState(() {
_seconds = vm.startTime.difference(now).inSeconds;
_seconds = parseTime(vm.roomInfo.startTime).difference(now).inSeconds;
});
_timer = Timer.periodic(Duration(seconds: 1), (timer) {
setState(() {
@@ -71,8 +74,9 @@ class _StatusViewState extends State<StatusView> {
///开启自习室
void openRoom() {
final vm = context.read<StudentsViewModel>();
final vm = context.read<TchRoomVM>();
vm.toggleRoom(isOpen: true);
vm.removeListener(openRoom);
}
@override

View File

@@ -1,3 +1,5 @@
import 'package:agora_rtc_engine/agora_rtc_engine.dart';
import 'package:app/pages/teacher/room/viewmodel/type.dart';
import 'package:app/request/dto/room/room_user_dto.dart';
import 'package:app/widgets/room/file_drawer.dart';
import 'package:app/widgets/room/video_surface.dart';
@@ -5,14 +7,16 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:remixicon/remixicon.dart';
import '../viewmodel/students_view_model.dart';
import '../viewmodel/tch_room_vm.dart';
class StudentItem extends StatefulWidget {
final RoomUserDto user;
final RtcEngine? engine;
const StudentItem({
super.key,
required this.user,
this.engine,
});
@override
@@ -27,12 +31,16 @@ class _StudentItemState extends State<StudentItem> {
@override
Widget build(BuildContext context) {
final vm = context.read<StudentsViewModel>();
final vm = context.read<TchRoomVM>();
//摄像头是否开启
bool isCameraOpen = widget.user.cameraStatus == 1;
///麦克风是否开启
bool isMicOpen = widget.user.microphoneStatus == 1;
///声音是否开启
bool isSpeakerOpen = widget.user.speekerStatus == 1;
return ClipRRect(
borderRadius: BorderRadius.circular(10),
child: Container(
@@ -44,6 +52,13 @@ class _StudentItemState extends State<StudentItem> {
width: double.infinity,
child: Stack(
children: [
if (widget.engine != null)
AgoraVideoView(
controller: VideoViewController(
rtcEngine: widget.engine!,
canvas: VideoCanvas(uid: widget.user.rtcUid),
),
),
// VideoSurface(),
Positioned(
bottom: 0,
@@ -95,14 +110,35 @@ class _StudentItemState extends State<StudentItem> {
_actionItem(
icon: isCameraOpen ? RemixIcons.video_on_fill : RemixIcons.video_off_fill,
isActive: isCameraOpen,
onTap: () {
vm.closeStudentAction(
uId: widget.user.userId,
action: StudentAction.camera,
);
},
),
_actionItem(
icon: isMicOpen ? RemixIcons.mic_fill : RemixIcons.mic_off_fill,
isActive: isMicOpen,
onTap: () {
vm.closeStudentAction(
uId: widget.user.userId,
action: StudentAction.microphone,
);
},
),
_actionItem(
icon: isSpeakerOpen
? RemixIcons.volume_up_fill
: RemixIcons.volume_mute_fill,
isActive: isSpeakerOpen,
onTap: () {
vm.closeStudentAction(
uId: widget.user.userId,
action: StudentAction.speaker,
);
},
),
// _actionItem(
// icon: RemixIcons.volume_mute_fill,
// ),
_actionItem(icon: RemixIcons.file_list_3_fill, onTap: _openFileList),
],
),