优化了老师端的等待状态判断逻辑

This commit is contained in:
zhutao
2025-11-23 22:09:39 +08:00
parent 5784a0a5d4
commit 4ecb0c35d6
8 changed files with 276 additions and 67 deletions

View File

@@ -1,4 +1,8 @@
import 'package:agora_rtc_engine/agora_rtc_engine.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../viewmodel/stu_room_vm.dart';
class TeacherVideo extends StatefulWidget { class TeacherVideo extends StatefulWidget {
const TeacherVideo({super.key}); const TeacherVideo({super.key});
@@ -10,10 +14,44 @@ class TeacherVideo extends StatefulWidget {
class _TeacherVideoState extends State<TeacherVideo> { class _TeacherVideoState extends State<TeacherVideo> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final vm = context.read<StuRoomVM>();
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( return SafeArea(
child: Align( child: Align(
child: Text( child: Text(
"画面准备中", title,
style: TextStyle(color: Colors.white), style: TextStyle(color: Colors.white),
), ),
), ),

View File

@@ -1,5 +1,7 @@
import 'dart:async'; 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/providers/user_store.dart';
import 'package:app/request/dto/room/room_info_dto.dart'; 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_type_dto.dart';
@@ -36,12 +38,48 @@ class StuRoomVM extends ChangeNotifier {
final RoomWebSocket _ws = RoomWebSocket(); final RoomWebSocket _ws = RoomWebSocket();
StreamSubscription<RoomMessage>? _sub; StreamSubscription<RoomMessage>? _sub;
RtcTokenDto? get rtcToken => _ws.rtcToken; /// 声网sdk管理
RtcEngine? _engine;
RtcEngine? get engine => _engine;
StuRoomVM({required this.roomInfo, required this.uid}) { StuRoomVM({required this.roomInfo, required this.uid}) {
_startRoom(); _startRoom();
} }
///初始化声网
Future<void> _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<void> _startRoom() async { Future<void> _startRoom() async {
//如果socket的token没有先初始化 //如果socket的token没有先初始化

View File

@@ -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/router/route_paths.dart';
import 'package:app/utils/permission.dart'; import 'package:app/utils/permission.dart';
import 'package:app/widgets/base/button/index.dart'; import 'package:app/widgets/base/button/index.dart';
import 'package:app/widgets/base/card/g_card.dart'; import 'package:app/widgets/base/card/g_card.dart';
import 'package:app/widgets/base/config/config.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/base/empty/index.dart';
import 'package:app/widgets/room/file_drawer.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
@@ -14,6 +14,8 @@ import 'package:provider/provider.dart';
import 'package:remixicon/remixicon.dart'; import 'package:remixicon/remixicon.dart';
import 'package:skeletonizer/skeletonizer.dart'; import 'package:skeletonizer/skeletonizer.dart';
import '../viewmodel/home_view_model.dart';
class TodayCard extends StatefulWidget { class TodayCard extends StatefulWidget {
const TodayCard({super.key}); const TodayCard({super.key});
@@ -28,7 +30,7 @@ class _TodayCardState extends State<TodayCard> {
permissions: [Permission.microphone, Permission.camera], permissions: [Permission.microphone, Permission.camera],
onGranted: () { onGranted: () {
final vm = context.read<HomeViewModel>(); final vm = context.read<HomeViewModel>();
context.push(RoutePaths.tRoom,extra: vm.roomInfo); context.push(RoutePaths.tRoom, extra: vm.roomInfo);
}, },
onDenied: () { onDenied: () {
EasyLoading.showError("请开启权限"); EasyLoading.showError("请开启权限");

View File

@@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'package:app/request/dto/room/room_info_dto.dart'; 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/room_user_dto.dart';
import 'package:app/request/dto/room/rtc_token_dto.dart'; import 'package:app/request/dto/room/rtc_token_dto.dart';
import 'package:app/request/websocket/room_protocol.dart'; import 'package:app/request/websocket/room_protocol.dart';
@@ -11,7 +12,10 @@ import 'package:flutter/cupertino.dart';
import 'type.dart'; import 'type.dart';
class TchRoomVM extends ChangeNotifier { class TchRoomVM extends ChangeNotifier {
TchRoomVM({required this.roomInfo, String? start}) { TchRoomVM({
required this.roomInfo,
String? start,
}) {
_startRoom(); _startRoom();
} }
@@ -20,6 +24,7 @@ class TchRoomVM extends ChangeNotifier {
///房间的基础信息 ///房间的基础信息
final RoomInfoDto roomInfo; final RoomInfoDto roomInfo;
int roomStatus = -1; // //-1加载中0没开始1进行中2关闭
///老师选中的学生id ///老师选中的学生id
int activeSId = 0; int activeSId = 0;
@@ -37,7 +42,8 @@ class TchRoomVM extends ChangeNotifier {
///websocket管理 ///websocket管理
final RoomWebSocket _ws = RoomWebSocket(); final RoomWebSocket _ws = RoomWebSocket();
bool wsConnected = false; // socket连接状态
// bool wsConnected = false; // socket连接状态
StreamSubscription<RoomMessage>? _sub; StreamSubscription<RoomMessage>? _sub;
RtcTokenDto? get rtcToken => _ws.rtcToken; RtcTokenDto? get rtcToken => _ws.rtcToken;
@@ -50,12 +56,13 @@ class TchRoomVM extends ChangeNotifier {
} }
//启动连接 //启动连接
await _ws.connect(); await _ws.connect();
wsConnected = true;
//监听各种ws事件 //监听各种ws事件
_sub = _ws.stream.listen((msg) { _sub = _ws.stream.listen((msg) {
// 自习室人员变化 // 自习室人员变化
if (msg.event == RoomEvent.changeUser) { if (msg.event == RoomEvent.changeUser) {
final list = RoomUserDto.listFromJson(msg.data['user_list']); final list = RoomUserDto.listFromJson(msg.data['user_list']);
final room = RoomTypeDto.fromJson(msg.data['room_info']);
roomStatus = room.roomStatus;
onStudentChange(list); onStudentChange(list);
} else if ([ } else if ([
RoomEvent.openSpeaker, RoomEvent.openSpeaker,

View File

@@ -1,5 +1,6 @@
import 'package:agora_rtc_engine/agora_rtc_engine.dart'; import 'package:agora_rtc_engine/agora_rtc_engine.dart';
import 'package:app/config/config.dart'; import 'package:app/config/config.dart';
import 'package:app/providers/user_store.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@@ -14,17 +15,23 @@ class ContentView extends StatefulWidget {
} }
class _ContentViewState extends State<ContentView> { class _ContentViewState extends State<ContentView> {
//声网数据 //声网数据
RtcEngine? _engine; RtcEngine? _engine;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
// _initRtc(); _initRtc();
}
@override
void dispose() {
super.dispose();
_dispose();
} }
void _initRtc() async { void _initRtc() async {
UserStore userStore = context.read<UserStore>();
final vm = context.read<TchRoomVM>(); final vm = context.read<TchRoomVM>();
_engine = createAgoraRtcEngine(); _engine = createAgoraRtcEngine();
//初始化 RtcEngine设置频道场景为 channelProfileLiveBroadcasting直播场景 //初始化 RtcEngine设置频道场景为 channelProfileLiveBroadcasting直播场景
@@ -44,8 +51,7 @@ class _ContentViewState extends State<ContentView> {
// 远端用户或主播加入当前频道回调 // 远端用户或主播加入当前频道回调
onUserJoined: (RtcConnection connection, int remoteUid, int elapsed) {}, onUserJoined: (RtcConnection connection, int remoteUid, int elapsed) {},
// 远端用户或主播离开当前频道回调 // 远端用户或主播离开当前频道回调
onUserOffline: onUserOffline: (RtcConnection connection, int remoteUid, UserOfflineReasonType reason) {},
(RtcConnection connection, int remoteUid, UserOfflineReasonType reason) {},
), ),
); );
//启动视频模块 //启动视频模块
@@ -54,7 +60,7 @@ class _ContentViewState extends State<ContentView> {
await _engine!.joinChannel( await _engine!.joinChannel(
token: vm.rtcToken!.token, token: vm.rtcToken!.token,
channelId: vm.rtcToken!.channel, channelId: vm.rtcToken!.channel,
uid: vm.rtcToken!.uid, uid: userStore.userInfo!.id,
options: ChannelMediaOptions( options: ChannelMediaOptions(
// 自动订阅所有视频流 // 自动订阅所有视频流
autoSubscribeVideo: true, autoSubscribeVideo: true,
@@ -70,6 +76,14 @@ class _ContentViewState extends State<ContentView> {
); );
} }
//销毁
Future<void> _dispose() async {
if (_engine != null) {
await _engine!.leaveChannel();
await _engine!.release();
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Consumer<TchRoomVM>( return Consumer<TchRoomVM>(

View File

@@ -1,7 +1,9 @@
import 'dart:async'; import 'dart:async';
import 'package:app/utils/time.dart'; import 'package:app/utils/time.dart';
import 'package:app/widgets/base/dialog/config_dialog.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'content_view.dart'; import 'content_view.dart';
@@ -15,86 +17,97 @@ class StatusView extends StatefulWidget {
} }
class _StatusViewState extends State<StatusView> { class _StatusViewState extends State<StatusView> {
///房间状态
RoomStatus status = RoomStatus.loading;
///剩余秒
int _seconds = 0; int _seconds = 0;
Timer? _timer; Timer? _timer;
@override
void initState() {
super.initState();
_init();
final vm = context.read<TchRoomVM>();
vm.addListener(openRoom);
}
@override @override
void dispose() { void dispose() {
super.dispose();
_timer?.cancel(); _timer?.cancel();
_timer = null; super.dispose();
} }
void _init() { void _startCountDown(DateTime startTime) {
final vm = context.read<TchRoomVM>(); // 避免重复计时器
//如果房间到点可以开始 if (_timer != null) return;
if (vm.canEnterRoom) {
status = RoomStatus.start; final now = DateTime.now();
// openRoom(); int diff = startTime.difference(now).inSeconds;
} else {
status = RoomStatus.waiting; if (diff <= 0) {
startCountDown(); return;
} }
}
///开始倒计时
void startCountDown() {
final vm = context.read<TchRoomVM>();
//当前时间
DateTime now = DateTime.now();
//远端时间
setState(() { 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(() { setState(() {
_seconds--; _seconds--;
}); });
if (_seconds <= 0) { if (_seconds <= 0) {
_timer?.cancel(); _timer?.cancel();
_timer = null; _timer = null;
setState(() {
status = RoomStatus.start;
});
} }
}); });
} }
///开启自习室 ///开播中返回拦截弹窗
void openRoom() { void _interceptPop() {
final vm = context.read<TchRoomVM>(); showDialog(
vm.toggleRoom(isOpen: true); context: context,
vm.removeListener(openRoom); builder: (context) {
return ConfigDialog(
content: "是否退出自习室",
onCancel: () {
context.pop();
},
onConfirm: () {
context.pop();
context.pop();
},
);
},
);
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (status == RoomStatus.waiting) { final vm = context.watch<TchRoomVM>();
/// 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( return Align(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Text( const Text(
"未到开播时间,到点后自动开播", "未到开播时间,到点后自动开播",
style: TextStyle(color: Colors.white), style: TextStyle(color: Colors.white),
), ),
Container( Container(
margin: EdgeInsets.symmetric(vertical: 10), margin: const EdgeInsets.symmetric(vertical: 10),
child: Text( child: Text(
formatSeconds(_seconds), formatSeconds(_seconds),
style: TextStyle( style: const TextStyle(
color: Colors.white, color: Colors.white,
fontSize: 26, fontSize: 26,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@@ -104,15 +117,21 @@ class _StatusViewState extends State<StatusView> {
], ],
), ),
); );
} 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, //房间开始中
}

View File

@@ -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,
),
),
),
),
],
),
),
],
);
}
}

View File