forked from Foxyz/sync_replay_project
Page links played here only also go through a reader of their own: Bilibili's cuts never end playback
Alone, or with Everyone fetches it, a page link with plain-file tracks plays through SharedLinks.local (a SharedSource without the upload): it resumes after a cut, backs off on 412/429/5xx and resolves the page again on 403/410/412, where ffmpeg's own reconnect gave up for good. Pieces tracks and a reader that cannot open keep yt-dlp's URLs as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f2721d125c
commit
4c8ddeb034
5 files changed
+183
-16
No files matched your search
@@ -233,9 +233,24 @@ class MediaPipeline {
|
||||
throw ResolveFailure(probe.ytDlpError ?? (probe.info == null ? 'Not a video page yt-dlp knows' : 'no format a player opens'),
|
||||
cookies: probe.cookies);
|
||||
}
|
||||
if (headers == null || headers.isEmpty) {
|
||||
// Played here only: through a reader of its own too, which resumes
|
||||
// after a cut and gets a fresh address (a CDN cuts mpv's reconnects
|
||||
// off). Pieces tracks, or a reader that cannot open: the site as is.
|
||||
final y = await l.ytDlp;
|
||||
final local =
|
||||
await shared.local(url, probe, () => LinkRelayMedia(Uri.parse(url), ytDlp: y, probe: probe), fresh: fresh);
|
||||
if (local != null) {
|
||||
return _Target(local.source.playerUrl, const {}, subtitles: local.subtitles, shared: true, local: true);
|
||||
}
|
||||
}
|
||||
return _Target(play.mpvUrl, {...play.headers, ...?headers}, subtitles: play.subtitles);
|
||||
}
|
||||
|
||||
/// The [SharedLinks.local] readers the player and the preload use stay;
|
||||
/// the rest close.
|
||||
void _trimLocal() => shared.trimLocal({_openUrl, _pre?.url});
|
||||
|
||||
/// The page's subtitles on [p] once its media is in (not selected: the
|
||||
/// usual choice applies).
|
||||
Future<void> _addSubtitles(HostedPlayer p, List<YtDlpSubtitle> subs) async {
|
||||
@@ -303,6 +318,7 @@ class MediaPipeline {
|
||||
_openHeaders = headers;
|
||||
_openResolve = resolve;
|
||||
_opening = true;
|
||||
_trimLocal();
|
||||
try {
|
||||
trace.mark(mediaId, SwitchTrace.open);
|
||||
final promoted = await _openInPlayer(url, headers,
|
||||
@@ -360,6 +376,7 @@ class MediaPipeline {
|
||||
}
|
||||
if (headers != null && headers.isNotEmpty) return 'Jellyfin, own sign-in';
|
||||
if (shared.has(url)) return 'link passed on, read once here';
|
||||
if (shared.hasLocal(url)) return 'link, read here with resume';
|
||||
return 'link, fetched here';
|
||||
}
|
||||
|
||||
@@ -465,6 +482,7 @@ class MediaPipeline {
|
||||
_openUrl = null;
|
||||
_openHeaders = null;
|
||||
await playback.stop();
|
||||
_trimLocal();
|
||||
}
|
||||
|
||||
/// The same media came back as [r.mediaId] (a restarted server put back
|
||||
@@ -546,6 +564,7 @@ class MediaPipeline {
|
||||
_openHeaders = null;
|
||||
_recovery = null;
|
||||
if (ready) await playback.stop();
|
||||
_trimLocal();
|
||||
}
|
||||
|
||||
/// The next state's media is new to this player: it opens again from
|
||||
@@ -794,9 +813,10 @@ class MediaPipeline {
|
||||
_pre = null;
|
||||
if (had != null) unawaited(_disposePrepared(had));
|
||||
final pre = _pre = _Prepared(url, headers ?? const {});
|
||||
_trimLocal();
|
||||
try {
|
||||
final target = await _resolve(url, headers, fresh: fresh);
|
||||
pre.shared = target.shared;
|
||||
pre.shared = target.shared && !target.local;
|
||||
if (!identical(_pre, pre)) {
|
||||
pre.fail();
|
||||
return false;
|
||||
@@ -857,6 +877,7 @@ class MediaPipeline {
|
||||
await pre.logSub?.cancel();
|
||||
final p = pre.player;
|
||||
if (p != null) await host.dispose(p);
|
||||
if (!identical(_pre, pre)) _trimLocal();
|
||||
}
|
||||
|
||||
/// [pre] becomes the session player; its buffer comes along.
|
||||
@@ -935,7 +956,10 @@ class _Target {
|
||||
|
||||
/// A link this member passes on, read through its shared reader.
|
||||
final bool shared;
|
||||
const _Target(this.url, this.headers, {this.subtitles = const [], this.shared = false});
|
||||
|
||||
/// Read through a [SharedLinks.local] reader (played here only).
|
||||
final bool local;
|
||||
const _Target(this.url, this.headers, {this.subtitles = const [], this.shared = false, this.local = false});
|
||||
}
|
||||
|
||||
/// A page could not be resolved: why ([message], yt-dlp's words) and which
|
||||
|
||||
@@ -20,6 +20,8 @@ class SharedLink {
|
||||
/// Each one's player opens [SharedSource.playerUrl] and its relay serves
|
||||
/// [SharedSource.upload]. A page with a pieces track (HLS/DASH) is not
|
||||
/// shared ([fits]): its player and relay keep their own fetches.
|
||||
/// Page links played here only ([local]) get a reader of their own too,
|
||||
/// for its resume after cuts.
|
||||
class SharedLinks {
|
||||
SharedLink? _current;
|
||||
({String queueId, SharedLink link})? _warm;
|
||||
@@ -146,6 +148,57 @@ class SharedLinks {
|
||||
/// [url] is shared here (the current media's or the warm one).
|
||||
bool has(String url) => _current?.url == url || _warm?.link.url == url;
|
||||
|
||||
// Page links this app plays without passing them on (alone, or
|
||||
// everyone fetches it), by page URL: the session player's and the
|
||||
// preload's.
|
||||
final _local = <String, Future<SharedLink?>>{};
|
||||
|
||||
/// A page link played here only: read through its own [SharedSource]
|
||||
/// over [media] too, so the player gets the reader's resume after a cut,
|
||||
/// back-off and fresh address (403/410/412) instead of ffmpeg's
|
||||
/// reconnect, which a CDN like Bilibili cuts off for good. The one [url]
|
||||
/// has is kept unless [fresh]. Null when the page does not fit ([fits]:
|
||||
/// a pieces track) or the reader cannot open it: the player reads the
|
||||
/// site itself. [trimLocal] closes the ones no longer needed.
|
||||
Future<SharedLink?> local(String url, LinkProbe probe, RelayMedia Function() media,
|
||||
{bool fresh = false, Duration timeout = const Duration(seconds: 15)}) {
|
||||
final had = _local[url];
|
||||
if (had != null && !fresh) return had;
|
||||
if (had != null) _local.remove(url)!.then((l) => l?.source.close());
|
||||
if (!fits(probe)) return Future.value();
|
||||
Future<SharedLink?> make() async {
|
||||
final s = SharedSource(media());
|
||||
try {
|
||||
// The sizes too: a format URL the site refuses (403) fails here,
|
||||
// and the player then gets the site's own answer.
|
||||
await s.start().timeout(timeout);
|
||||
return SharedLink(url, s, subtitles: _subs(probe));
|
||||
} catch (e) {
|
||||
_log.info('link ${logUrl(url)} read by the player itself: $e');
|
||||
unawaited(s.close());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
final f = _local[url] = make();
|
||||
unawaited(f.then((l) {
|
||||
if (l == null && identical(_local[url], f)) _local.remove(url);
|
||||
}));
|
||||
return f;
|
||||
}
|
||||
|
||||
/// [url] is read here through a [local] reader.
|
||||
bool hasLocal(String url) => _local.containsKey(url);
|
||||
|
||||
/// Closes the [local] readers of every URL but [keep] (the player's and
|
||||
/// the preload's).
|
||||
void trimLocal(Set<String?> keep) {
|
||||
for (final url in [..._local.keys]) {
|
||||
if (keep.contains(url)) continue;
|
||||
unawaited(_local.remove(url)!.then((l) => l?.source.close()));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> closeCurrent() async {
|
||||
final c = _current;
|
||||
_current = null;
|
||||
@@ -156,6 +209,7 @@ class SharedLinks {
|
||||
Future<void> closeAll() async {
|
||||
_dropPending();
|
||||
dropWarm();
|
||||
trimLocal(const {});
|
||||
await closeCurrent();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user