Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lib/models/playback/playback_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ class PlaybackModel {
Future<PlaybackModel?> playbackStopped(Duration position, Duration? totalDuration, Ref ref) =>
throw UnimplementedError();

void dispose() {}

final MediaStreamsModel? mediaStreams;
List<SubStreamModel>? get subStreams => throw UnimplementedError();
List<AudioStreamModel>? get audioStreams => throw UnimplementedError();
Expand Down
96 changes: 65 additions & 31 deletions lib/models/playback/tv_playback_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,12 @@ import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/wrappers/media_control_wrapper.dart';

class TvPlaybackModel extends PlaybackModel {
Timer? _refreshTimer;
Timer? _tickTimer;
static Timer? _refreshTimer;
static Timer? _tickTimer;

DateTime? _lastScheduledAt;
String? _lastGuideProgId;
static DateTime? _lastScheduledAt;
static String? _lastGuideProgId;
static bool _isSwitching = false;
final ChannelModel channel;

final bool isNativePlayerBackend;
Expand Down Expand Up @@ -61,6 +62,13 @@ class TvPlaybackModel extends PlaybackModel {

void stopTracking() {
_stopTimers();
_isSwitching = false;
_lastGuideProgId = null;
}

@override
void dispose() {
stopTracking();
}

void _stopTimers() {
Expand All @@ -72,7 +80,13 @@ class TvPlaybackModel extends PlaybackModel {
}

void _tick(Ref ref) {
final currentProgram = playingProgram;
final model = ref.read(playBackModel);
if (model is! TvPlaybackModel) {
_stopTimers();
return;
}

final currentProgram = model.playingProgram;
if (currentProgram == null || !ref.read(mediaPlaybackProvider).playing) {
return;
}
Expand All @@ -89,7 +103,7 @@ class TvPlaybackModel extends PlaybackModel {
final newPosition = now.isBefore(start) ? Duration.zero : now.difference(start);
final newDuration = end.difference(start);

ref.read(playBackModel.notifier).update((state) => copyWith(
ref.read(playBackModel.notifier).update((state) => model.copyWith(
position: newPosition,
duration: newDuration,
));
Expand All @@ -100,43 +114,63 @@ class TvPlaybackModel extends PlaybackModel {
}

Future<void> _switchProgram(Ref ref) async {
final tempState = await ref.read(liveTvProvider.notifier).fetchDashboard();
final updatedChannel = tempState.channels.firstWhereOrNull((c) => c.id == channel.id) ?? channel;
final currentChannelPrograms = await ref.read(liveTvProvider.notifier).fetchPrograms(updatedChannel);
final channelWithPrograms = updatedChannel.copyChannelWith(programs: currentChannelPrograms);

final now = DateTime.now();
final prog = channelWithPrograms.currentProgram;

final start = prog?.startDate ?? now;
final end = prog?.endDate ?? now;
final newPosition = now.isBefore(start) ? Duration.zero : now.difference(start);
final newDuration = end.difference(start);
if (_isSwitching) return;
_isSwitching = true;
try {
final currentModel = ref.read(playBackModel);
if (currentModel is! TvPlaybackModel) {
_stopTimers();
return;
}

final tempState = await ref.read(liveTvProvider.notifier).fetchDashboard();
final updatedChannel =
tempState.channels.firstWhereOrNull((c) => c.id == currentModel.channel.id) ?? currentModel.channel;
final currentChannelPrograms = await ref.read(liveTvProvider.notifier).fetchPrograms(updatedChannel);
final channelWithPrograms = updatedChannel.copyChannelWith(programs: currentChannelPrograms);

final now = DateTime.now();
final prog = channelWithPrograms.currentProgram;

final start = prog?.startDate ?? now;
final end = prog?.endDate ?? now;
final newPosition = now.isBefore(start) ? Duration.zero : now.difference(start);
final newDuration = end.difference(start);

// Re-read in case model changed during async operations
final latestModel = ref.read(playBackModel);
if (latestModel is! TvPlaybackModel) {
_stopTimers();
return;
}

final newModel = latestModel.copyWith(
channel: channelWithPrograms,
currentProgram: prog,
position: newPosition,
duration: newDuration,
);

final newModel = copyWith(
channel: channelWithPrograms,
currentProgram: prog,
position: newPosition,
duration: newDuration,
);
ref.read(playBackModel.notifier).update((state) => newModel);

ref.read(playBackModel.notifier).update((state) => newModel);
if (prog != null && prog.endDate.isAfter(now)) {
_scheduleRefreshAt(prog.endDate, ref);
}

if (prog != null && prog.endDate.isAfter(now)) {
_scheduleRefreshAt(prog.endDate, ref);
await _sendNativeGuideUpdate(ref, prog, channelWithPrograms, tempState, latestModel.isNativePlayerBackend);
} finally {
_isSwitching = false;
}

await _sendNativeGuideUpdate(ref, prog, channelWithPrograms, tempState);
}

Future<void> _sendNativeGuideUpdate(
Ref ref,
ChannelProgram? prog,
ChannelModel channelWithPrograms,
LiveTvModel tempState,
bool isNativePlayerBackend,
) async {
final isNativePlayer = isNativePlayerBackend;
if (!isNativePlayer || tempState.channels.isEmpty || _lastGuideProgId == prog?.id) {
if (!isNativePlayerBackend || tempState.channels.isEmpty || _lastGuideProgId == prog?.id) {
return;
}

Expand Down
3 changes: 3 additions & 0 deletions lib/providers/video_player_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ class VideoPlayerNotifier extends StateNotifier<MediaControlsWrapper> {
Future<void> updatePlaying(bool event) async {
final currentState = playbackState;
if (!state.hasPlayer || currentState.playing == event) return;
if (currentState.state == VideoPlayerState.disposed) return;
mediaState.update(
(state) => state.copyWith(playing: event),
);
Expand All @@ -90,6 +91,7 @@ class VideoPlayerNotifier extends StateNotifier<MediaControlsWrapper> {
if (!state.hasPlayer) return;
if (playbackState.playing == false) return;
final currentState = playbackState;
if (currentState.state == VideoPlayerState.disposed) return;
final currentPosition = currentState.position;

if ((currentPosition - event).inSeconds.abs() < 1) return;
Expand All @@ -113,6 +115,7 @@ class VideoPlayerNotifier extends StateNotifier<MediaControlsWrapper> {
}

Future<bool> loadPlaybackItem(PlaybackModel model, Duration startPosition) async {
ref.read(playBackModel)?.dispose();
await state.stop();
ref.read(playbackRateProvider.notifier).state = 1.0;
mediaState.update((state) => state.copyWith(
Expand Down
31 changes: 25 additions & 6 deletions lib/wrappers/media_control_wrapper.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:developer';
import 'dart:io';

import 'package:flutter/foundation.dart';
Expand Down Expand Up @@ -133,6 +134,21 @@ class MediaControlsWrapper extends BaseAudioHandler implements VideoPlayerContro

Future<void> openPlayer(BuildContext context) async => _player?.open(context);

// Update playback play/pause state with single retry
Future<void> _updatePositionWithRetry(PlaybackModel model, Duration position, bool isPlaying) async {
try {
await model.updatePlaybackPosition(position, isPlaying, ref);
} catch (error, stackTrace) {
log('Failed to send playing: $isPlaying state to server. Retrying once. Error: $error\n$stackTrace');
try {
await Future.delayed(const Duration(milliseconds: 250));
await model.updatePlaybackPosition(position, isPlaying, ref);
} catch (retryError, retryStackTrace) {
log('Retry failed for playing: $isPlaying state update. Error: $retryError\n$retryStackTrace');
}
}
}

void _subscribePlayer() {
if (Platform.isWindows && !kIsWeb) {
smtc = SMTCWindows(
Expand Down Expand Up @@ -218,7 +234,10 @@ class MediaControlsWrapper extends BaseAudioHandler implements VideoPlayerContro
WakelockPlus.disable();
final playerState = _player;
if (playerState != null) {
ref.read(playBackModel)?.updatePlaybackPosition(playerState.lastState.position, false, ref);
final model = ref.read(playBackModel);
if (model != null) {
await _updatePositionWithRetry(model, playerState.lastState.position, false);
}
}
}

Expand Down Expand Up @@ -305,13 +324,12 @@ class MediaControlsWrapper extends BaseAudioHandler implements VideoPlayerContro

ref.read(mediaPlaybackProvider.notifier).update((state) => state.copyWith(state: VideoPlayerState.disposed));
WakelockPlus.disable();
super.stop();
_player?.stop();

final position = _player?.lastState.position;
final totalDuration = _player?.lastState.duration;

// //Small delay so we don't post right after playback/progress update
// Small delay so we don't post right after playback/progress update
await Future.delayed(const Duration(seconds: 1));

await playbackModel.playbackStopped(position ?? Duration.zero, totalDuration, ref);
Expand Down Expand Up @@ -347,9 +365,10 @@ class MediaControlsWrapper extends BaseAudioHandler implements VideoPlayerContro

final playerState = _player;
if (playerState != null) {
ref
.read(playBackModel)
?.updatePlaybackPosition(playerState.lastState.position, playerState.lastState.playing, ref);
final model = ref.read(playBackModel);
if (model != null) {
await _updatePositionWithRetry(model, playerState.lastState.position, playerState.lastState.playing);
}
}
}

Expand Down
Loading