mirror of
https://github.com/R0m1k3/xtremflow.git
synced 2026-10-23 23:18:06 +02:00
feat: implement IPTV player using a web-based lite player and a dedicated Flutter screen.
This commit is contained in:
1 parent
211699d17e
commit
b589a834a0
3 files changed
+107
-31
No files matched your search
+1
-1
@@ -19,7 +19,7 @@ RUN flutter config --enable-web
|
|||||||
# Copy dependency files first for better caching
|
# Copy dependency files first for better caching
|
||||||
# Note: pubspec.lock will be generated by flutter pub get if missing
|
# Note: pubspec.lock will be generated by flutter pub get if missing
|
||||||
# Cache buster - INCREMENT THIS VALUE TO FORCE REBUILD ON DEPLOY
|
# Cache buster - INCREMENT THIS VALUE TO FORCE REBUILD ON DEPLOY
|
||||||
ARG CACHEBUST=2025-12-13-v6
|
ARG CACHEBUST=2025-12-13-v7
|
||||||
COPY pubspec.yaml ./
|
COPY pubspec.yaml ./
|
||||||
|
|
||||||
# Get dependencies (generates pubspec.lock automatically)
|
# Get dependencies (generates pubspec.lock automatically)
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
|
|||||||
String? _currentStreamUrl;
|
String? _currentStreamUrl;
|
||||||
String _statusMessage = 'Loading...';
|
String _statusMessage = 'Loading...';
|
||||||
String? _errorMessage;
|
String? _errorMessage;
|
||||||
|
bool _isMuted = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -115,8 +116,9 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
|
|||||||
|
|
||||||
final encodedUrl = Uri.encodeComponent(streamUrl);
|
final encodedUrl = Uri.encodeComponent(streamUrl);
|
||||||
final playerSettings = ref.read(iptvSettingsProvider);
|
final playerSettings = ref.read(iptvSettingsProvider);
|
||||||
|
final streamTypeParam = widget.streamType == StreamType.live ? 'live' : 'vod';
|
||||||
var playerSrc = playerSettings.playerType == PlayerType.lite
|
var playerSrc = playerSettings.playerType == PlayerType.lite
|
||||||
? 'player_lite.html?url=$encodedUrl'
|
? 'player_lite.html?url=$encodedUrl&type=$streamTypeParam'
|
||||||
: 'player.html?url=$encodedUrl';
|
: 'player.html?url=$encodedUrl';
|
||||||
|
|
||||||
if (startTimeOverride != null) {
|
if (startTimeOverride != null) {
|
||||||
@@ -140,6 +142,13 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
|
|||||||
_isInitialized = true;
|
_isInitialized = true;
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Force unmute on load (after a short delay to ensure iframe is ready)
|
||||||
|
Future.delayed(const Duration(milliseconds: 500), () {
|
||||||
|
if (mounted) {
|
||||||
|
_sendMessage({'type': 'set_volume', 'value': 1.0});
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
@@ -218,6 +227,11 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _toggleMute() {
|
||||||
|
setState(() => _isMuted = !_isMuted);
|
||||||
|
_sendMessage({'type': 'set_volume', 'value': _isMuted ? 0.0 : 1.0});
|
||||||
|
}
|
||||||
|
|
||||||
void _previousChannel() {
|
void _previousChannel() {
|
||||||
if (widget.channels != null && widget.channels!.isNotEmpty) {
|
if (widget.channels != null && widget.channels!.isNotEmpty) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -298,10 +312,29 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
// Mute/Unmute Button
|
||||||
|
Material(
|
||||||
|
color: Colors.black54,
|
||||||
|
shape: const CircleBorder(),
|
||||||
|
child: InkWell(
|
||||||
|
onTap: _toggleMute,
|
||||||
|
customBorder: const CircleBorder(),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
child: Icon(
|
||||||
|
_isMuted ? Icons.volume_off_rounded : Icons.volume_up_rounded,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
widget.title,
|
widget.channels != null
|
||||||
|
? widget.channels![_currentIndex].name
|
||||||
|
: widget.title,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
|
|||||||
+71
-28
@@ -108,6 +108,7 @@
|
|||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
let streamUrl = urlParams.get('url');
|
let streamUrl = urlParams.get('url');
|
||||||
const startTime = parseFloat(urlParams.get('t') || '0');
|
const startTime = parseFloat(urlParams.get('t') || '0');
|
||||||
|
const streamType = urlParams.get('type') || ''; // 'live' or 'vod'
|
||||||
|
|
||||||
function log(msg) {
|
function log(msg) {
|
||||||
console.log('[LitePlayer] ' + msg);
|
console.log('[LitePlayer] ' + msg);
|
||||||
@@ -289,33 +290,60 @@
|
|||||||
showLoading('Loading video...');
|
showLoading('Loading video...');
|
||||||
|
|
||||||
video.preload = 'auto';
|
video.preload = 'auto';
|
||||||
|
let playStarted = false;
|
||||||
|
|
||||||
video.addEventListener('waiting', () => {
|
video.addEventListener('waiting', () => {
|
||||||
log('Buffering...');
|
log('Buffering...');
|
||||||
showLoading('Buffering...');
|
showLoading('Buffering...');
|
||||||
});
|
});
|
||||||
|
|
||||||
video.addEventListener('playing', hideLoading);
|
video.addEventListener('playing', () => {
|
||||||
|
playStarted = true;
|
||||||
video.addEventListener('canplay', () => {
|
|
||||||
hideLoading();
|
hideLoading();
|
||||||
video.play().catch(e => log('Autoplay prevented: ' + e));
|
|
||||||
}, { once: true });
|
|
||||||
|
|
||||||
video.addEventListener('error', () => {
|
|
||||||
const err = video.error;
|
|
||||||
let msg = 'Playback error';
|
|
||||||
if (err) {
|
|
||||||
switch (err.code) {
|
|
||||||
case MediaError.MEDIA_ERR_NETWORK: msg = 'Network error'; break;
|
|
||||||
case MediaError.MEDIA_ERR_DECODE: msg = 'Video decode error'; break;
|
|
||||||
case MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED: msg = 'Format not supported'; break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
showError(msg);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const onCanPlay = () => {
|
||||||
|
playStarted = true;
|
||||||
|
hideLoading();
|
||||||
|
// FORCE AUDIO SETTINGS
|
||||||
|
video.muted = false;
|
||||||
|
video.volume = 1.0;
|
||||||
|
|
||||||
|
video.play().catch(e => {
|
||||||
|
log('Autoplay prevented: ' + e);
|
||||||
|
// If autoplay prevents sound/video, we should show a UI to user but native controls are there
|
||||||
|
// Try playing muted if failed
|
||||||
|
video.muted = true;
|
||||||
|
video.play().then(() => {
|
||||||
|
log('Autoplay started muted');
|
||||||
|
showError('Click to unmute'); // User can verify context
|
||||||
|
}).catch(e2 => log('Autoplay failed completely: ' + e2));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onError = async () => {
|
||||||
|
if (!playStarted) {
|
||||||
|
log('Direct playback failed, trying fallbacks...');
|
||||||
|
video.removeEventListener('canplay', onCanPlay);
|
||||||
|
video.removeEventListener('error', onError);
|
||||||
|
|
||||||
|
// Fallback 1: HLS (common for streams)
|
||||||
|
log('Trying HLS fallback...');
|
||||||
|
if (await playWithHLS(url)) return;
|
||||||
|
|
||||||
|
// Fallback 2: MpegTS (for TS streams)
|
||||||
|
log('Comparing HLS failed, trying MpegTS fallback...');
|
||||||
|
if (await playWithMpegTS(url)) return;
|
||||||
|
|
||||||
|
showError('Format not supported. Try Standard player mode.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
video.addEventListener('canplay', onCanPlay, { once: true });
|
||||||
|
video.addEventListener('error', onError, { once: true });
|
||||||
|
|
||||||
video.src = url;
|
video.src = url;
|
||||||
|
video.load();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,20 +354,35 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
log('Stream URL: ' + streamUrl);
|
log('Stream URL: ' + streamUrl);
|
||||||
|
log('Stream type: ' + streamType);
|
||||||
const lowerUrl = streamUrl.toLowerCase();
|
const lowerUrl = streamUrl.toLowerCase();
|
||||||
|
|
||||||
// Route to appropriate player
|
// Use type parameter if available, otherwise detect from URL
|
||||||
if (lowerUrl.includes('.m3u8') || lowerUrl.includes('/hls/')) {
|
if (streamType === 'vod') {
|
||||||
log('Detected HLS stream');
|
// VOD content - use direct HTML5 playback
|
||||||
await playWithHLS(streamUrl);
|
log('VOD mode: Using direct playback');
|
||||||
} else if (lowerUrl.includes('.ts') || lowerUrl.includes('/live/') || lowerUrl.includes('mpegts')) {
|
|
||||||
log('Detected MPEG-TS (via extension or URL)');
|
|
||||||
log('Using mpegts.js');
|
|
||||||
await playWithMpegTS(streamUrl);
|
|
||||||
} else {
|
|
||||||
// Try direct playback (mp4, etc.)
|
|
||||||
log('Trying direct playback');
|
|
||||||
await playDirect(streamUrl);
|
await playDirect(streamUrl);
|
||||||
|
} else if (streamType === 'live') {
|
||||||
|
// Live TV - check for HLS first, then MPEG-TS
|
||||||
|
if (lowerUrl.includes('.m3u8') || lowerUrl.includes('/hls/')) {
|
||||||
|
log('Live HLS detected');
|
||||||
|
await playWithHLS(streamUrl);
|
||||||
|
} else {
|
||||||
|
log('Live MPEG-TS mode');
|
||||||
|
await playWithMpegTS(streamUrl);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback: detect from URL pattern
|
||||||
|
if (lowerUrl.includes('.m3u8') || lowerUrl.includes('/hls/')) {
|
||||||
|
await playWithHLS(streamUrl);
|
||||||
|
} else if (lowerUrl.includes('.mp4') || lowerUrl.includes('.mkv') ||
|
||||||
|
lowerUrl.includes('/movie/') || lowerUrl.includes('/series/')) {
|
||||||
|
await playDirect(streamUrl);
|
||||||
|
} else if (lowerUrl.includes('.ts') || lowerUrl.includes('/live/')) {
|
||||||
|
await playWithMpegTS(streamUrl);
|
||||||
|
} else {
|
||||||
|
await playDirect(streamUrl);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user