feat: Add generated Hive TypeAdapters for AppUser and PlaylistConfig models, and new build/doctor log files.

This commit is contained in:
Michael committed 2026-03-27 11:08:04 +01:00
1 parent 780f6c1842
commit ef97c6dc90
81 files changed
+3233 -2428

No files matched your search

@@ -6,22 +6,21 @@ import 'package:video_player/video_player.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
import 'package:path_provider/path_provider.dart';
import 'package:xtremflow/core/models/iptv_models.dart';
import 'package:xtremflow/core/models/playlist_config.dart';
import 'package:xtremflow/features/iptv/services/xtream_service_mobile.dart';
import 'package:xtremflow/mobile/providers/mobile_settings_providers.dart';
import 'package:xtremflow/core/theme/app_colors.dart';
import 'package:xtremflow/features/iptv/screens/native_player_screen.dart';
import 'package:xtremflow/features/iptv/models/xtream_models.dart' as xm;
import 'package:xtremflow/mobile/widgets/tv_focusable.dart';
import 'package:xtremobile/core/models/iptv_models.dart' as model;
import 'package:xtremobile/core/models/playlist_config.dart';
import 'package:xtremobile/features/iptv/services/xtream_service_mobile.dart';
import 'package:xtremobile/mobile/providers/mobile_settings_providers.dart';
import 'package:xtremobile/core/theme/app_colors.dart';
import 'package:xtremobile/features/iptv/screens/native_player_screen.dart';
import 'package:xtremobile/mobile/widgets/tv_focusable.dart';
class LitePlayerScreen extends ConsumerStatefulWidget {
final String streamId;
final String title;
final StreamType streamType;
final model.StreamType streamType;
final String? containerExtension;
final PlaylistConfig playlist;
final List<Channel>? channels;
final List<model.Channel>? channels;
final int initialIndex;
// For series episodes
@@ -59,7 +58,7 @@ class _LitePlayerScreenState extends ConsumerState<LitePlayerScreen>
bool _isPlaying = false;
// EPG & Clock
xm.ShortEPG? _epg;
model.ShortEPG? _epg;
Timer? _epgTimer;
String _currentTime = "";
Timer? _clockTimer;
@@ -135,7 +134,7 @@ class _LitePlayerScreenState extends ConsumerState<LitePlayerScreen>
await _xtreamService!.setPlaylistAsync(widget.playlist);
_loadStream();
if (widget.streamType == StreamType.live) {
if (widget.streamType == model.StreamType.live) {
_updateEPG();
_epgTimer = Timer.periodic(
const Duration(minutes: 5),
@@ -148,7 +147,7 @@ class _LitePlayerScreenState extends ConsumerState<LitePlayerScreen>
}
Future<void> _updateEPG() async {
if (widget.streamType != StreamType.live || _xtreamService == null) return;
if (widget.streamType != model.StreamType.live || _xtreamService == null) return;
try {
final currentChannelId =
widget.channels?[_currentIndex].streamId ?? widget.streamId;
@@ -179,11 +178,11 @@ class _LitePlayerScreenState extends ConsumerState<LitePlayerScreen>
? widget.channels![_currentIndex].streamId
: widget.streamId;
if (widget.streamType == StreamType.live) _updateEPG();
if (widget.streamType == model.StreamType.live) _updateEPG();
final streamUrl = widget.streamType == StreamType.live
final streamUrl = widget.streamType == model.StreamType.live
? _xtreamService!.getLiveStreamUrl(currentStreamId)
: (widget.streamType == StreamType.vod
: (widget.streamType == model.StreamType.vod
? _xtreamService!.getVodStreamUrl(
currentStreamId,
widget.containerExtension ?? 'mp4',
@@ -210,7 +209,7 @@ class _LitePlayerScreenState extends ConsumerState<LitePlayerScreen>
}
// Buffer Optimization: Pre-roll delay for TS streams
if (widget.streamType == StreamType.live) {
if (widget.streamType == model.StreamType.live) {
await Future.delayed(const Duration(milliseconds: 2000));
}
@@ -489,7 +488,7 @@ class _LitePlayerScreenState extends ConsumerState<LitePlayerScreen>
mainAxisSize: MainAxisSize.min,
children: [
// Seeker (VOD)
if (widget.streamType != StreamType.live) _buildSeeker(),
if (widget.streamType != model.StreamType.live) _buildSeeker(),
// Unified Box: EPG & Controls
Container(
@@ -511,7 +510,7 @@ class _LitePlayerScreenState extends ConsumerState<LitePlayerScreen>
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (widget.streamType == StreamType.live)
if (widget.streamType == model.StreamType.live)
TVFocusable(
focusNode: _prevFocusNode,
onPressed: () => _switchChannel(
@@ -539,7 +538,7 @@ class _LitePlayerScreenState extends ConsumerState<LitePlayerScreen>
),
),
const SizedBox(width: 16),
if (widget.streamType == StreamType.live)
if (widget.streamType == model.StreamType.live)
TVFocusable(
focusNode: _nextFocusNode,
onPressed: () => _switchChannel(
@@ -601,7 +600,7 @@ class _LitePlayerScreenState extends ConsumerState<LitePlayerScreen>
}
Widget _buildEPGBox(String title) {
String program = widget.streamType == StreamType.live
String program = widget.streamType == model.StreamType.live
? (_epg?.nowPlaying ?? "Pas d'infos EPG")
: title;
String next =
@@ -646,7 +645,7 @@ class _LitePlayerScreenState extends ConsumerState<LitePlayerScreen>
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
if (widget.streamType == StreamType.live &&
if (widget.streamType == model.StreamType.live &&
_epg?.nowPlaying != null) ...[
const SizedBox(height: 6),
ClipRRect(
@@ -1,14 +1,14 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../core/models/playlist_config.dart';
import '../../../widgets/mobile_scaffold.dart';
import '../../../theme/mobile_theme.dart';
import '../widgets/mobile_live_tv_tab.dart';
import '../widgets/mobile_movies_tab.dart';
import '../widgets/mobile_series_tab.dart';
import '../widgets/mobile_settings_tab.dart';
import '../../../providers/mobile_xtream_providers.dart';
import 'package:xtremobile/core/models/playlist_config.dart';
import 'package:xtremobile/mobile/widgets/mobile_scaffold.dart';
import 'package:xtremobile/mobile/theme/mobile_theme.dart';
import 'package:xtremobile/features/iptv/widgets/mobile_live_tv_tab.dart';
import 'package:xtremobile/features/iptv/widgets/mobile_movies_tab.dart';
import 'package:xtremobile/features/iptv/widgets/mobile_series_tab.dart';
import 'package:xtremobile/features/iptv/widgets/mobile_settings_tab.dart';
import 'package:xtremobile/mobile/providers/mobile_xtream_providers.dart';
class MobileDashboardScreen extends ConsumerStatefulWidget {
final PlaylistConfig playlist;
@@ -3,7 +3,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../core/models/playlist_config.dart';
import '../../../../core/theme/app_colors.dart';
import '../../../theme/mobile_theme.dart';
import '../../../mobile/theme/mobile_theme.dart';
import '../../../../core/services/playlist_api_service.dart';
import 'mobile_dashboard_screen.dart';
@@ -6,7 +6,7 @@ import '../../../../core/services/playlist_api_service.dart';
import '../../../../core/models/playlist_config.dart';
import '../../../../core/theme/app_colors.dart';
import '../../../theme/mobile_theme.dart';
import '../../../mobile/theme/mobile_theme.dart';
final playlistsProvider = FutureProvider<List<PlaylistConfig>>((ref) async {
final service = PlaylistApiService();
@@ -2,15 +2,16 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:google_fonts/google_fonts.dart';
import '../../../../core/models/playlist_config.dart';
import '../../../providers/mobile_xtream_providers.dart';
import '../../../providers/mobile_settings_providers.dart';
import 'native_player_screen.dart';
import '../../../../core/theme/app_colors.dart';
import '../../../../core/utils/image_cache_config.dart';
import 'package:xtremobile/core/models/playlist_config.dart';
import 'package:xtremobile/mobile/providers/mobile_xtream_providers.dart';
import 'package:xtremobile/mobile/providers/mobile_settings_providers.dart';
import 'package:xtremobile/features/iptv/screens/native_player_screen.dart';
import 'package:xtremobile/core/theme/app_colors.dart';
import 'package:xtremobile/core/utils/image_cache_config.dart';
import 'package:xtremobile/core/models/iptv_models.dart' as model;
class MobileSeriesDetailScreen extends ConsumerStatefulWidget {
final Series series;
final model.Series series;
final PlaylistConfig playlist;
const MobileSeriesDetailScreen({
@@ -26,7 +27,7 @@ class MobileSeriesDetailScreen extends ConsumerStatefulWidget {
class _MobileSeriesDetailScreenState
extends ConsumerState<MobileSeriesDetailScreen> {
SeriesInfo? _seriesInfo;
model.SeriesInfo? _seriesInfo;
bool _isLoading = true;
String? _error;
int _selectedSeason = 1;
@@ -62,8 +63,9 @@ class _MobileSeriesDetailScreenState
setState(() {
_seriesInfo = info;
_isLoading = false;
if (info.episodes.isNotEmpty) {
_selectedSeason = info.episodes.keys.first;
if (_seriesInfo!.episodes.isNotEmpty) {
final firstKey = _seriesInfo!.episodes.keys.first;
_selectedSeason = int.tryParse(firstKey.toString()) ?? 1;
}
});
}
@@ -137,10 +139,10 @@ class _MobileSeriesDetailScreenState
background: Stack(
fit: StackFit.expand,
children: [
if (_seriesInfo!.cover != null)
if (_seriesInfo!.coverUrl != null)
// [P1-2 FIX] Optimize series cover image cache (400x600 display)
CachedNetworkImage(
imageUrl: _seriesInfo!.cover!,
imageUrl: _seriesInfo!.coverUrl!,
fit: BoxFit.cover,
memCacheWidth: 480,
memCacheHeight: 720,
@@ -175,7 +177,7 @@ class _MobileSeriesDetailScreenState
mainAxisSize: MainAxisSize.min,
children: [
Text(
_seriesInfo!.name,
_seriesInfo!.title,
style: GoogleFonts.inter(
fontSize: 24,
fontWeight: FontWeight.bold,
@@ -224,7 +226,7 @@ class _MobileSeriesDetailScreenState
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
_seriesInfo!.plot!,
_seriesInfo?.plot ?? '',
style: const TextStyle(
color: AppColors.textSecondary,
height: 1.4,
@@ -256,7 +258,7 @@ class _MobileSeriesDetailScreenState
selected: isSelected,
onSelected: (selected) {
if (selected) {
setState(() => _selectedSeason = seasonNum);
setState(() => _selectedSeason = int.tryParse(seasonNum.toString()) ?? 1);
}
},
backgroundColor: isDark
@@ -304,7 +306,7 @@ class _MobileSeriesDetailScreenState
);
}
Widget _buildEpisodeTile(Episode episode) {
Widget _buildEpisodeTile(model.Episode episode) {
final watchHistory = ref.watch(mobileWatchHistoryProvider);
final episodeKey = MobileWatchHistory.episodeKey(
widget.series.seriesId,
@@ -316,10 +318,9 @@ class _MobileSeriesDetailScreenState
// Calculate progress
double progress = 0.0;
if (episode.durationSecs != null &&
episode.durationSecs! > 0 &&
if (episode.duration > 0 &&
resumePos > 0) {
progress = (resumePos / episode.durationSecs!).clamp(0.0, 1.0);
progress = (resumePos / episode.duration).clamp(0.0, 1.0);
}
// Force dark mode colors since background is hardcoded dark
@@ -335,14 +336,14 @@ class _MobileSeriesDetailScreenState
context,
MaterialPageRoute(
builder: (context) => NativePlayerScreen(
streamId: episode.streamId,
streamId: episode.id,
title:
"${widget.series.name} S${episode.seasonNum}E${episode.episodeNum}",
"${widget.series.name} S${episode.season}E${episode.episodeNum}",
playlist: widget.playlist,
streamType: StreamType.series,
containerExtension: episode.containerExtension ?? 'mp4',
streamType: model.StreamType.series,
containerExtension: episode.containerExtension,
seriesId: widget.series.seriesId,
season: episode.seasonNum,
season: int.tryParse(episode.season.toString()),
episodeNum: episode.episodeNum,
// Pass explicit initial position for resume
initialPosition:
@@ -394,10 +395,9 @@ class _MobileSeriesDetailScreenState
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
if (episode.durationSecs != null &&
episode.durationSecs! > 0)
if (episode.duration > 0)
Text(
_formatDuration(episode.durationSecs!),
_formatDuration(episode.duration),
style: TextStyle(
color: secondaryTextColor,
fontSize: 12,
@@ -5,30 +5,29 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:media_kit/media_kit.dart';
import 'package:media_kit_video/media_kit_video.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
import 'package:xtremflow/mobile/widgets/tv_focusable.dart';
import 'package:xtremobile/mobile/widgets/tv_focusable.dart';
import 'package:path_provider/path_provider.dart';
import 'package:xtremflow/core/models/iptv_models.dart';
import 'package:xtremflow/core/models/playlist_config.dart';
import 'package:xtremflow/features/iptv/services/xtream_service_mobile.dart';
import 'package:xtremflow/mobile/providers/mobile_settings_providers.dart';
import 'package:xtremflow/core/theme/app_colors.dart';
import 'package:xtremflow/features/iptv/screens/lite_player_screen.dart';
import 'package:xtremobile/core/models/iptv_models.dart' as model;
import 'package:xtremobile/core/models/playlist_config.dart';
import 'package:xtremobile/features/iptv/services/xtream_service_mobile.dart';
import 'package:xtremobile/mobile/providers/mobile_settings_providers.dart';
import 'package:xtremobile/core/theme/app_colors.dart';
import 'package:xtremobile/features/iptv/screens/lite_player_screen.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:xtremflow/mobile/providers/mobile_xtream_providers.dart';
import 'package:xtremflow/core/utils/device_info.dart';
import 'package:xtremobile/core/utils/device_info.dart';
/// Stream type enum for player
enum StreamType { live, vod, series }
/// Stream type enum for player
// (Moved to global or imported from iptv_models if available)
/// Native video player screen for Android/iOS
/// Uses media_kit (FFmpeg) for full codec support (AC3, EAC3, DTS, etc.)
class NativePlayerScreen extends ConsumerStatefulWidget {
final String streamId;
final String title;
final StreamType streamType;
final model.StreamType streamType;
final String? containerExtension;
final PlaylistConfig playlist;
final List<Channel>? channels;
final List<model.Channel>? channels;
final int initialIndex;
// For series episodes (to track watch history)
@@ -160,7 +159,7 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen>
// Unified Buffering Logic (Live vs VOD)
(_player.platform as dynamic)?.setProperty('cache', 'yes');
if (widget.streamType == StreamType.live) {
if (widget.streamType == model.StreamType.live) {
// LIVE PROFILE: Increased stability, no longer low-latency (prevents cuts)
debugPrint(
'MediaKitPlayer: Applying LIVE optimization profile (STABILITY)',
@@ -246,7 +245,7 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen>
// Low-latency intentionally removed for stability
/*
if (widget.streamType == StreamType.live) {
if (widget.streamType == model.StreamType.live) {
(_player.platform as dynamic)?.setProperty('profile', 'low-latency');
}
*/
@@ -310,7 +309,7 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen>
_checkAndMarkWatched(position);
// Auto-save position every 10 seconds (resilience against app kill/crash)
if (widget.streamType != StreamType.live && !_hasMarkedAsWatched) {
if (widget.streamType != model.StreamType.live && !_hasMarkedAsWatched) {
final now = DateTime.now();
if (_lastSaveTime == null ||
now.difference(_lastSaveTime!) > const Duration(seconds: 10)) {
@@ -367,7 +366,7 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen>
_player.stream.completed.listen((completed) {
if (completed && mounted) {
// Logic for Live TV
if (widget.streamType == StreamType.live) {
if (widget.streamType == model.StreamType.live) {
debugPrint(
'MediaKitPlayer: Live stream completed unexpectedly, attempting reconnect...',
);
@@ -431,7 +430,7 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen>
if (state == AppLifecycleState.paused ||
state == AppLifecycleState.inactive ||
state == AppLifecycleState.detached) {
if (widget.streamType == StreamType.live) {
if (widget.streamType == model.StreamType.live) {
// Force stop for Live TV
_player.stop();
} else {
@@ -490,7 +489,7 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen>
/// Get content ID for resume position storage
String get _contentId {
if (widget.streamType == StreamType.series &&
if (widget.streamType == model.StreamType.series &&
widget.seriesId != null &&
widget.season != null &&
widget.episodeNum != null) {
@@ -505,7 +504,7 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen>
/// Save resume position when exiting player
void _saveResumePositionOnExit() {
if (widget.streamType == StreamType.live) return;
if (widget.streamType == model.StreamType.live) return;
debugPrint(
'MediaKitPlayer: Saving position - pos: ${_position.inSeconds}s, dur: ${_duration.inSeconds}s, contentId: $_contentId',
@@ -595,7 +594,7 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen>
Future.delayed(Duration(seconds: 2 * _reconnectAttempts), () {
if (mounted) {
// If VOD/Series, try to resume from current position
if (widget.streamType != StreamType.live && _position.inSeconds > 0) {
if (widget.streamType != model.StreamType.live && _position.inSeconds > 0) {
_loadStream(startAt: _position);
} else {
_loadStream();
@@ -608,7 +607,7 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen>
void _checkAndMarkWatched(Duration position) {
// Skip if already marked, live TV, or no duration
if (_hasMarkedAsWatched) return;
if (widget.streamType == StreamType.live) return;
if (widget.streamType == model.StreamType.live) return;
if (_duration.inSeconds <= 0) return;
final progress = position.inSeconds / _duration.inSeconds;
@@ -617,12 +616,12 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen>
_hasMarkedAsWatched = true;
debugPrint('MediaKitPlayer: 80% reached, marking as watched');
if (widget.streamType == StreamType.vod) {
if (widget.streamType == model.StreamType.vod) {
// Mark movie as watched
ref
.read(mobileWatchHistoryProvider.notifier)
.markMovieWatched(widget.streamId);
} else if (widget.streamType == StreamType.series &&
} else if (widget.streamType == model.StreamType.series &&
widget.seriesId != null &&
widget.season != null &&
widget.episodeNum != null) {
@@ -642,7 +641,7 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen>
/// Start watchdog timer for live streams
void _startLiveWatchdog() {
_liveWatchdog?.cancel();
if (widget.streamType != StreamType.live) return;
if (widget.streamType != model.StreamType.live) return;
// Check every 30 seconds if stream is still playing
_liveWatchdog = Timer.periodic(const Duration(seconds: 30), (_) {
@@ -678,7 +677,7 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen>
}
}
ShortEPG? _epg;
model.ShortEPG? _epg;
// ... (existing initState)
@@ -699,7 +698,7 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen>
: widget.streamId;
// Load EPG if Live TV
if (widget.streamType == StreamType.live) {
if (widget.streamType == model.StreamType.live) {
_loadEpg(currentStreamId);
}
@@ -707,9 +706,9 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen>
// Build stream URL based on type
String streamUrl;
if (widget.streamType == StreamType.live) {
if (widget.streamType == model.StreamType.live) {
streamUrl = _xtreamService!.getLiveStreamUrl(currentStreamId);
} else if (widget.streamType == StreamType.vod) {
} else if (widget.streamType == model.StreamType.vod) {
streamUrl = _xtreamService!.getVodStreamUrl(
currentStreamId,
widget.containerExtension ?? 'mp4',
@@ -744,12 +743,12 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen>
});
// Show watchdog for live streams
if (widget.streamType == StreamType.live) {
if (widget.streamType == model.StreamType.live) {
_startLiveWatchdog();
}
// Resume from saved position (VOD/Series only)
if (widget.streamType != StreamType.live) {
if (widget.streamType != model.StreamType.live) {
// Priority 0: Explicit reconnect position (passed from _attemptReconnect)
if (startAt != null && startAt.inSeconds > 0) {
debugPrint(
@@ -835,7 +834,7 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen>
if (_isPlaying) {
_player.pause();
// Save position on pause
if (widget.streamType != StreamType.live) {
if (widget.streamType != model.StreamType.live) {
ref
.read(mobileWatchHistoryProvider.notifier)
.saveResumePosition(_contentId, _position.inSeconds);
@@ -913,7 +912,7 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen>
// Left Arrow - Show controls or navigate, seek in VOD when hidden
if (key == LogicalKeyboardKey.arrowLeft) {
if (!_showControls) {
if (widget.streamType != StreamType.live) {
if (widget.streamType != model.StreamType.live) {
final newPos = _position - const Duration(seconds: 10);
_player.seek(newPos < Duration.zero ? Duration.zero : newPos);
}
@@ -926,7 +925,7 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen>
// Right Arrow - Show controls or navigate, seek in VOD when hidden
if (key == LogicalKeyboardKey.arrowRight) {
if (!_showControls) {
if (widget.streamType != StreamType.live) {
if (widget.streamType != model.StreamType.live) {
final newPos = _position + const Duration(seconds: 10);
if (newPos < _duration) {
_player.seek(newPos);
@@ -950,7 +949,7 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen>
// Channel Down = Previous Channel (inverted as requested)
if (key == LogicalKeyboardKey.channelDown) {
if (widget.streamType == StreamType.live && widget.channels != null) {
if (widget.streamType == model.StreamType.live && widget.channels != null) {
_playPrevious();
_onUserInteraction();
return true;
@@ -959,7 +958,7 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen>
// Channel Up = Next Channel (inverted as requested)
if (key == LogicalKeyboardKey.channelUp) {
if (widget.streamType == StreamType.live && widget.channels != null) {
if (widget.streamType == model.StreamType.live && widget.channels != null) {
_playNext();
_onUserInteraction();
return true;
@@ -1128,7 +1127,7 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen>
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
if (widget.streamType == StreamType.live)
if (widget.streamType == model.StreamType.live)
Text(
_currentTime,
style: const TextStyle(
@@ -1387,7 +1386,7 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen>
}
Widget _buildControlsOverlay(String title) {
if (widget.streamType == StreamType.live) {
if (widget.streamType == model.StreamType.live) {
return _buildUnifiedOSD(title);
}