feat: add Dart server for API, Xtream proxy, and FFmpeg streaming, updating Dockerfile for ffmpeg support.

This commit is contained in:
Michael committed 2025-12-06 11:49:39 +01:00
1 parent 0ed1d19ea0
commit 39dabfa7b5
4 files changed
+297 -55

No files matched your search

+106 -47
View File
@@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'dart:html' as html;
import 'dart:ui_web' as ui_web;
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../providers/xtream_provider.dart';
import '../widgets/epg_overlay.dart';
import '../../../core/models/playlist_config.dart';
@@ -9,7 +11,7 @@ import '../../../core/models/playlist_config.dart';
/// Stream type enum for player
enum StreamType { live, vod, series }
/// Video player screen using HTML5 player with mpegts.js for TS streams
/// Video player screen using FFmpeg transcoding for live streams
class PlayerScreen extends ConsumerStatefulWidget {
final String streamId;
final String title;
@@ -31,10 +33,12 @@ class PlayerScreen extends ConsumerStatefulWidget {
}
class _PlayerScreenState extends ConsumerState<PlayerScreen> {
String? _streamUrl;
String? _hlsUrl;
String? _errorMessage;
String? _statusMessage;
late String _viewId;
bool _isInitialized = false;
bool _isLoading = true;
@override
void initState() {
@@ -43,8 +47,13 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
_initializePlayer();
}
void _initializePlayer() {
Future<void> _initializePlayer() async {
try {
setState(() {
_isLoading = true;
_statusMessage = 'Validating playlist...';
});
// Validate playlist before proceeding
if (widget.playlist.dns.isEmpty ||
widget.playlist.username.isEmpty ||
@@ -53,35 +62,57 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
}
final xtreamService = ref.read(xtreamServiceProvider(widget.playlist));
// Explicitly ensure playlist is set (defensive programming)
xtreamService.setPlaylist(widget.playlist);
// Generate stream URL based on type
late String streamUrl;
switch (widget.streamType) {
case StreamType.live:
streamUrl = xtreamService.getLiveStreamUrl(widget.streamId);
break;
case StreamType.vod:
streamUrl = xtreamService.getVodStreamUrl(
widget.streamId,
widget.containerExtension,
);
break;
case StreamType.series:
streamUrl = xtreamService.getSeriesStreamUrl(
widget.streamId,
widget.containerExtension,
);
break;
String hlsUrl;
if (widget.streamType == StreamType.live) {
// For live streams, use FFmpeg transcoding
setState(() {
_statusMessage = 'Starting stream transcoding...';
});
// Build the FFmpeg endpoint URL
final baseUrl = html.window.location.origin;
final iptvUrl = '${widget.playlist.dns}/live/${widget.playlist.username}/${widget.playlist.password}/${widget.streamId}.ts';
final encodedUrl = Uri.encodeComponent(iptvUrl);
final streamEndpoint = '$baseUrl/api/stream/${widget.streamId}?url=$encodedUrl';
debugPrint('PlayerScreen: Starting FFmpeg stream: $streamEndpoint');
// Call the FFmpeg streaming endpoint
final response = await http.get(Uri.parse(streamEndpoint));
if (response.statusCode != 200) {
throw Exception('Failed to start stream: ${response.body}');
}
final data = jsonDecode(response.body);
if (data['status'] != 'started') {
throw Exception('Stream failed to start: ${data['error'] ?? 'Unknown error'}');
}
// Get the local HLS URL
hlsUrl = '$baseUrl${data['hlsUrl']}';
debugPrint('PlayerScreen: FFmpeg HLS URL: $hlsUrl');
} else {
// For VOD and Series, use direct proxy (they usually work)
final streamUrl = widget.streamType == StreamType.vod
? xtreamService.getVodStreamUrl(widget.streamId, widget.containerExtension)
: xtreamService.getSeriesStreamUrl(widget.streamId, widget.containerExtension);
hlsUrl = streamUrl;
}
debugPrint('PlayerScreen: Initializing stream URL: $streamUrl');
setState(() {
_statusMessage = 'Loading player...';
});
// Register the HTML view with an iframe pointing to our player.html
final encodedUrl = Uri.encodeComponent(streamUrl);
final playerUrl = 'player.html?url=$encodedUrl';
final encodedHlsUrl = Uri.encodeComponent(hlsUrl);
final playerUrl = 'player.html?url=$encodedHlsUrl';
debugPrint('PlayerScreen: Player URL: $playerUrl');
// Register platform view factory
// ignore: undefined_prefixed_name
@@ -100,11 +131,14 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
);
setState(() {
_streamUrl = streamUrl;
_hlsUrl = hlsUrl;
_isInitialized = true;
_isLoading = false;
});
} catch (e) {
debugPrint('PlayerScreen error: $e');
setState(() {
_isLoading = false;
_errorMessage = e.toString();
});
}
@@ -147,30 +181,55 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
textAlign: TextAlign.center,
),
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: () {
setState(() {
_errorMessage = null;
_isLoading = true;
});
_initializePlayer();
},
child: const Text('Retry'),
),
],
),
)
: !_isInitialized
? const Center(
child: CircularProgressIndicator(),
)
: Stack(
children: [
// HTML5 video player via iframe
HtmlElementView(viewType: _viewId),
// EPG overlay for live streams
if (widget.streamType == StreamType.live)
Positioned(
bottom: 80,
left: 0,
right: 0,
child: EpgOverlay(
streamId: widget.streamId,
playlist: widget.playlist,
),
: _isLoading
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const CircularProgressIndicator(),
const SizedBox(height: 16),
Text(
_statusMessage ?? 'Loading...',
style: const TextStyle(color: Colors.white70),
),
],
),
],
),
)
: !_isInitialized
? const Center(
child: CircularProgressIndicator(),
)
: Stack(
children: [
// HTML5 video player via iframe
HtmlElementView(viewType: _viewId),
// EPG overlay for live streams
if (widget.streamType == StreamType.live)
Positioned(
bottom: 80,
left: 0,
right: 0,
child: EpgOverlay(
streamId: widget.streamId,
playlist: widget.playlist,
),
),
],
),
);
}
}
+10 -5
View File
@@ -52,15 +52,20 @@ class XtreamService {
_currentPlaylist = playlist;
}
/// Generate stream URL for live TV
/// Generate stream URL for live TV via FFmpeg transcoding
///
/// Uses .m3u8 format for HLS streaming (compatible with hls.js player)
/// Returns the start URL for FFmpeg transcoding endpoint.
/// The endpoint will start FFmpeg and return the local HLS URL.
String getLiveStreamUrl(String streamId) {
if (_currentPlaylist == null) throw Exception('No playlist configured');
// Use .m3u8 format for HLS streaming - the player.html handles this via hls.js
final url = '${_currentPlaylist!.dns}/live/${_currentPlaylist!.username}/${_currentPlaylist!.password}/$streamId.m3u8';
return _wrapWithProxy(url);
// Use .ts format for direct MPEG-TS streaming (FFmpeg handles this better)
final iptvUrl = '${_currentPlaylist!.dns}/live/${_currentPlaylist!.username}/${_currentPlaylist!.password}/$streamId.ts';
// Use FFmpeg transcoding endpoint
final baseUrl = html.window.location.origin;
final encodedUrl = Uri.encodeComponent(iptvUrl);
return '$baseUrl/api/stream/$streamId?url=$encodedUrl';
}
/// Generate stream URL for VOD (movies)