From 4ecb0c35d611adeaac7d2338919a16356638122d Mon Sep 17 00:00:00 2001 From: zhutao <1812073942@qq.com> Date: Sun, 23 Nov 2025 22:09:39 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E4=BA=86=E8=80=81=E5=B8=88?= =?UTF-8?q?=E7=AB=AF=E7=9A=84=E7=AD=89=E5=BE=85=E7=8A=B6=E6=80=81=E5=88=A4?= =?UTF-8?q?=E6=96=AD=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../student/room/video/teacher_video.dart | 40 +++++- .../student/room/viewmodel/stu_room_vm.dart | 40 +++++- .../teacher/home/widgets/today_card.dart | 8 +- .../teacher/room/viewmodel/tch_room_vm.dart | 13 +- .../teacher/room/widgets/content_view.dart | 24 +++- .../teacher/room/widgets/status_view.dart | 127 ++++++++++-------- lib/widgets/base/dialog/config_dialog.dart | 91 +++++++++++++ lib/widgets/room/exit_room_dialog.dart | 0 8 files changed, 276 insertions(+), 67 deletions(-) create mode 100644 lib/widgets/base/dialog/config_dialog.dart create mode 100644 lib/widgets/room/exit_room_dialog.dart diff --git a/lib/pages/student/room/video/teacher_video.dart b/lib/pages/student/room/video/teacher_video.dart index 2880368..81dc960 100644 --- a/lib/pages/student/room/video/teacher_video.dart +++ b/lib/pages/student/room/video/teacher_video.dart @@ -1,4 +1,8 @@ +import 'package:agora_rtc_engine/agora_rtc_engine.dart'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import '../viewmodel/stu_room_vm.dart'; class TeacherVideo extends StatefulWidget { const TeacherVideo({super.key}); @@ -10,10 +14,44 @@ class TeacherVideo extends StatefulWidget { class _TeacherVideoState extends State { @override Widget build(BuildContext context) { + final vm = context.read(); + final teacherInfo = vm.teacherInfo; + + ///没开始 + if (vm.roomStatus == 0) { + return _empty("自习室还没开始"); + } + + ///开始 + if (vm.roomStatus == 1 && vm.engine != null) { + if (teacherInfo == null) { + return _empty("老师不在自习室内"); + } + if (teacherInfo.online == 0) { + return _empty("老师掉线了,请耐心等待"); + } + return AgoraVideoView( + controller: VideoViewController( + rtcEngine: vm.engine!, + canvas: VideoCanvas( + uid: vm.teacherInfo!.userId, + ), + ), + ); + } + + ///结束 + if (vm.roomStatus == 2) { + return _empty("自习室已结束"); + } + return _empty("加载中"); + } + + Widget _empty(String title) { return SafeArea( child: Align( child: Text( - "画面准备中", + title, style: TextStyle(color: Colors.white), ), ), diff --git a/lib/pages/student/room/viewmodel/stu_room_vm.dart b/lib/pages/student/room/viewmodel/stu_room_vm.dart index 3ca0d9e..251c917 100644 --- a/lib/pages/student/room/viewmodel/stu_room_vm.dart +++ b/lib/pages/student/room/viewmodel/stu_room_vm.dart @@ -1,5 +1,7 @@ import 'dart:async'; +import 'package:agora_rtc_engine/agora_rtc_engine.dart'; +import 'package:app/config/config.dart'; import 'package:app/providers/user_store.dart'; import 'package:app/request/dto/room/room_info_dto.dart'; import 'package:app/request/dto/room/room_type_dto.dart'; @@ -36,12 +38,48 @@ class StuRoomVM extends ChangeNotifier { final RoomWebSocket _ws = RoomWebSocket(); StreamSubscription? _sub; - RtcTokenDto? get rtcToken => _ws.rtcToken; + /// 声网sdk管理 + RtcEngine? _engine; + + RtcEngine? get engine => _engine; StuRoomVM({required this.roomInfo, required this.uid}) { _startRoom(); } + ///初始化声网 + Future _initRtc() async { + _engine = createAgoraRtcEngine(); + //初始化 RtcEngine,设置频道场景为 channelProfileLiveBroadcasting(直播场景) + await _engine!.initialize( + RtcEngineContext( + appId: Config.swAppId, + channelProfile: ChannelProfileType.channelProfileCommunication, + ), + ); + //启动视频模块 + await _engine!.enableVideo(); + //加入频道 + await _engine!.joinChannel( + token: _ws.rtcToken!.token, + channelId: _ws.rtcToken!.channel, + uid: uid, + // uid: _ws.rtcToken!.uid, + options: ChannelMediaOptions( + // 自动订阅所有视频流 + autoSubscribeVideo: true, + // 自动订阅所有音频流 + autoSubscribeAudio: true, + // 发布摄像头采集的视频 + publishCameraTrack: true, + // 发布麦克风采集的音频 + publishMicrophoneTrack: true, + // 设置用户角色为 clientRoleBroadcaster(主播)或 clientRoleAudience(观众) + clientRoleType: ClientRoleType.clientRoleBroadcaster, + ), + ); + } + ///开始链接房间 Future _startRoom() async { //如果socket的token没有,先初始化 diff --git a/lib/pages/teacher/home/widgets/today_card.dart b/lib/pages/teacher/home/widgets/today_card.dart index 4c217c1..accdc89 100644 --- a/lib/pages/teacher/home/widgets/today_card.dart +++ b/lib/pages/teacher/home/widgets/today_card.dart @@ -1,11 +1,11 @@ -import 'package:app/pages/teacher/home/viewmodel/home_view_model.dart'; -import 'package:app/request/dto/room/room_info_dto.dart'; import 'package:app/router/route_paths.dart'; import 'package:app/utils/permission.dart'; import 'package:app/widgets/base/button/index.dart'; import 'package:app/widgets/base/card/g_card.dart'; import 'package:app/widgets/base/config/config.dart'; +import 'package:app/widgets/base/dialog/config_dialog.dart'; import 'package:app/widgets/base/empty/index.dart'; +import 'package:app/widgets/room/file_drawer.dart'; import 'package:flutter/material.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:go_router/go_router.dart'; @@ -14,6 +14,8 @@ import 'package:provider/provider.dart'; import 'package:remixicon/remixicon.dart'; import 'package:skeletonizer/skeletonizer.dart'; +import '../viewmodel/home_view_model.dart'; + class TodayCard extends StatefulWidget { const TodayCard({super.key}); @@ -28,7 +30,7 @@ class _TodayCardState extends State { permissions: [Permission.microphone, Permission.camera], onGranted: () { final vm = context.read(); - context.push(RoutePaths.tRoom,extra: vm.roomInfo); + context.push(RoutePaths.tRoom, extra: vm.roomInfo); }, onDenied: () { EasyLoading.showError("请开启权限"); diff --git a/lib/pages/teacher/room/viewmodel/tch_room_vm.dart b/lib/pages/teacher/room/viewmodel/tch_room_vm.dart index ee9c78d..1b05457 100644 --- a/lib/pages/teacher/room/viewmodel/tch_room_vm.dart +++ b/lib/pages/teacher/room/viewmodel/tch_room_vm.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:app/request/dto/room/room_info_dto.dart'; +import 'package:app/request/dto/room/room_type_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'; @@ -11,7 +12,10 @@ import 'package:flutter/cupertino.dart'; import 'type.dart'; class TchRoomVM extends ChangeNotifier { - TchRoomVM({required this.roomInfo, String? start}) { + TchRoomVM({ + required this.roomInfo, + String? start, + }) { _startRoom(); } @@ -20,6 +24,7 @@ class TchRoomVM extends ChangeNotifier { ///房间的基础信息 final RoomInfoDto roomInfo; + int roomStatus = -1; // //-1加载中,0没开始,1进行中,2关闭 ///老师选中的学生id int activeSId = 0; @@ -37,7 +42,8 @@ class TchRoomVM extends ChangeNotifier { ///websocket管理 final RoomWebSocket _ws = RoomWebSocket(); - bool wsConnected = false; // socket连接状态 + + // bool wsConnected = false; // socket连接状态 StreamSubscription? _sub; RtcTokenDto? get rtcToken => _ws.rtcToken; @@ -50,12 +56,13 @@ class TchRoomVM extends ChangeNotifier { } //启动连接 await _ws.connect(); - wsConnected = true; //监听各种ws事件 _sub = _ws.stream.listen((msg) { // 自习室人员变化 if (msg.event == RoomEvent.changeUser) { final list = RoomUserDto.listFromJson(msg.data['user_list']); + final room = RoomTypeDto.fromJson(msg.data['room_info']); + roomStatus = room.roomStatus; onStudentChange(list); } else if ([ RoomEvent.openSpeaker, diff --git a/lib/pages/teacher/room/widgets/content_view.dart b/lib/pages/teacher/room/widgets/content_view.dart index f3ae86f..a5d6191 100644 --- a/lib/pages/teacher/room/widgets/content_view.dart +++ b/lib/pages/teacher/room/widgets/content_view.dart @@ -1,5 +1,6 @@ import 'package:agora_rtc_engine/agora_rtc_engine.dart'; import 'package:app/config/config.dart'; +import 'package:app/providers/user_store.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -14,17 +15,23 @@ class ContentView extends StatefulWidget { } class _ContentViewState extends State { - //声网数据 RtcEngine? _engine; @override void initState() { super.initState(); - // _initRtc(); + _initRtc(); + } + + @override + void dispose() { + super.dispose(); + _dispose(); } void _initRtc() async { + UserStore userStore = context.read(); final vm = context.read(); _engine = createAgoraRtcEngine(); //初始化 RtcEngine,设置频道场景为 channelProfileLiveBroadcasting(直播场景) @@ -44,8 +51,7 @@ class _ContentViewState extends State { // 远端用户或主播加入当前频道回调 onUserJoined: (RtcConnection connection, int remoteUid, int elapsed) {}, // 远端用户或主播离开当前频道回调 - onUserOffline: - (RtcConnection connection, int remoteUid, UserOfflineReasonType reason) {}, + onUserOffline: (RtcConnection connection, int remoteUid, UserOfflineReasonType reason) {}, ), ); //启动视频模块 @@ -54,7 +60,7 @@ class _ContentViewState extends State { await _engine!.joinChannel( token: vm.rtcToken!.token, channelId: vm.rtcToken!.channel, - uid: vm.rtcToken!.uid, + uid: userStore.userInfo!.id, options: ChannelMediaOptions( // 自动订阅所有视频流 autoSubscribeVideo: true, @@ -70,6 +76,14 @@ class _ContentViewState extends State { ); } + //销毁 + Future _dispose() async { + if (_engine != null) { + await _engine!.leaveChannel(); + await _engine!.release(); + } + } + @override Widget build(BuildContext context) { return Consumer( diff --git a/lib/pages/teacher/room/widgets/status_view.dart b/lib/pages/teacher/room/widgets/status_view.dart index 632b982..5582c72 100644 --- a/lib/pages/teacher/room/widgets/status_view.dart +++ b/lib/pages/teacher/room/widgets/status_view.dart @@ -1,7 +1,9 @@ import 'dart:async'; import 'package:app/utils/time.dart'; +import 'package:app/widgets/base/dialog/config_dialog.dart'; import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; import 'package:provider/provider.dart'; import 'content_view.dart'; @@ -15,86 +17,97 @@ class StatusView extends StatefulWidget { } class _StatusViewState extends State { - ///房间状态 - RoomStatus status = RoomStatus.loading; - - ///剩余秒 int _seconds = 0; Timer? _timer; - @override - void initState() { - super.initState(); - _init(); - final vm = context.read(); - vm.addListener(openRoom); - } - @override void dispose() { - super.dispose(); _timer?.cancel(); - _timer = null; + super.dispose(); } - void _init() { - final vm = context.read(); - //如果房间到点可以开始 - if (vm.canEnterRoom) { - status = RoomStatus.start; - // openRoom(); - } else { - status = RoomStatus.waiting; - startCountDown(); + void _startCountDown(DateTime startTime) { + // 避免重复计时器 + if (_timer != null) return; + + final now = DateTime.now(); + int diff = startTime.difference(now).inSeconds; + + if (diff <= 0) { + return; } - } - - ///开始倒计时 - void startCountDown() { - final vm = context.read(); - //当前时间 - DateTime now = DateTime.now(); - //远端时间 setState(() { - _seconds = parseTime(vm.roomInfo.startTime).difference(now).inSeconds; + _seconds = diff; }); - _timer = Timer.periodic(Duration(seconds: 1), (timer) { + + _timer = Timer.periodic(const Duration(seconds: 1), (timer) { + if (!mounted) return; setState(() { _seconds--; }); + if (_seconds <= 0) { _timer?.cancel(); _timer = null; - setState(() { - status = RoomStatus.start; - }); } }); } - ///开启自习室 - void openRoom() { - final vm = context.read(); - vm.toggleRoom(isOpen: true); - vm.removeListener(openRoom); + ///开播中返回拦截弹窗 + void _interceptPop() { + showDialog( + context: context, + builder: (context) { + return ConfigDialog( + content: "是否退出自习室", + onCancel: () { + context.pop(); + }, + onConfirm: () { + context.pop(); + context.pop(); + }, + ); + }, + ); } @override Widget build(BuildContext context) { - if (status == RoomStatus.waiting) { + final vm = context.watch(); + + /// 1. 未加载 + if (vm.roomStatus == -1) { + return const Align( + child: Text("加载中", style: TextStyle(color: Colors.white)), + ); + } + + /// 2. 未开始的房间 + if (vm.roomStatus == 0) { + if (vm.canEnterRoom) { + // 到时间了 → 自动开播 + WidgetsBinding.instance.addPostFrameCallback((_) { + vm.toggleRoom(isOpen: true); + }); + } else { + // 没到时间 → 启动倒计时 + _startCountDown(parseTime(vm.roomInfo.startTime)); + } + return Align( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Text( + const Text( "未到开播时间,到点后自动开播", style: TextStyle(color: Colors.white), ), Container( - margin: EdgeInsets.symmetric(vertical: 10), + margin: const EdgeInsets.symmetric(vertical: 10), child: Text( formatSeconds(_seconds), - style: TextStyle( + style: const TextStyle( color: Colors.white, fontSize: 26, fontWeight: FontWeight.bold, @@ -104,15 +117,21 @@ class _StatusViewState extends State { ], ), ); - } else if (status == RoomStatus.start) { - return ContentView(); } - return SizedBox(); + + /// 3. 已开播 + if (vm.roomStatus == 1) { + return PopScope( + canPop: false, + onPopInvokedWithResult: (didPop, _) { + if (!didPop) { + _interceptPop(); + } + }, + child: const ContentView(), + ); + } + + return const SizedBox(); } } - -enum RoomStatus { - loading, // 加载中 - waiting, //房间倒计时等待中 - start, //房间开始中 -} diff --git a/lib/widgets/base/dialog/config_dialog.dart b/lib/widgets/base/dialog/config_dialog.dart new file mode 100644 index 0000000..fcfb47b --- /dev/null +++ b/lib/widgets/base/dialog/config_dialog.dart @@ -0,0 +1,91 @@ +import 'package:flutter/material.dart'; + +class ConfigDialog extends StatelessWidget { + final String title; + final String content; + final TextStyle? contentStyle; + final String confirmText; + final String cancelText; + final bool showCancel; + final void Function()? onConfirm; + final void Function()? onCancel; + + const ConfigDialog({ + super.key, + this.title = "", + required this.content, + this.contentStyle, + this.confirmText = '确定', + this.cancelText = '取消', + this.showCancel = true, + this.onConfirm, + this.onCancel, + }); + + @override + Widget build(BuildContext context) { + Color borderColor = Theme.of(context).colorScheme.surfaceContainer; + return AlertDialog( + actionsPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 5), + contentPadding: const EdgeInsets.symmetric(horizontal: 0, vertical: 0), + elevation: 0, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + title: Visibility( + visible: title.isNotEmpty, + child: Container( + margin: const EdgeInsets.only(bottom: 15), + child: Text( + title, + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w700), + ), + ), + ), + content: Container( + padding: const EdgeInsets.only(bottom: 20, left: 20, right: 20), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide(width: 1, color: borderColor), + ), + ), + child: Text(content, textAlign: TextAlign.center, style: contentStyle), + ), + actions: [ + SizedBox( + height: 40, + child: Row( + children: [ + if (showCancel) ...[ + Expanded( + child: TextButton( + onPressed: onCancel, + child: Text( + cancelText, + textAlign: TextAlign.center, + ), + ), + ), + Container( + width: 1, + margin: EdgeInsets.symmetric(horizontal: 10), + color: borderColor, + ), + ], + Expanded( + child: TextButton( + onPressed: onConfirm, + child: Text( + confirmText, + style: TextStyle( + color: Theme.of(context).primaryColor, + ), + ), + ), + ), + ], + ), + ), + ], + ); + } +} diff --git a/lib/widgets/room/exit_room_dialog.dart b/lib/widgets/room/exit_room_dialog.dart new file mode 100644 index 0000000..e69de29