perf: anti micro-stutter buffer optimizations for live TV

This commit is contained in:
Michael committed 2025-12-16 21:35:47 +01:00
1 parent 4e694a2cc0
commit 02d13404fd
5 files changed
+404 -277

No files matched your search

+4
View File
@@ -0,0 +1,4 @@
/// Platform utilities - conditional export
/// Uses web implementation on web, stub on other platforms
export 'platform_utils_stub.dart'
if (dart.library.html) 'platform_utils_web.dart';
+14
View File
@@ -0,0 +1,14 @@
/// Platform-specific utilities for web
/// This file is used when compiling for web
/// Check if we're running on HTTPS
bool isHttps() {
// Implemented in platform_utils_web.dart
return true;
}
/// Get the current window origin
String getWindowOrigin() {
// Implemented in platform_utils_web.dart
return '';
}
+13
View File
@@ -0,0 +1,13 @@
/// Platform-specific utilities for web
/// This file is used when compiling for web
import 'dart:html' as html;
/// Check if we're running on HTTPS
bool isHttps() {
return html.window.location.protocol == 'https:';
}
/// Get the current window origin
String getWindowOrigin() {
return html.window.location.origin;
}
+47 -35
View File
@@ -4,22 +4,24 @@ import 'package:dio_cache_interceptor_hive_store/dio_cache_interceptor_hive_stor
import '../../../core/models/playlist_config.dart'; import '../../../core/models/playlist_config.dart';
import '../../../core/models/iptv_models.dart'; import '../../../core/models/iptv_models.dart';
import '../models/xtream_models.dart' as xm; import '../models/xtream_models.dart' as xm;
import 'dart:html' as html; import '../../../core/utils/platform_utils.dart' as platform;
/// Xtream Codes API Service /// Xtream Codes API Service
/// ///
/// Handles all communication with Xtream API servers /// Handles all communication with Xtream API servers
class XtreamService { class XtreamService {
late final Dio _dio; late final Dio _dio;
late final CacheOptions _cacheOptions; late final CacheOptions _cacheOptions;
PlaylistConfig? _currentPlaylist; PlaylistConfig? _currentPlaylist;
XtreamService() { XtreamService() {
_dio = Dio(BaseOptions( _dio = Dio(
connectTimeout: const Duration(seconds: 15), BaseOptions(
receiveTimeout: const Duration(seconds: 15), connectTimeout: const Duration(seconds: 15),
),); receiveTimeout: const Duration(seconds: 15),
),
);
// Setup caching for API responses // Setup caching for API responses
_cacheOptions = CacheOptions( _cacheOptions = CacheOptions(
@@ -34,14 +36,14 @@ class XtreamService {
/// Check if we're running on HTTPS (production) /// Check if we're running on HTTPS (production)
bool get _isHttps { bool get _isHttps {
return html.window.location.protocol == 'https:'; return platform.isHttps();
} }
/// Wrap URL with proxy if needed (for HTTPS to HTTP bridge) /// Wrap URL with proxy if needed (for HTTPS to HTTP bridge)
String _wrapWithProxy(String url) { String _wrapWithProxy(String url) {
// If we're on HTTPS and the target URL is HTTP, use the proxy // If we're on HTTPS and the target URL is HTTP, use the proxy
if (_isHttps && url.startsWith('http://')) { if (_isHttps && url.startsWith('http://')) {
final baseUrl = html.window.location.origin; final baseUrl = platform.getWindowOrigin();
return '$baseUrl/api/xtream/$url'; return '$baseUrl/api/xtream/$url';
} }
return url; return url;
@@ -53,17 +55,18 @@ class XtreamService {
} }
/// Generate stream URL for live TV via FFmpeg transcoding /// Generate stream URL for live TV via FFmpeg transcoding
/// ///
/// Returns the start URL for FFmpeg transcoding endpoint. /// Returns the start URL for FFmpeg transcoding endpoint.
/// The endpoint will start FFmpeg and return the local HLS URL. /// The endpoint will start FFmpeg and return the local HLS URL.
String getLiveStreamUrl(String streamId) { String getLiveStreamUrl(String streamId) {
if (_currentPlaylist == null) throw Exception('No playlist configured'); if (_currentPlaylist == null) throw Exception('No playlist configured');
// Use .ts format for direct MPEG-TS streaming (FFmpeg handles this better) // Use .ts format for direct MPEG-TS streaming (FFmpeg handles this better)
final iptvUrl = '${_currentPlaylist!.dns}/live/${_currentPlaylist!.username}/${_currentPlaylist!.password}/$streamId.ts'; final iptvUrl =
'${_currentPlaylist!.dns}/live/${_currentPlaylist!.username}/${_currentPlaylist!.password}/$streamId.ts';
// Use FFmpeg transcoding endpoint // Use FFmpeg transcoding endpoint
final baseUrl = html.window.location.origin; final baseUrl = platform.getWindowOrigin();
final encodedUrl = Uri.encodeComponent(iptvUrl); final encodedUrl = Uri.encodeComponent(iptvUrl);
return '$baseUrl/api/stream/$streamId?url=$encodedUrl'; return '$baseUrl/api/stream/$streamId?url=$encodedUrl';
} }
@@ -71,16 +74,18 @@ class XtreamService {
/// Generate stream URL for VOD (movies) /// Generate stream URL for VOD (movies)
String getVodStreamUrl(String streamId, String containerExtension) { String getVodStreamUrl(String streamId, String containerExtension) {
if (_currentPlaylist == null) throw Exception('No playlist configured'); if (_currentPlaylist == null) throw Exception('No playlist configured');
final url = '${_currentPlaylist!.dns}/movie/${_currentPlaylist!.username}/${_currentPlaylist!.password}/$streamId.$containerExtension'; final url =
'${_currentPlaylist!.dns}/movie/${_currentPlaylist!.username}/${_currentPlaylist!.password}/$streamId.$containerExtension';
return _wrapWithProxy(url); return _wrapWithProxy(url);
} }
/// Generate stream URL for series episodes /// Generate stream URL for series episodes
String getSeriesStreamUrl(String streamId, String containerExtension) { String getSeriesStreamUrl(String streamId, String containerExtension) {
if (_currentPlaylist == null) throw Exception('No playlist configured'); if (_currentPlaylist == null) throw Exception('No playlist configured');
final url = '${_currentPlaylist!.dns}/series/${_currentPlaylist!.username}/${_currentPlaylist!.password}/$streamId.$containerExtension'; final url =
'${_currentPlaylist!.dns}/series/${_currentPlaylist!.username}/${_currentPlaylist!.password}/$streamId.$containerExtension';
return _wrapWithProxy(url); return _wrapWithProxy(url);
} }
@@ -138,7 +143,7 @@ class XtreamService {
} }
/// Get all live TV channels grouped by category /// Get all live TV channels grouped by category
/// ///
/// Returns a Map where key is category name and value is list of channels /// Returns a Map where key is category name and value is list of channels
Future<Map<String, List<Channel>>> getLiveChannels() async { Future<Map<String, List<Channel>>> getLiveChannels() async {
if (_currentPlaylist == null) throw Exception('No playlist configured'); if (_currentPlaylist == null) throw Exception('No playlist configured');
@@ -165,10 +170,10 @@ class XtreamService {
// Get category name from our mapping using category_id // Get category name from our mapping using category_id
final categoryId = data['category_id']?.toString() ?? ''; final categoryId = data['category_id']?.toString() ?? '';
final categoryName = categoryMap[categoryId] ?? 'Uncategorized'; final categoryName = categoryMap[categoryId] ?? 'Uncategorized';
// Inject category_name into data before parsing // Inject category_name into data before parsing
data['category_name'] = categoryName; data['category_name'] = categoryName;
final channel = Channel.fromJson(data); final channel = Channel.fromJson(data);
if (!groupedChannels.containsKey(categoryName)) { if (!groupedChannels.containsKey(categoryName)) {
@@ -326,7 +331,8 @@ class XtreamService {
} }
/// Get movies with pagination support /// Get movies with pagination support
Future<List<xm.Movie>> getMoviesPaginated({int offset = 0, int limit = 100}) async { Future<List<xm.Movie>> getMoviesPaginated(
{int offset = 0, int limit = 100}) async {
if (_currentPlaylist == null) throw Exception('No playlist configured'); if (_currentPlaylist == null) throw Exception('No playlist configured');
try { try {
@@ -344,13 +350,15 @@ class XtreamService {
); );
final List<dynamic> allMovies = response.data as List<dynamic>; final List<dynamic> allMovies = response.data as List<dynamic>;
// Apply pagination // Apply pagination
final endIndex = (offset + limit) > allMovies.length ? allMovies.length : offset + limit; final endIndex = (offset + limit) > allMovies.length
? allMovies.length
: offset + limit;
if (offset >= allMovies.length) return []; if (offset >= allMovies.length) return [];
final paginatedMovies = allMovies.sublist(offset, endIndex); final paginatedMovies = allMovies.sublist(offset, endIndex);
return paginatedMovies.map((movieData) { return paginatedMovies.map((movieData) {
final data = movieData as Map<String, dynamic>; final data = movieData as Map<String, dynamic>;
final categoryId = data['category_id']?.toString() ?? ''; final categoryId = data['category_id']?.toString() ?? '';
@@ -382,7 +390,7 @@ class XtreamService {
final List<dynamic> allMovies = response.data as List<dynamic>; final List<dynamic> allMovies = response.data as List<dynamic>;
final queryLower = query.toLowerCase(); final queryLower = query.toLowerCase();
// Filter by search query // Filter by search query
return allMovies return allMovies
.where((m) { .where((m) {
@@ -403,7 +411,8 @@ class XtreamService {
} }
/// Get series with pagination support (returns flat list) /// Get series with pagination support (returns flat list)
Future<List<xm.Series>> getSeriesPaginated({int offset = 0, int limit = 100}) async { Future<List<xm.Series>> getSeriesPaginated(
{int offset = 0, int limit = 100}) async {
if (_currentPlaylist == null) throw Exception('No playlist configured'); if (_currentPlaylist == null) throw Exception('No playlist configured');
try { try {
@@ -421,13 +430,15 @@ class XtreamService {
); );
final List<dynamic> allSeries = response.data as List<dynamic>; final List<dynamic> allSeries = response.data as List<dynamic>;
// Apply pagination // Apply pagination
final endIndex = (offset + limit) > allSeries.length ? allSeries.length : offset + limit; final endIndex = (offset + limit) > allSeries.length
? allSeries.length
: offset + limit;
if (offset >= allSeries.length) return []; if (offset >= allSeries.length) return [];
final paginatedSeries = allSeries.sublist(offset, endIndex); final paginatedSeries = allSeries.sublist(offset, endIndex);
return paginatedSeries.map((seriesData) { return paginatedSeries.map((seriesData) {
final data = seriesData as Map<String, dynamic>; final data = seriesData as Map<String, dynamic>;
final categoryId = data['category_id']?.toString() ?? ''; final categoryId = data['category_id']?.toString() ?? '';
@@ -459,7 +470,7 @@ class XtreamService {
final List<dynamic> allSeries = response.data as List<dynamic>; final List<dynamic> allSeries = response.data as List<dynamic>;
final queryLower = query.toLowerCase(); final queryLower = query.toLowerCase();
// Filter by search query // Filter by search query
return allSeries return allSeries
.where((s) { .where((s) {
@@ -502,7 +513,7 @@ class XtreamService {
} }
/// Get short EPG for a specific stream /// Get short EPG for a specific stream
/// ///
/// Returns "Now" and "Next" program info /// Returns "Now" and "Next" program info
Future<List<EpgEntry>> getShortEpg(String streamId) async { Future<List<EpgEntry>> getShortEpg(String streamId) async {
if (_currentPlaylist == null) throw Exception('No playlist configured'); if (_currentPlaylist == null) throw Exception('No playlist configured');
@@ -529,7 +540,8 @@ class XtreamService {
return []; return [];
} }
final List<dynamic> epgData = response.data['epg_listings'] as List<dynamic>; final List<dynamic> epgData =
response.data['epg_listings'] as List<dynamic>;
return epgData return epgData
.map((entry) => EpgEntry.fromJson(entry as Map<String, dynamic>)) .map((entry) => EpgEntry.fromJson(entry as Map<String, dynamic>))
.toList(); .toList();
File diff suppressed because it is too large. Load diff