Files
plezy/lib/utils/stream_buffer_sizing.dart
T

72 lines
3.0 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. MKV is
/// interleaved by spec and excluded.
const poorlyInterleavedContainers = {'mp4', 'mov', 'm4v', '3gp', '3g2'};
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());
if (!tokens.any(poorlyInterleavedContainers.contains)) return null;
final cap = max(minStreamRingBytes, min(maxBytes, maxStreamRingBytes));
if (bitrateKbps == null || bitrateKbps <= 0) return min(unknownBitrateStreamRingBytes, cap);
final bytesPerSecond = bitrateKbps * 1000 ~/ 8;
final ring = nextPowerOfTwo(bytesPerSecond * _streamRingContentSeconds);
return max(minStreamRingBytes, min(ring, cap));
}