Skip to content

feat(player): audio offset per playlist and in the player - #382

Open
TheDutchSmoke wants to merge 4 commits into
bilipp:mainfrom
TheDutchSmoke:feat/audio-offset
Open

TheDutchSmoke wants to merge 4 commits into
bilipp:mainfrom
TheDutchSmoke:feat/audio-offset

Conversation

@TheDutchSmoke

@TheDutchSmoke TheDutchSmoke commented Nov 5, 2026 •

Copy link
Copy Markdown

Summary

Adds an audio offset (lip-sync correction) for providers whose streams carry the sound ahead of or behind the picture.

  • Per provider: a new Audio Delay picker in the playlist's Playback section (iOS/macOS form and tvOS pane), stored as Playlist.audioOffsetMilliseconds. Positive plays audio later, the same sign as AetherEngine's engine-wide setting. Device-local like the stream format (the correction depends on the TV/receiver as much as on the provider), so no CloudKit / Lume Cloud schema change. The value rides on PlayableMedia.audioOffsetMilliseconds, so Multiview tiles and catch-up get it too.
  • In the player: an Audio Delay menu in the controls pill (iOS/macOS/visionOS) and in the tvOS trailing row, shown only while the engine on screen can shift its audio while playing. The pick is a session nudge on top of the provider's value; the menu offers Playlist Default to go back and Save as Playlist Default to persist it. It carries across episodes/channels of the same provider and resets on another provider.

Engines

Engine How While playing
KSPlayer (FFmpeg path) KSOptions.videoDelay with the opposite sign (videoClockSync shows the frame at clock - videoDelay) yes, next frame; KSAVPlayer ignores it, so the menu registers only when the layer's player is KSMEPlayer
VLCKit currentAudioPlaybackDelay (µs), re-asserted after every installMedia + play() and on .playing, like deinterlace yes
AetherEngine added to the engine-wide player.aether.audioDelay at load; LumeAetherSession.setAudioDelay (new bridge method, engine API since 7.x) while playing yes, re-anchors at the playhead (seek on the software path, session-preserving reload on loopback; live without DVR takes it at the next seam)
AVPlayer no public API menu hidden
LumeEngine no offset API in PlayerSession yet menu hidden; engine-side follow-up

AetherAudioDelayPreset became the shared AudioOffsetPreset, which owns the presets (−500…+500 ms) and every per-engine conversion; the Aether settings UI keeps working unchanged.

Type of change

  • feat — new feature

Platforms verified

  • iOS / iPadOS (iPhone 17 simulator, iOS 27: LumeTests)
  • macOS (compile only)
  • tvOS (not built: on my machine the LumeEngine sibling is v0.4.0, which lacks VideoSampleTap for the Hue frame source, so the tvOS target fails before this change too; no tvOS runtime either)
  • visionOS

Screenshots / recordings

None yet — happy to add once a maintainer confirms the placement of the menu (after Quality in the pill / trailing row).

Checklist

  • I opened an issue first (for features / non-trivial changes) and linked it above.
  • My commits follow the Conventional Commits format.
  • Pre-commit hooks pass (SwiftFormat, SwiftLint --strict, String Catalog normalization).
  • The test suite passes (LumeTests on the iOS simulator; see note below).
  • New/changed user-facing text is localized in all ten languages (three new keys; the rest reuses existing ones).
  • I added or updated tests covering my change where it makes sense (PlayerAudioOffsetTests, AetherEngineSettingsTests).
  • My change contains no credentials, stream URLs, or other content.

Notes for review

  • LumeTests on iPhone 17 / iOS 27: 3450 passed; 27 failures are the same ones main shows on this machine (DTODecodingTests/ContentSyncTests without the generated ExampleData, SportsLabelsTests under a Dutch locale, SportsResolveCacheTests, the LumeCloudVectorExportTests drift, and two wall-clock tests that pass when re-run alone). The suites touched here (PlayerAudioOffsetTests, AetherEngineSettingsTests, PlayableMediaTests, PlaylistStreamFormatTests) plus LumeEngineShutdownWaitTests and CloudSyncParentalEngineTests were re-run in isolation: 150 passed, 0 failed.
  • The KSPlayer mapping relies on videoDelay being read per frame in KSOptions.videoClockSync (verified in the 2.3.x source); a large nudge towards "audio later" briefly drops frames while the picture catches up, the same way a lagging stream does.
  • Not changed: SyncedPlaylist, the Lume Cloud payload, PlayerEngineKind.

Adds a lip-sync correction in milliseconds (positive plays audio later):
a per-playlist default in the Playback section, carried on PlayableMedia,
and an in-player Audio Delay menu for the session that can be saved back
as the playlist default. Shown only while the engine on screen can shift
its audio while playing: KSPlayer's FFmpeg path (videoDelay, opposite
sign), VLCKit (currentAudioPlaybackDelay) and AetherEngine (setAudioDelay
through a new bridge method, on top of its engine-wide delay). AVPlayer
and LumeEngine have no API and hide the menu.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant