feat: Add IPTV player screen for stream playback with controls, EPG integration, and channel switching.

This commit is contained in:
Michael committed 2025-12-13 13:51:04 +01:00
1 parent 67e59b804c
commit ddc0ea64e0
2 files changed
+134 -23

No files matched your search

+116 -22
View File
@@ -70,6 +70,9 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
_currentIndex = widget.channels!.indexWhere((c) => c.streamId == widget.streamId); _currentIndex = widget.channels!.indexWhere((c) => c.streamId == widget.streamId);
if (_currentIndex == -1) _currentIndex = 0; if (_currentIndex == -1) _currentIndex = 0;
} }
// Show controls at start, then auto-hide after timeout
_showControls = true;
_onHover(); // Start the auto-hide timer
_initializePlayer(startTimeOverride: widget.startTime); _initializePlayer(startTimeOverride: widget.startTime);
_setupMessageListener(); _setupMessageListener();
} }
@@ -253,36 +256,127 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final settings = ref.watch(iptvSettingsProvider); final settings = ref.watch(iptvSettingsProvider);
// LITE PLAYER MODE: Simple iframe with native HTML5 controls, no Flutter overlay // LITE PLAYER MODE: Simple iframe with native HTML5 controls, minimal Flutter overlay
if (settings.playerType == PlayerType.lite) { if (settings.playerType == PlayerType.lite) {
return Scaffold( return Scaffold(
backgroundColor: Colors.black, backgroundColor: Colors.black,
body: Stack( body: MouseRegion(
fit: StackFit.expand, onHover: (_) => _onHover(),
children: [ child: GestureDetector(
// Video Player (iframe with native controls) - player_lite.html has its own loading indicator onTap: () {
if (_isInitialized) if (_showControls) {
HtmlElementView(viewType: _viewId), setState(() => _showControls = false);
} else {
_onHover();
}
},
child: Stack(
fit: StackFit.expand,
children: [
// Video Player (iframe with native controls)
if (_isInitialized)
HtmlElementView(viewType: _viewId),
// Simple Back Button - wrapped in PointerInterceptor to work above iframe // Top Bar: Back Button + Title (with auto-hide)
Positioned( AnimatedPositioned(
top: 24, duration: const Duration(milliseconds: 200),
left: 24, top: _showControls ? 24 : -80,
child: PointerInterceptor( left: 24,
child: GestureDetector( right: 24,
onTap: () => Navigator.pop(context), child: PointerInterceptor(
child: Container( child: Row(
padding: const EdgeInsets.all(12), children: [
decoration: BoxDecoration( Material(
color: Colors.black54, color: Colors.black54,
shape: BoxShape.circle, shape: const CircleBorder(),
child: InkWell(
onTap: () => Navigator.pop(context),
customBorder: const CircleBorder(),
child: const Padding(
padding: EdgeInsets.all(12),
child: Icon(Icons.arrow_back_rounded, color: Colors.white),
),
),
),
const SizedBox(width: 16),
Expanded(
child: Text(
widget.title,
style: const TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
shadows: [Shadow(blurRadius: 4, color: Colors.black)],
),
overflow: TextOverflow.ellipsis,
),
),
],
), ),
child: const Icon(Icons.arrow_back_rounded, color: Colors.white),
), ),
), ),
),
// Channel Zapping Controls (with auto-hide)
if (widget.channels != null && widget.channels!.length > 1)
AnimatedPositioned(
duration: const Duration(milliseconds: 200),
bottom: 100,
right: _showControls ? 24 : -80,
child: PointerInterceptor(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Previous Channel
Material(
color: Colors.black54,
shape: const CircleBorder(),
child: InkWell(
onTap: _previousChannel,
customBorder: const CircleBorder(),
child: const Padding(
padding: EdgeInsets.all(14),
child: Icon(Icons.keyboard_arrow_up, color: Colors.white, size: 28),
),
),
),
const SizedBox(height: 12),
// Next Channel
Material(
color: Colors.black54,
shape: const CircleBorder(),
child: InkWell(
onTap: _nextChannel,
customBorder: const CircleBorder(),
child: const Padding(
padding: EdgeInsets.all(14),
child: Icon(Icons.keyboard_arrow_down, color: Colors.white, size: 28),
),
),
),
],
),
),
),
// EPG Overlay (for Live TV, with auto-hide, higher position)
if (widget.streamType == StreamType.live)
AnimatedPositioned(
duration: const Duration(milliseconds: 200),
bottom: _showControls ? 60 : -120,
left: 0,
right: 0,
child: PointerInterceptor(
child: EpgOverlay(
playlist: widget.playlist,
streamId: widget.channels != null
? widget.channels![_currentIndex].streamId
: widget.streamId,
),
),
),
],
), ),
], ),
), ),
); );
} }
+18 -1
View File
@@ -169,6 +169,23 @@
}, '*'); }, '*');
}); });
// Report user activity (mouse movement or touch) to parent for auto-hide controls
let lastActivityTime = 0;
function reportActivity() {
const now = Date.now();
if (now - lastActivityTime > 100) { // Throttle to 100ms
lastActivityTime = now;
window.parent.postMessage({
type: 'user_activity'
}, '*');
}
}
document.addEventListener('mousemove', reportActivity);
document.addEventListener('touchstart', reportActivity);
document.addEventListener('touchmove', reportActivity);
document.addEventListener('click', reportActivity);
document.addEventListener('touchend', reportActivity);
async function playWithHLS(url) { async function playWithHLS(url) {
log('Trying HLS.js with: ' + url); log('Trying HLS.js with: ' + url);
showLoading('Loading HLS stream...'); showLoading('Loading HLS stream...');
@@ -331,4 +348,4 @@
</script> </script>
</body> </body>
</html> </html>