mirror of
https://github.com/R0m1k3/xtremobile.git
synced 2026-10-11 17:29:26 +02:00
perf: anti micro-stutter buffer optimizations for live TV
This commit is contained in:
1 parent
4e694a2cc0
commit
02d13404fd
5 files changed
+404
-277
No files matched your search
@@ -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';
|
||||||
@@ -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 '';
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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
Reference in new issue
Block a user