zeroratehls 1.0.11
zeroratehls: ^1.0.11 copied to clipboard
A comprehensive Flutter SDK for seamless HLS (HTTP Live Streaming) video playback. This package provides a robust, feature-rich player with advanced capabilities including adaptive bitrate streaming, [...]
zeroratehls #
A robust HLS video player SDK with built-in quality switching, live stream support for flutter.
Features #
- Cross-platform support: Android, iOS, and Linux
- Supports HLS & MP4 video and audio playback
- Adaptive bitrate streaming
- Automatic HLS quality discovery from master manifests
- Startup quality capping with pre-open rendition selection
- Fullscreen pinch-to-fill / stretch-to-fit for video
- Handles live and on-demand streaming
- Preview frames while seeking through the video(Android, iOS,)
- Live stream support with DVR
- Customizable UI components
- Dynamic source switching
- Android and iOS system Now Playing controls
- Linux optimization for TV platforms
Platform Support #
| Platform | Video Playback | Audio Playback | Seek Preview | Picture-in-Picture | Now Playing |
|---|---|---|---|---|---|
| Android | Yes | Yes | Yes | Yes | Yes |
| iOS | Yes | Yes | Yes | No | Yes |
| Linux | Yes | Yes | No | No | No |
Installation #
Add zeroratehls to your pubspec.yaml file:
dependencies:
zeroratehls: ^1.0.11
Run the install command:
flutter pub get
Setup #
Basic Setup #
All platforms require SDK initialization in your main function:
import 'package:zeroratehls/zerorate_hls_player.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
ZerorateHlsSDK.initialize(); // Required for cross-platform support
runApp(MyApp());
}
Android Setup #
For Android, add the following to your AndroidManifest.xml file (required for HTTP streams):
<application
android:usesCleartextTraffic="true"
...>
IOS Setup #
⚠️ Note: iOS requires a minimum deployment target of 14.0 due to ffmpeg_kit_flutter_new No additional setup required
Linux Setup #
Linux support is automatically configured when you call ZerorateHlsSDK.initialize(). No additional setup required.
⚠️ Note: On Linux, seek preview thumbnails are disabled for optimal performance on TV platforms.
Basic Usage #
Video Player
import 'package:zeroratehls/zerorate_hls_player.dart';
ZerorateHlsPlayer(
options: ZerorateHlsPlayerOptions(
appId: 'your-app-id',
region: 'region-code',
src: 'https://example.com/video.m3u8',
type: MediaType.video,
aspectRatio: 16 / 9,
autoplay: true,
),
)
Audio Player
ZerorateHlsPlayer(
options: ZerorateHlsPlayerOptions(
appId: 'your-app-id',
region: 'region-code',
src: 'https://example.com/audio.m3u8',
type: MediaType.audio,
poster: 'https://example.com/poster.jpg',
autoplay: false,
),
)
Configuration Options #
Video Configuration
ZerorateHlsPlayerOptions(
// Required parameters
appId: 'your-app-id',
region: 'region-code',
src: 'https://example.com/video.m3u8',
type: MediaType.video,
// ⭐ IMPORTANT:
// 🔒 User Authentication for Protected Content
// - subscriberId and authUrl are REQUIRED for paid/protected content
// - If accessing a paid or protected stream, you MUST provide these
// - Omitting these for protected content will prevent the stream from loading
// - subscriberId: A unique identifier for the user accessing the stream
// * This is typically a user-specific ID from your authentication system
// * Helps track and validate individual user access
// * Example: user's email, internal user ID, or generated unique token
subscriberId: 'unique-user-identifier',
// - authUrl: The authentication endpoint provided by the content provider
// * This URL is used to validate the user's access to the stream
// * The backend will use the subscriberId to check user permissions
// * Provided by the content/streaming service developer
authUrl: 'https://your-content-provider-auth-endpoint',
// Optional parameters
mediaType: StreamType.onDemand, // or StreamType.live
autoplay: true,
muted: false,
aspectRatio: 16 / 9,
// The placeholder is shown until video playback begins.
// - Use `placeholder` for a custom loading or preview widget
// - Use `poster` for a network image fallback if no custom placeholder is provided
// - `placeholderOnTop: true` keeps it above the player UI without blocking touches
// - `placeholderOnTop: false` keeps it below controls/overlays but above the video surface
// Placeholder Hierarchy
// 1. **Explicitly Provided Placeholder**
// - Highest priority
// - Custom widget you define
// 2. **Poster Image**
// - Used if no custom placeholder is provided
// - Static image representing the video
// Custom placeholder widget
placeholder: YourCustomWidget(),
// Or use a poster image
poster: 'https://example.com/poster.jpg',
placeholderOnTop: true,
// Video specific controls
hlsPlaybackConfig: HlsPlaybackConfig(
maxResolutionHeight: 720,
bufferSizeBytes: 64 * 1024 * 1024,
networkTimeoutSeconds: 15,
),
videoControlsConfig: VideoControlsConfig(
enableSeekPreview: true, // Enable thumbnail preview during seeking
enablePlayPause: true,
enableProgressBar: true,
progressBarColor: Colors.red,
useDefaultControls: true, // Set to false to use your own controls
overlayBuilder: (context, data) {
return Padding(
padding: data.overlayPadding,
child: Stack(
children: [
Align(
alignment: Alignment.topLeft,
child: Text(
data.isFullscreen ? 'Fullscreen Title' : 'Inline Title',
style: const TextStyle(color: Colors.white),
),
),
if (data.isFullscreen)
const Align(
alignment: Alignment.bottomRight,
child: MyScrollableSidePanel(),
),
],
),
);
},
// You can add multiple subtitle files to your video configuration:
// Subtitle Source Types
// - Supports local asset files
// - Supports network subtitle files
subtitles: [
SubtitleSource(
path: 'assets/subtitles/movie_en.srt',
languageCode: 'en',
name: 'English Subtitles'
type: SubtitleSourceType.asset,
),
SubtitleSource(
path: 'http/link_to_subtitle',
languageCode: 'es',
name: 'Spanish Subtitles'
type: SubtitleSourceType.network,
),
],
/// Manual Video Resolution Override
///
/// If provided, these options are used instead of automatic HLS manifest
/// discovery. This is useful when your app/backend already knows the
/// exact rendition URLs and wants to control the menu explicitly.
resolutions: [
VideoResolution(
url: 'https://example.com/video_720p.m3u8',
name: '720p'
),
VideoResolution(
url: 'https://example.com/video_1080p.m3u8',
name: '1080p'
),
]
),
)
Audio Configuration
ZerorateHlsPlayerOptions(
// Required parameters
appId: 'your-app-id',
region: 'region-code',
src: 'https://example.com/audio.m3u8',
type: MediaType.audio,
// Optional parameters
mediaType: StreamType.onDemand,
autoplay: false,
aspectRatio: 16 / 9,
poster: 'https://example.com/poster.jpg',
// Audio specific controls
audioControlsConfiguration: AudioControlsConfiguration(
enableProgressBar: true,
enableProgressText: true,
enableVolumeControl: true,
enableSpeedControl: true, // Allow changing playback speed
progressBarColor: Colors.blue,
textColor: Colors.white,
useDefaultControls: true, // Set to false to use your own controls
),
)
Event Handling #
Listen to player events to respond to changes in playback state:
ZerorateHlsPlayerEventType Enum #
| Event Type | Description | Payload Type | Typical Use Case |
|---|---|---|---|
play |
Triggered when media starts playing | null |
Track when playback begins |
pause |
Triggered when media is paused | null |
Monitor pause interactions |
seeked |
Triggered after seeking to a new position | Duration |
Track user navigation |
bufferingStart |
Indicates the start of buffering | null |
Handle loading states |
bufferingEnd |
Indicates buffering has completed | null |
Resume UI interactions |
error |
Occurs when a playback error happens | Exception |
Error handling and logging |
loaded |
Media source successfully loaded | Duration? |
Prepare player after loading |
positionChanged |
Periodic update of playback position | Duration |
Progress tracking |
completed |
Media playback finished | null |
Handle end of media |
fullscreenChanged |
Fullscreen mode entered or exited | bool |
Sync fullscreen-aware UI |
thumbnailGenerated |
Thumbnail created during seek preview | Map<String, dynamic> |
Custom thumbnail handling |
controlsVisibilityChanged |
Controls visibility toggled | bool |
UI state management |
qualityChanged |
Stream quality changed | VideoQualityOption |
Quality tracking |
volumeChanged |
Volume level modified | double |
Volume control tracking |
audioStateChanged |
Audio-specific state changes | Map<String, dynamic> |
Audio playback monitoring |
streamUnavailable |
Optional HLS startup availability heuristic fired | HlsStreamUnavailableDetails |
Diagnostics or fallback UI |
ZerorateHlsPlayerOptions(
// Basic options
appId: 'your-app-id',
region: 'region-code',
src: 'https://example.com/video.m3u8',
type: MediaType.video,
// Event listeners
eventListeners: {
ZerorateHlsPlayerEventType.play: (event) {
print('Video started playing');
},
ZerorateHlsPlayerEventType.pause: (event) {
print('Video paused');
},
ZerorateHlsPlayerEventType.seeked: (event) {
print('Seeked to position: ${event.data}');
},
ZerorateHlsPlayerEventType.error: (event) {
print('Error occurred: ${event.data}');
},
ZerorateHlsPlayerEventType.streamUnavailable: (event) {
final details = event.data as HlsStreamUnavailableDetails;
print(details.message);
// Prefer backend stream availability status for user-facing offline UI.
},
ZerorateHlsPlayerEventType.completed: (event) {
print('Playback completed');
},
// Only for video with seek preview enabled
ZerorateHlsPlayerEventType.thumbnailGenerated: (event) {
print('Thumbnail generated: ${event.data}');
},
},
// Custom error UI
errorBuilder: (error) => Center(
child: Text('Error: ${error.toString()}'),
),
)
UI Customization #
Custom Video Controls For complete control over the video player UI:
ZerorateHlsPlayerOptions(
// Basic setup
appId: 'your-app-id',
region: 'region-code',
src: 'https://example.com/video.m3u8',
type: MediaType.video,
// Disable default controls
videoControlsConfig: VideoControlsConfig(
useDefaultControls: false,
customControlsBuilder: (context, data) {
return Padding(
padding: data.overlayPadding,
child: Stack(
children: [
Align(
alignment: Alignment.bottomCenter,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.play_arrow),
onPressed: () => data.player.play(),
),
IconButton(
icon: const Icon(Icons.fullscreen),
onPressed: data.toggleFullscreen,
),
if (data.availableQualities.isNotEmpty)
PopupMenuButton<VideoQualityOption>(
onSelected: (quality) => data.selectQuality(quality),
itemBuilder: (context) {
return data.availableQualities.map((quality) {
return PopupMenuItem<VideoQualityOption>(
value: quality,
child: Text(quality.label),
);
}).toList();
},
child: Text(
data.currentQuality?.label ?? 'Quality',
style: const TextStyle(color: Colors.white),
),
),
],
),
),
],
),
);
},
),
)
Adaptive HLS Playback and Quality Discovery
If your backend returns a master .m3u8 URL, the SDK can discover available
variants and, by default, play through a local Dart ABR proxy. The proxy gives
media_kit one stable local playlist while it dynamically chooses which remote
variant segment to serve.
ZerorateHlsPlayerOptions(
appId: 'your-app-id',
region: 'region-code',
src: 'https://example.com/live/master.m3u8',
type: MediaType.video,
hlsPlaybackConfig: const HlsPlaybackConfig(
enableAdaptiveBitrate: true,
maxResolutionHeight: 720,
showQualitySelectorWhenSingleOption: true,
),
)
How startup behaves:
- If
videoControlsConfig.resolutionsis provided, the SDK uses those manual rendition URLs and keeps the existing fixed-quality switching behavior. - If
enableAdaptiveBitrateis enabled, the SDK starts a local ABR proxy and opens the local playlist URL inmedia_kit. - The quality selector shows
Autoplus the variants allowed bymaxResolutionHeight. Autolets the proxy adapt per segment. Selecting a quality locks the proxy to that variant without reopening the player. SelectingAutoresumes ABR.- If the stream only has one variant, ABR behaves as a clean pass-through.
- If ABR proxy startup fails or the source is not HLS, the SDK falls back to the
original
src. - Live video streams keep buffering through transcoder gaps by default. The local ABR proxy keeps refreshing the live playlist and waits for the requested segment instead of reopening the player or replaying older chunks.
- While the proxy is waiting for the next live segment, the SDK reports buffering and the default controls keep showing the loader even if the underlying player has not emitted a new buffering event.
- If the underlying player reports
completedfor a live video after a transcoder gap, the SDK treats it as recoverable live exhaustion and continues playback instead of emitting a terminal completion event.
What the HLS playback knobs mean:
enableAdaptiveBitrate: use the local segment-level ABR proxy when possible.maxResolutionHeight: hard startup cap for auto-discovered HLS renditions.bufferSizeBytes: player demuxer cache budget in bytes.64 * 1024 * 1024means64 MB.networkTimeoutSeconds: HLS manifest/network request timeout override.showQualitySelectorWhenSingleOption: still shows the selector when only one quality is available.abrSafetyFactor: bandwidth headroom used before selecting a variant.abrEmaAlpha: smoothing weight for segment throughput measurements.abrUpgradeHysteresisMs: minimum delay between ABR upgrades.abrPanicBufferThresholdSeconds: buffer level that forces lowest quality.abrInitialBandwidthBps: initial bandwidth estimate before segments download.enableStreamAvailabilityCheck: opt-in diagnostic heuristic that reportsstreamUnavailablewhen autoplay HLS startup reaches playlists but no playable segments arrive. It is disabled by default because weak networks can look like unavailable streams. Prefer backend stream availability status for user-facing offline UI.streamAvailabilityTimeoutSeconds: startup grace period before that event.liveSegmentRetryWindowSeconds: how long the ABR proxy retries temporarily missing or not-yet-fetchable live segments before failing the request. Values less than or equal to0keep waiting until playback stops. Defaults to0for live streams that should buffer through transcoder gaps instead of replaying old chunks.liveSegmentRetryIntervalMs: delay between those ABR proxy segment retries.
Example:
ZerorateHlsPlayerOptions(
appId: 'your-app-id',
region: 'region-code',
src: 'https://example.com/live/master.m3u8',
type: MediaType.video,
hlsPlaybackConfig: const HlsPlaybackConfig(
enableAdaptiveBitrate: true,
maxResolutionHeight: 720,
bufferSizeBytes: 64 * 1024 * 1024,
networkTimeoutSeconds: 15,
abrPanicBufferThresholdSeconds: 6,
abrUpgradeHysteresisMs: 8000,
liveSegmentRetryWindowSeconds: 0,
liveSegmentRetryIntervalMs: 750,
enableStreamAvailabilityCheck: false,
streamAvailabilityTimeoutSeconds: 15,
showQualitySelectorWhenSingleOption: true,
),
)
To force the older fixed startup-cap behavior instead of segment-level ABR:
hlsPlaybackConfig: const HlsPlaybackConfig(
enableAdaptiveBitrate: false,
maxResolutionHeight: 720,
)
Notes:
bufferSizeBytesis the player cache budget, not a "download everything" setting.- Under the current native player stack, disk-backed caching is allowed underneath, so this should be treated as a player cache limit rather than a RAM-only guarantee.
Overlay Layout With Default Controls
Use data.overlayPadding as the first placement guide. It automatically
reserves room for the SDK's visible controls, safe areas, fullscreen back
button, and bottom bar.
videoControlsConfig: VideoControlsConfig(
useDefaultControls: true,
overlayBuilder: (context, data) {
return Padding(
padding: data.overlayPadding,
child: Stack(
children: [
if (data.isFullscreen)
const Align(
alignment: Alignment.topLeft,
child: StationHeader(),
),
if (data.isFullscreen)
const Align(
alignment: Alignment.bottomRight,
child: EpgToggleButton(),
),
],
),
);
},
)
Interactive Overlays
If your overlay should follow the same visibility system as the built-in
controls, use the callbacks provided in VideoControlsData.
class TvPlayerOverlay extends StatelessWidget {
final VideoControlsData data;
final bool isEpgOpen;
final VoidCallback onToggleEpg;
const TvPlayerOverlay({
super.key,
required this.data,
required this.isEpgOpen,
required this.onToggleEpg,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: data.overlayPadding,
child: Stack(
children: [
AnimatedOpacity(
opacity: data.areDefaultControlsVisible ? 1 : 0,
duration: const Duration(milliseconds: 200),
child: const Align(
alignment: Alignment.topLeft,
child: StationHeader(),
),
),
Align(
alignment: Alignment.bottomRight,
child: IconButton(
icon: Icon(isEpgOpen ? Icons.close : Icons.menu),
onPressed: () {
data.showControls();
data.setControlsPinned(!isEpgOpen);
onToggleEpg();
},
),
),
if (isEpgOpen)
Align(
alignment: Alignment.centerRight,
child: NotificationListener<ScrollNotification>(
onNotification: (notification) {
data.restartVisibilityTimer();
return false;
},
child: const SizedBox(
width: 320,
child: EpgPanel(),
),
),
),
],
);
},
}
}
Live Fullscreen Quality Trigger
For live streams, many apps want the SDK to own quality discovery/switching
logic but still place the quality trigger in an app-specific fullscreen layout.
Use data.showQualitySelector(context) from your overlayBuilder.
videoControlsConfig: VideoControlsConfig(
useDefaultControls: true,
overlayBuilder: (context, data) {
return Padding(
padding: data.overlayPadding,
child: Stack(
children: [
if (data.isFullscreen && data.availableQualities.isNotEmpty)
Align(
alignment: Alignment.topRight,
child: GestureDetector(
onTap: () => data.showQualitySelector(context),
child: DecoratedBox(
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.12),
borderRadius: BorderRadius.circular(8),
),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 8,
),
child: Text(
data.currentQuality?.label ?? 'Quality',
style: const TextStyle(color: Colors.white),
),
),
),
),
),
],
),
);
},
)
This keeps:
- quality discovery and switching inside the SDK
- trigger placement and styling in the app
- the built-in SDK quality selector available through
data.showQualitySelector(context)
Custom Audio Controls For custom audio player UI:
ZerorateHlsPlayerOptions(
// Basic setup
appId: 'your-app-id',
region: 'region-code',
src: 'https://example.com/audio.m3u8',
type: MediaType.audio,
// Option 1: Custom component builders
audioControlsConfiguration: AudioControlsConfiguration(
playPauseBuilder: (player, onTap) {
return CustomPlayButton(
isPlaying: player.playing,
onTap: onTap,
);
},
progressBarBuilder: (player, current, total) {
return CustomProgressBar(
current: current,
total: total,
onSeek: (position) => player.seek(position),
);
},
),
// Option 2: Disable default controls completely
audioControlsConfiguration: AudioControlsConfiguration(
useDefaultControls: false,
),
)
You can provide custom widgets for different playback states:
ZerorateHlsPlayerOptions(
// ... other configurations
audioControlsConfiguration: AudioControlsConfiguration(
// Custom widget when audio is in default/idle state
audioDefaultStateWidget: MyCustomIdleWidget(),
// Custom widget when audio is playing
audioPlayingStateWidget: MyCustomPlayingWidget(),
// Set to false to use completely custom controls
useDefaultControls: false,
),
)
Usage Scenarios:
- Default Controls (useDefaultControls: true)
- Built-in UI with standard controls
- You can still add extra fullscreen or inline UI with
overlayBuilder - Minimal configuration required
- Quick setup for standard use cases
- Custom Controls (useDefaultControls: false)
- Full control over in-player UI with
customControlsBuilder overlayBuildercan still be used for additional positioned elements- Implement your own play/pause, progress tracking
- Recommended for branded or complex UIs
System Now Playing #
Android and iOS system media controls are opt-in through NowPlayingConfig.
Only one player owns the system session at a time. Starting a second configured
player pauses the previous owner and transfers the system controls.
import 'package:zeroratehls/models/now_playing_config.dart';
final player = ZerorateHlsPlayer(
options: ZerorateHlsPlayerOptions(
appId: 'your-app-id',
region: 'region-code',
src: 'https://example.com/live.m3u8',
mediaType: StreamType.live,
nowPlayingConfig: const NowPlayingConfig(
metadata: NowPlayingMetadata(
title: 'Channel name',
subtitle: 'Current programme',
artworkUrl: 'https://example.com/channel-logo.png',
isLive: true,
),
),
),
);
player.updateNowPlayingMetadata(
const NowPlayingMetadata(
title: 'Channel name',
subtitle: 'Updated programme title',
artworkUrl: 'https://example.com/channel-logo.png',
isLive: true,
),
);
player.clearNowPlaying();
The package clears the session when its owner is disposed, when the app is backgrounded without PiP, or when Android PiP is dismissed. A paused player that remains visible in the app keeps a paused Now Playing card by default.
Live streams expose play and pause only. On-demand metadata supports duration, elapsed position, and iOS seek commands for future podcast/audio integrations.
No Android media playback service or iOS background-audio mode is required for this foreground/PiP-only behavior. Android PiP still requires the activity configuration below. iOS PiP is not implemented.
Picture-in-Picture (PiP) Support #
Android Configuration #
To enable Picture-in-Picture mode for your Android app, you need to make two key modifications:
- Update
AndroidManifest.xml:
<activity
android:name=".MainActivity"
android:supportsPictureInPicture="true"
... other configurations>
</activity>
- Minimum SDK Requirements:
- Requires Android 8.0 (API level 26) or higher
- Update
android/app/build.gradle:
android {
defaultConfig {
minSdkVersion 26 // Minimum SDK for PiP support
}
}
PiP is currently implemented for Android only.
API Reference #
ZerorateHlsPlayer Methods #
| Method | Description |
|---|---|
play() |
Start playback |
pause() |
Pause playback |
seekTo(Duration position) |
Seek to a position |
setVolume(double volume) |
Set volume (0.0 to 1.0) |
setPlaybackSpeed(double speed) |
Set audio playback speed |
toggleFullscreen() |
Toggle fullscreen mode (video only) |
resetToBeginning() |
Reset player duration to the start position |
seekForward10Seconds() |
Skip forward 10 seconds |
seekBackward10Seconds() |
Skip backward 10 seconds |
toggleMute() |
Mute/unmute the player |
enterPictureInPicture |
Activate Picture-in-Picture mode |
exitPictureInPicture |
Exit Picture-in-Picture mode |
updateNowPlayingMetadata(...) |
Update system media metadata at runtime |
clearNowPlaying() |
Release this player's system media session |
ZerorateHlsPlayer Properties #
| Property | Description |
|---|---|
currentPosition |
Current playback position |
duration |
Total duration of the media |
isPlaying |
Whether playback is currently playing |
isBuffering |
Whether playback is currently buffering |
isFullscreen |
Whether playback is currently in fullscreen |
currentVolume |
Current volume level |
isCompleted |
Has media playback finished |
currentSpeed |
Current playback speed |
Important Notes #
- The seek preview feature only works when using the built-in video controlson supported platforms (Android, iOS,)
Example #
A complete example with event handling and custom controls:
class PlayerScreen extends StatefulWidget {
@override
State<PlayerScreen> createState() => _PlayerScreenState();
}
class _PlayerScreenState extends State<PlayerScreen> {
ZerorateHlsPlayer? _player;
@override
void initState() {
super.initState();
_initializePlayer();
}
void _initializePlayer() {
setState(() {
_player = ZerorateHlsPlayer(
options: ZerorateHlsPlayerOptions(
appId: 'my-app-id',
region: 'us-01',
src: 'https://example.com/video.m3u8',
type: MediaType.video,
autoplay: false,
aspectRatio: 16 / 9,
videoControlsConfig: VideoControlsConfig(
enableSeekPreview: true,
),
eventListeners: {
ZerorateHlsPlayerEventType.play: (event) => print('Playing'),
ZerorateHlsPlayerEventType.error: (event) => print('Error: ${event.data}'),
},
),
);
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Video Player')),
body: Column(
children: [
if (_player != null)
AspectRatio(
aspectRatio: 16 / 9,
child: _player!,
),
// Custom controls below the player
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: Icon(Icons.replay_10),
onPressed: () {
final position = _player?.currentPosition;
if (position != null) {
_player?.seekTo(
Duration(seconds: position.inSeconds - 10),
);
}
},
),
IconButton(
icon: Icon(Icons.forward_10),
onPressed: () {
final position = _player?.currentPosition;
if (position != null) {
_player?.seekTo(
Duration(seconds: position.inSeconds + 10),
);
}
},
),
],
),
],
),
);
}
}
Troubleshooting #
HTTP streams not loading on Android
Make sure you've added android:usesCleartextTraffic="true" to your AndroidManifest.xml file.
Missing thumbnails in seek preview
The thumbnail generation runs in the background. On first run, thumbnails may take a moment to appear.
Contact Us #
Contributions are not accepted at this time. For any feature requests, bug fixes, or inquiries regarding licensing and reuse, please contact us at hi@freepass.africa.