Skip to content

Releases: yuzicapp/yuzic-engine

v1.0.2

Choose a tag to compare

@eftpmc eftpmc released this 10 Sep 00:28

Fixed

On Android, the lock screen, notification and car display kept showing the previous track after a crossfade.

EnginePlayer is a ForwardingPlayer around one of the two voices. It overrode only the getters describing where playback is — position, duration, state — and left everything describing what is loaded to the wrapped player. That was correct while swapVoices() was never called, and the comment above it said so. Giving Android engine-driven advances made the crossfade swap for the first time, so after an odd number of fades the wrapped player was the idle voice, still holding the track it last played.

The metadata, timeline and media-item getters now follow the active voice like the rest. Verified on device across a crossfade: session metadata moves with the audio, where before the outgoing track's title persisted indefinitely.

Full changelog: https://github.com/yuzicapp/yuzic-engine/blob/main/CHANGELOG.md

yuzic-engine 1.0.1

Choose a tag to compare

@eftpmc eftpmc released this 09 Sep 22:55
9be134a

Test and tooling only. The engine's behaviour and its public API are unchanged from 1.0.0.

Fixed

  • testReconnectionGivesUpAfterItsBudget was flaky, failing about three runs in four. It injected a fixed number of read failures and asserted the engine had seen every one, which it had not: onReadFailed hops to the main queue and only then checks activePlayback === playback, so a failure arriving while a reconnection is swapping the playback out finds a different object and returns. That drop is correct — a failure belongs to the playback that raised it — so the test was wrong rather than the engine. It now injects until the budget is actually spent, and the assertion still catches a broken budget: removing the cap gives sixteen opens against the expected four.
  • Tools/mutate.py had been unusable since the repository movedROOT was an absolute path to a checkout that no longer exists, so it died on its first file read. Derived from __file__ now. With it running again: all nine mutations caught, no survivors.

yuzic-engine 1.0.0

Choose a tag to compare

@eftpmc eftpmc released this 09 Sep 21:15
977e900

An audio playback engine for React Native, built around an audio graph rather than a single player. iOS and Android, Apache-2.0.

Two tracks have to be audible at once for a crossfade, and an equalizer has to sit somewhere in the signal path. Neither is expressible against an API that plays one URL at a time — you can fade a single output down and back up, but the join is a hole rather than an overlap. So the engine keeps two voices, each a player node feeding its own gain, summed into a mixer, through an EQ, to the output.

The API is what 1.0.0 commits to, not the code. It has been in production in yuzic across both platforms for months. Semver here is a promise about the JavaScript surface: the methods on YuzicEngine, the event vocabulary, and the exported types.

What it does

  • Queue, natively — set, append, insert, remove, move, clear, skip. It lives in native code because backgrounded JavaScript is suspended while the lock screen, the notification and the car still have to work.
  • Crossfade with a gapless-aware mode that hard-cuts where a track follows the previous one, so a segued album is not faded through its own joins. A gapless join is the same machinery with a zero-length fade.
  • DSP — a ten-band equalizer, bypassed entirely when flat, and replay gain with album/track/auto modes.
  • Sources — local files and HTTP streaming, mutual TLS on both platforms, and an on-device LRU cache keyed by media id rather than URL, because Subsonic and Jellyfin rotate tokens through the URL.
  • Vorbis and Opus on iOS, which Core Audio cannot open at all.
  • Platform integration — background playback, lock-screen and notification controls, CarPlay and Android Auto browse trees, audio focus, interruptions, route changes, and a sleep timer that fades rather than cuts.

Fixed in this release

  • A seek no longer ends the stream it is seeking within. StreamingByteSource.cancel() stopped its producer — which for the HTTP producer cancels the task and invalidates the session, irreversibly — while resume() only cleared a flag. Since every seek cancels and resumes the source being decoded from, the first seek ended a transcoded stream for good and every read afterwards timed out: silence, then a track that ended itself. Streamed audio only; a downloaded file never takes that path.
  • An estimated length is no longer mistaken for the end of a track. For a sequential source the reported length is duration × bitrate until the stream ends, and an empty read at that figure was treated as a clean end-of-file, which the engine follows by advancing the queue.

Known gaps

configureCache is absent on Android, deliberately rather than stubbed, so it rejects by name at the bridge. Tools/parity.py declares it; every other method agrees across the two platforms by signature and event vocabulary.