Releases: vitobotta/streamvault
Release list
v1.4.1 — Playback synchronization & subtitle reliability
Summary
This patch release fixes two recurring streaming problems: audio gradually moving out of sync with video, and subtitles frequently failing to load or appear after resuming, seeking, reconnecting, or changing playback paths.
Audio/video synchronization
- Re-encodes video after a non-zero seek instead of copying from the preceding keyframe. This keeps video aligned with accurately trimmed audio during resume, seek, and recovery operations.
- Normalizes all audio to AAC and continuously reconciles samples with source timestamps using asynchronous resampling.
- Pads or trims the beginning of audio onto the same zero-based timeline as video.
- Generates missing presentation timestamps at the input so both HLS and fragmented MP4 output receive consistent timing.
Subtitle reliability
- Uses the browser's absolute media time during native direct play, preventing resume offsets from being added twice to subtitle requests, progress, and seek calculations.
- Loads track metadata before starting iOS HLS playback so subtitle controls and burn-in choices are available.
- Passes selected bitmap subtitle tracks to HLS and switches native direct play to the transcode path when image-based subtitles need to be burned into the video.
- Keeps text subtitles in the HTML overlay path and reloads them after seeks, reconnects, and stream restarts.
- Cancels stale subtitle requests and invalidates cleared cue windows so an empty overlay cannot incorrectly be treated as already loaded.
- Extends packet extraction time for slower remote files and reports extraction timeouts as retryable errors instead of treating them as genuinely empty subtitle windows.
- Preserves the absolute playhead when direct play falls back to transcoding.
Verification
- 365 Rails examples passing
- 6 JavaScript controller regression tests passing
- RuboCop clean
Full changelog: v1.4.0...v1.4.1
v1.4.0 — Buffering fixes, security hardening & code review
Summary
21 commits since v1.3.0. The focus was eliminating persistent stream buffering, hardening security, and resolving 33 code review findings.
Buffering fixes (the big one)
Root cause: The remux path (-c:v copy, no video re-encode) was capped at 1× playback speed by ffmpeg's -re flag, leaving zero buffer margin. The browser could never build a buffer ahead, so any transient dip (probe, audio re-encode, CDN latency) immediately stalled the video. Stremio doesn't have this problem because it hands the RD URL directly to the player, which downloads at network speed.
Fixes applied:
- Remove
-refrom remux/copy path — ffmpeg now reads at full speed, the Tempfile buffer decouples it from HTTP backpressure, and-reconnecthandles upstream drops - Enable direct play for H.264 MP4 with B-frames —
direct_playable?was usingbrowser_safe_video?which rejects B-frames (needed for MSE, not for native<video>). Most x264 releases have B-frames, so direct play was almost always blocked - Allow HEVC remux on macOS/iOS/Android —
canPlayTypereturns""for HEVC even on Chrome 107+ where<video>plays it via VideoToolbox. AddedplatformSupportsHevc()to check the platform directly - Allow direct play when selected audio = default track —
loadMediaTracksauto-setsselectedAudioStreamto the preferred track, which blocked direct play for all multi-audio files even when the preferred track was the default - Stop reconnect cycle when browser buffer is full — the progress watchdog's download stall detector was firing every ~15s for remux direct play because the browser stops reading when its internal buffer is full (normal for 4K HEVC). Now skips reconnect when
bufferedAhead > 5s - Wait for first frame before playback (HEVC) — HEVC via VideoToolbox has higher decoder startup latency than H.264. Without waiting, audio started before the first video frame was ready, causing a brief freeze
- Disk-based Tempfile buffer — decouples ffmpeg stdout from HTTP backpressure so the reader thread keeps draining ffmpeg's pipe even when the browser pauses reading
- Abort stale progress requests — prevents connection pool exhaustion when seeking rapidly
- Pre-emptive reconnect — detects download stalls before the buffer runs dry and reconnects while the video is still playing from buffer
Security & code quality
- 33 code review findings resolved — security, correctness, performance, and test coverage
- Rack::Attack rate limiting — protects against abuse on API endpoints
- Content param validation — shared concern for validating imdb_id, type, season, episode
- Stream URL validation — validates and sanitises stream URLs before use
- CSRF token fix — stall telemetry POST body was not being sent
- Comet 200 trailer rejection — Comet proxy returning HTTP 200 with trailer data instead of the real stream URL was not handled
Other changes
- Recommendation refresh job — background job with spec coverage
- HLS session improvements — record model, spec coverage
- Torrentio service refactor — cleaner stream provider interface
- Comprehensive test suite — 363 examples, 87.69% line coverage
Files changed
54 files changed, 2453 insertions(+), 284 deletions(-)
Full changelog: v1.3.0...v1.4.0
v1.3.0 — Playback performance overhaul
Major playback performance improvements
This release eliminates the frequent buffering that occurred during playback by adding a direct play path, prioritising compatible stream formats, and fixing false stall detection.
Direct play (bypass ffmpeg entirely)
For H.264/AAC MP4 content, the browser now downloads directly from the RealDebrid CDN via a transparent proxy (DirectStreamController) at network speed — no ffmpeg involved. The browser builds its own deep buffer and seeks via native Range requests, matching Stremio-level performance. This is the single biggest improvement: previously every stream went through an ffmpeg transcode proxy, even when the source was already browser-compatible.
Stream compatibility sorting
Stream listings now detect video codec, audio codec, and container from release names and sort compatible formats first:
- Direct play ready (MP4 + H.264 + AAC) — sorted highest
- Stream copy (MP4 + H.264, any audio) — ffmpeg copies video, light load
- Heavy transcode (HEVC/AV1/VP9) — needs decode + re-encode, sorted last
This means the auto-selected stream and the top of the stream list favour formats that play without ffmpeg bottlenecks.
Buffering stability fixes
- Rebuffer threshold raised from 5s → 30s: after a stall, the player now rebuilds 30 seconds of buffer before resuming, absorbing transient upstream dips instead of immediately re-stalling
- False "Buffering..." overlay fix: the browser fires
waitingnot only when the buffer is empty, but also during transient decoder pipeline flushes (e.g. aftersourceBuffer.remove()from buffer eviction). The player now checks whether buffered data exists before showing the overlay, eliminating false stall-recovery cycles - O(n²) → O(n) buffer management: the fMP4 box accumulation buffer was reallocating and copying the entire buffer on every incoming chunk (~128 MB of copying per 2 MB of video). Replaced with a pre-allocated doubling buffer
- Correct buffer-ahead calculation:
maybeStartPlaybacknow uses the buffered range containingcurrentTimeinstead of the last range's end, preventing premature playback starts with disjoint buffered ranges
ffmpeg performance
- Hardware HEVC decode (
-hwaccel videotoolbox) on macOS — offloads 4K HEVC decode to the media engine instead of software decode (was the #1 CPU bottleneck) - Multi-threaded encoding (
-threads auto) forlibx264on Linux — uses all CPU cores instead of defaulting to one - Configurable hardware acceleration via
FFMPEG_HWACCELandFFMPEG_ENCODERenv vars for production Docker deployments with GPUs (VAAPI, NVENC) - Larger read chunks (32 KB → 128 KB) reducing syscall overhead
- Probe cache expanded (100 → 500 entries, 5min → 10min TTL) with thread-safe Mutex
Observability
- New
POST /streaming/stall_telemetryendpoint logs client-side stall events (position, buffer-ahead, mode, recovery count) — grep[StallTelemetry]in production logs to diagnose buffering
Infrastructure fixes
- Tailscale/Comet host validation: user-configured provider hosts (e.g. Comet on a Tailscale
100.x.x.xIP) now bypass the private-address DNS check that was returning 400 Bad Request for all Comet streams
Comet fixes (contributed by @hazrid93 in #7)
Two bugs in CometService that together made Comet completely unusable in StreamVault:
-
Padded base64 for debrid config:
CometServiceencoded the Stremio debrid config with unpadded base64, which Comet's config-path parser does not accept. Comet silently failed to decode the debrid config and returned a single placeholder stream instead of real results. Fixed by droppingpadding: false. -
Parse Comet's real metadata:
CometService#parse_streamswas reading Torrentio-shaped fields (title,infoHash,sources,seeders) that Comet does not send. Comet puts metadata indescriptionandbehaviorHints. Fixed to read from Comet's actual fields — streams now show real release names, file sizes, info hashes, and seeder counts instead of identical placeholder rows.
Full changelog: v1.2.0...v1.3.0
v1.2.0 — CSP, video player & iOS fixes
CSP & security
- Enabled CSP nonce generator for importmap bootstrap (was breaking all JS in production)
- Dropped style-src from nonce directives to unblock inline styles
- Preload hero background image only for unauthenticated users
Video player
- Pause stream immediately on back-navigation for instant visual feedback
- Fix stuck "Buffering" overlay after rebuffer resume with low buffer
iOS
- Fix remove/delete buttons invisible on touch devices (Continue Watching, Library, Wishlist, Watch History)
- Fix Watch History delete button nested inside anchor (invalid HTML)
v1.1.0 — Security & correctness review fixes
Security
- RealDebrid API key leak via logs — Torrentio resolve URLs embed the RD key in the path;
ContentStreamingServicelogged them verbatim. Now logs onlyimdb_id+filename. RealDebridService#with_retryinfinite loop —retryre-executed the method body, resettingretries=0on every network failure, hanging the calling thread forever on a persistent RD outage. Restructured with an explicit loop.- Orphaned HLS ffmpeg processes —
HlsSession.cleanup_expiredwas never scheduled. Added a recurring job (every 5 min);HlsSession.stopnow falls back to the persistedrecord.pidso the SolidQueue worker can kill orphaned ffmpeg from other Puma workers. - SSRF DNS-rebinding TOCTOU —
valid_stream_url?resolved the host and rejected private IPs, but ffmpeg re-resolved independently. Addedverify_stream_url!(re-resolve before spawn) + an outbound host allowlist (RealDebrid CDN + provider resolve origins only). force_ssl+assume_sslenabled in production (were commented out despite the app carrying session cookies + RD keys).- Content-Security-Policy enabled (initializer was fully commented out).
- Open redirect via
Referer—content/showlinked to rawrequest.referer. Replaced with internal route helpers.
Correctness
- Reject non-positive progress saves —
save_progressusedblank?, which is false for0. - Idempotent + transactional
move_to_library—create!raised 500 on duplicate; nowfind_or_create_by!inside a transaction.
Performance
- Cache + parallelize home page catalog calls — the 4 cinemeta calls were sequential and uncached; now cached (1h TTL) and run concurrently.
- Cache movie stream listings (60s TTL) in
ContentController.
Quality
- Reword the Active Record encryption comment (was misleading — nil is fail-closed).
- Set Devise
mailer_senderfromMAILER_FROMenv (was placeholder). - Add XSS invariant comment to
episode_pickerinnerHTML sink.
Tests: 248 examples, 0 failures.
v1.0.0
StreamVault started as a personal project to solve a simple frustration: I wanted a clean, fast, self-hosted way to browse and watch movies and shows from my own RealDebrid account, without the clutter and friction of the existing options. This is the first release — a complete, working version of that idea.
What this release does
Discovery and browsing. Search for any movie or TV show by title and get rich results with posters, plots, cast, and ratings from IMDb, Rotten Tomatoes, and Metacritic. Browse what's popular and trending, or check out a personalised "Recommended for You" carousel that learns from what you watch.
A real streaming experience. Pick a title, choose from a list of available streams sorted by quality and your language preferences, and press play. StreamVault handles everything behind the scenes — finding the stream, sending it to RealDebrid for caching, and converting the file on-the-fly to a format your browser can play. You never touch a command line or wait for a download to finish.
A player that doesn't get in the way. The in-browser player was built from scratch with a focus on the things that actually matter when watching: seeking, volume, audio track switching, subtitles (both embedded and fetched automatically when missing), and full keyboard support. Close the tab and come back later — you'll resume right where you left off.
TV shows done right. Browse seasons and episodes with air dates and overviews. When an episode finishes, the next one is queued up automatically. Your progress is tracked per episode, so you always know what to watch next.
Your personal library. Add content to your library or a wishlist for later. Your watch history is always there, and a "Continue Watching" row on the home page lets you jump back into anything in a single click.
Personalisation. Set your preferred audio languages and StreamVault will prioritise streams in those languages. Each user gets their own library, history, wishlist, and settings — with API keys encrypted at rest.
Works everywhere. A responsive dark-themed interface with a sidebar on desktop and a bottom navigation bar on mobile. Install it as a PWA on your home screen for a full-screen, app-like experience — including on iOS, where it falls back to a native HLS player when needed.
Stays running. Deploy with Docker in a single command. Containers restart automatically after crashes or reboots. A two-server setup guide is included for when your main server's IP is blocked by Cloudflare.
What's next
This is a solid foundation, but it's still a v1. The player has been hardened with stall recovery, buffer management, and pause protection, but there's more polish to come — faster startup, better mobile gestures, and a few surprises. Feedback is welcome.
Full setup instructions are in the README.