Files
plezy/lib/utils/stream_buffer_sizing.dart
T

83 lines
3.7 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:math';
/// mpv's built-in `stream-buffer-size` default (128 KiB). Written back for
/// opens that don't qualify for an enlarged ring so a reused player instance
/// never carries one item's tuning into the next.
const mpvDefaultStreamBufferBytes = 128 * 1024;
/// QuickTime/MP4 muxer family. Apple capture muxers (iPhone recordings and
/// friends) can store audio packets seconds away from coeval video packets —
/// in bytes, seconds × video byterate — which makes ffmpeg's DTS-ordered
/// reads ping-pong across the file. Over HTTP every ping-pong that escapes
/// mpv's stream ring buffer is a byte seek, and ffmpeg's http layer drops and
/// redials the connection on every seek, collapsing throughput.
const quickTimeFamilyContainers = {'mp4', 'mov', 'm4v', '3gp', '3g2'};
/// Matroska/WebM is normally well-interleaved, but UHD direct-play remuxes can
/// still overrun mpv's tiny 128 KiB default stream ring during normal demuxer
/// read bursts. Only opt in when the server reports a high bitrate so common
/// MKV playback keeps the default memory profile.
const matroskaFamilyContainers = {'mkv', 'matroska', 'webm'};
const _highBitrateMatroskaThresholdKbps = 40 * 1000;
const minStreamRingBytes = 16 * 1024 * 1024;
const maxStreamRingBytes = 128 * 1024 * 1024;
/// Ring for QuickTime-family content whose bitrate the backend didn't report.
const unknownBitrateStreamRingBytes = 64 * 1024 * 1024;
/// Seconds of content bytes the ring should hold. mpv guarantees only half
/// the ring as seek-back history, so 6s of bytes ⇒ ≥3s of guaranteed
/// interleave-skew coverage (observed iPhone skew ~2.9s) before the
/// power-of-two round-up adds headroom.
const _streamRingContentSeconds = 6;
int nextPowerOfTwo(int value) {
var result = 1;
while (result < value) {
result <<= 1;
}
return result;
}
/// Heap-tier cap for the stream ring on Android, mirroring the demuxer
/// auto-scaling tiers in video_player_screen.dart — mpv is the
/// ExoPlayer-fallback engine there, which is exactly where low-RAM TV boxes
/// land.
int androidStreamRingCapBytes(int heapMB) {
if (heapMB <= 0) return unknownBitrateStreamRingBytes;
if (heapMB <= 256) return 32 * 1024 * 1024;
if (heapMB <= 512) return 64 * 1024 * 1024;
return maxStreamRingBytes;
}
/// mpv `stream-buffer-size` for a network direct play of [container] at
/// [bitrateKbps] total bitrate, or null when mpv's 128 KiB default suffices.
///
/// The ring absorbs the demuxer's audio↔video byte alternation entirely in
/// RAM so the underlying HTTP reads stay linear. The ring is fully allocated
/// (power-of-two rounded) per stream, hence the container gate and bitrate
/// scaling instead of a flat global value.
int? networkStreamRingBytes({
required String? container,
required int? bitrateKbps,
int maxBytes = maxStreamRingBytes,
}) {
if (container == null) return null;
// Jellyfin may report ffmpeg demuxer alias lists ('mov,mp4,m4a,3gp,3g2,mj2').
final tokens = container.toLowerCase().split(',').map((token) => token.trim());
final isQuickTimeFamily = tokens.any(quickTimeFamilyContainers.contains);
final isMatroskaFamily = tokens.any(matroskaFamilyContainers.contains);
if (!isQuickTimeFamily && !isMatroskaFamily) return null;
final cap = max(minStreamRingBytes, min(maxBytes, maxStreamRingBytes));
if (bitrateKbps == null || bitrateKbps <= 0) {
return isQuickTimeFamily ? min(unknownBitrateStreamRingBytes, cap) : null;
}
if (!isQuickTimeFamily && bitrateKbps < _highBitrateMatroskaThresholdKbps) return null;
final bytesPerSecond = bitrateKbps * 1000 ~/ 8;
final ring = nextPowerOfTwo(bytesPerSecond * _streamRingContentSeconds);
return max(minStreamRingBytes, min(ring, cap));
}