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
16 changes: 16 additions & 0 deletions docs/syncplay-implementation.md
Original file line number Diff line number Diff line change
Expand Up @@ -1456,6 +1456,22 @@ AGENTS.md #10.
direct-stream item. Confirm no group-level pause and no visible
desync in the other client.

### Initiate playback (cross-client interop)

- [ ] **Episode start propagates to official clients:** In a group with an
official Jellyfin client (e.g. webOS/Android TV) as a second
participant, start a TV episode from Fladder. Confirm the other
client actually begins playing the episode (was: `_playSyncPlay` sent
a single-item queue, which official clients do not start; movies,
being single-item by nature, masked the bug and worked).
- [ ] **Resume position on initiate:** Press "Continue Watching" on a
partially-watched item while in a group. Confirm every participant
resumes near the saved position, not from 0:00 (was: the SyncPlay
initiate path always sent startPositionTicks: 0).
- [ ] **Series/Season start:** Press play on a series or season (not a
specific episode) in a group. Confirm it resolves to the next-up
episode and all participants start there.

### UI placement

- [ ] **Side rail FAB count:** With a side navigation rail visible, each
Expand Down
36 changes: 33 additions & 3 deletions lib/util/item_base_model/play_item_helpers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import 'package:collection/collection.dart';
import 'package:fladder/models/book_model.dart';
import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/models/items/channel_model.dart';
import 'package:fladder/models/items/episode_model.dart';
import 'package:fladder/models/items/photos_model.dart';
import 'package:fladder/models/items/season_model.dart';
import 'package:fladder/models/items/series_model.dart';
import 'package:fladder/models/media_playback_model.dart';
import 'package:fladder/models/playback/playback_model.dart';
import 'package:fladder/models/playback/tv_playback_model.dart';
Expand Down Expand Up @@ -255,7 +258,34 @@ Future<void> _playSyncPlay(
WidgetRef ref, {
Duration? startPosition,
}) async {
final startPositionTicks = startPosition != null ? secondsToTicks(startPosition.inMilliseconds / 1000) : 0;
// Build the full play queue so episodes are sent to the group WITH their
// series context. The local playback path builds this via
// createPlaybackModel -> collectQueue; the SyncPlay path must match it.
// A lone single-episode queue is not started by the official Jellyfin
// clients (e.g. the webOS TV app) even though movies — naturally
// single-item — work fine. Movies return an empty seriesQueue and fall
// back to a single item.
final helper = ref.read(playbackModelHelper);
final List<ItemBaseModel> seriesQueue = await helper.collectQueue(itemModel);

// Series/Season tiles resolve to their next-up episode (mirrors
// createPlaybackModel's firstItemToPlay switch).
ItemBaseModel target = itemModel;
if (itemModel is SeriesModel || itemModel is SeasonModel) {
final resolved = seriesQueue.whereType<EpisodeModel>().toList().nextUp;
if (resolved != null) target = resolved;
}

final List<String> itemIds = seriesQueue.isNotEmpty ? seriesQueue.map((e) => e.id).toList() : [target.id];
final int playingItemPosition =
seriesQueue.isNotEmpty ? seriesQueue.indexWhere((e) => e.id == target.id).clamp(0, itemIds.length - 1) : 0;

// Fall back to the resolved item's saved resume position (mirrors the local
// path's `model.startDuration()`) so "Continue Watching" resumes mid-item
// for the whole group instead of restarting from 0. Previously, every play
// that didn't pass an explicit startPosition sent startPositionTicks: 0.
final effectiveStart = startPosition ?? target.userData.playBackPosition;
final startPositionTicks = secondsToTicks(effectiveStart.inMilliseconds / 1000);

final notifier = ref.read(syncPlayProvider.notifier);
final pending = notifier.awaitNextStartPlayback(
Expand All @@ -266,8 +296,8 @@ Future<void> _playSyncPlay(
_showLoadingIndicator(context, itemModel, op, autoCloseOnComplete: true);

final queueAccepted = await notifier.setNewQueue(
itemIds: [itemModel.id],
playingItemPosition: 0,
itemIds: itemIds,
playingItemPosition: playingItemPosition,
startPositionTicks: startPositionTicks,
);

Expand Down