Update Saqel Platform: 2026-09-12 16:23:39
This commit is contained in:
+91
-28
@@ -20,6 +20,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../core/theme/app_colors.dart';
|
||||
import '../../../core/theme/app_theme.dart';
|
||||
import '../../../core/utils/saqel_toast.dart';
|
||||
import '../../../core/config/app_config.dart';
|
||||
import '../../../core/services/storage_service.dart';
|
||||
import '../../../data/models/subject_model.dart';
|
||||
import '../../../logic/cubits/auth_cubit.dart';
|
||||
@@ -81,15 +82,23 @@ class _SocraticVideoPlayerScreenState extends State<SocraticVideoPlayerScreen> w
|
||||
}
|
||||
|
||||
final token = await StorageService().getToken();
|
||||
final headers = <String, String>{};
|
||||
if (token != null && token.isNotEmpty) {
|
||||
headers['Authorization'] = 'Bearer $token';
|
||||
}
|
||||
|
||||
String finalVideoUrl = videoUrl;
|
||||
if (token != null && token.isNotEmpty && finalVideoUrl.contains('/api/videos/') && !finalVideoUrl.contains('token=')) {
|
||||
final sep = finalVideoUrl.contains('?') ? '&' : '?';
|
||||
finalVideoUrl = '$finalVideoUrl${sep}token=${Uri.encodeComponent(token)}';
|
||||
|
||||
// IMPORTANT: Check whether the URL points to our own Saqel API or to an external CDN (BunnyCDN, Cloudflare R2).
|
||||
// NEVER send the Saqel user JWT Bearer token to external CDNs! CDNs reject unauthorized Bearer headers
|
||||
// with HTTP 403 Forbidden ("Resource is protected by access token").
|
||||
final isOurBackend = finalVideoUrl.contains('/api/videos/') ||
|
||||
finalVideoUrl.startsWith(AppConfig.baseUrl) ||
|
||||
finalVideoUrl.contains('localhost') ||
|
||||
finalVideoUrl.contains('127.0.0.1');
|
||||
|
||||
final headers = <String, String>{};
|
||||
if (isOurBackend && token != null && token.isNotEmpty) {
|
||||
headers['Authorization'] = 'Bearer $token';
|
||||
if (!finalVideoUrl.contains('token=')) {
|
||||
final sep = finalVideoUrl.contains('?') ? '&' : '?';
|
||||
finalVideoUrl = '$finalVideoUrl${sep}token=${Uri.encodeComponent(token)}';
|
||||
}
|
||||
}
|
||||
|
||||
_videoController?.dispose();
|
||||
@@ -119,6 +128,38 @@ class _SocraticVideoPlayerScreenState extends State<SocraticVideoPlayerScreen> w
|
||||
}
|
||||
}
|
||||
}).catchError((err) {
|
||||
final errStr = err.toString();
|
||||
// Resilient Fallback: If 403 Forbidden or Access Token error occurred on an external URL with token query params,
|
||||
// retry once without the token parameters (e.g. public pull zones or direct CDNs).
|
||||
if ((errStr.contains('403') || errStr.contains('token') || errStr.contains('permission')) &&
|
||||
finalVideoUrl.contains('?')) {
|
||||
final strippedUrl = finalVideoUrl.split('?').first;
|
||||
if (strippedUrl != finalVideoUrl && strippedUrl.isNotEmpty) {
|
||||
_videoController?.dispose();
|
||||
_videoController = VideoPlayerController.networkUrl(
|
||||
Uri.parse(strippedUrl),
|
||||
httpHeaders: const {},
|
||||
)..initialize().then((_) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isVideoInitialized = true;
|
||||
_videoInitError = null;
|
||||
});
|
||||
_revealVideoControls();
|
||||
if (shouldPlay) _videoController!.play();
|
||||
}
|
||||
}).catchError((retryErr) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isVideoInitialized = false;
|
||||
_videoInitError = 'تعذر تشغيل بث الفيديو من المصدر السحابي، يمكنك استخدام السبورة التفاعلية للشرح.';
|
||||
});
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isVideoInitialized = false;
|
||||
@@ -407,26 +448,48 @@ class _SocraticVideoPlayerScreenState extends State<SocraticVideoPlayerScreen> w
|
||||
style: const TextStyle(color: AppColors.textSecondaryDark, fontSize: 12),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
ElevatedButton.icon(
|
||||
icon: const Icon(CupertinoIcons.arrow_clockwise, size: 16),
|
||||
label: const Text('بدء تشغيل بث الفيديو المباشر 🎥'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.saqelCyan,
|
||||
foregroundColor: Colors.black,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_videoInitError = null;
|
||||
_isVideoInitialized = false;
|
||||
});
|
||||
_initPlayer(
|
||||
state.playbackData.videoUrl,
|
||||
state.currentPositionSeconds,
|
||||
true,
|
||||
);
|
||||
},
|
||||
Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
spacing: 10,
|
||||
runSpacing: 10,
|
||||
children: [
|
||||
ElevatedButton.icon(
|
||||
icon: const Icon(CupertinoIcons.arrow_clockwise, size: 16),
|
||||
label: const Text('إعادة محاولة البث 🎥'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.saqelCyan,
|
||||
foregroundColor: Colors.black,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 10),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_videoInitError = null;
|
||||
_isVideoInitialized = false;
|
||||
});
|
||||
_initPlayer(
|
||||
state.playbackData.videoUrl,
|
||||
state.currentPositionSeconds,
|
||||
true,
|
||||
);
|
||||
},
|
||||
),
|
||||
OutlinedButton.icon(
|
||||
icon: const Icon(CupertinoIcons.mic_fill, size: 16, color: AppColors.appleBlue),
|
||||
label: const Text('السبورة والشرح الصوتي 🎙️', style: TextStyle(color: Colors.white)),
|
||||
style: OutlinedButton.styleFrom(
|
||||
side: BorderSide(color: AppColors.appleBlue.withAlpha(150)),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 10),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_showChalkboardMode = true;
|
||||
_videoInitError = null;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user