From 517543f574a984d340734def8d111103d0460b8a Mon Sep 17 00:00:00 2001 From: professorDeveloper <108933534+professorDeveloper@users.noreply.github.com> Date: Tue, 10 Nov 2026 10:13:05 +0500 Subject: [PATCH] Player: a server that cannot seek is a notice, not an error UHD Movies' Google links ignore byte ranges. mpv reports "Cannot seek in this stream" on every skip, and the page treated it as a playback error, reopening the url at 0:00. Viewers saw a 30 s skip start the film over. The mpv controller now raises seekRefused instead of an error, the film plays on, and the player says once that this server cannot skip, with Change server when the title has other servers. --- assets/translations/ar.json | 1 + assets/translations/ckb.json | 1 + assets/translations/de.json | 1 + assets/translations/en.json | 1 + assets/translations/es.json | 1 + assets/translations/fr.json | 1 + assets/translations/hi.json | 1 + assets/translations/id.json | 1 + assets/translations/ku.json | 1 + assets/translations/nl.json | 1 + assets/translations/pt.json | 1 + assets/translations/ru.json | 1 + assets/translations/tr.json | 1 + assets/translations/uz.json | 1 + assets/translations/vi.json | 1 + assets/translations/yue.json | 1 + lib/core/player/media_controller.dart | 18 ++++++++++++++++++ lib/core/player/playback_readiness.dart | 9 +++++++++ .../detail/presentation/pages/player_page.dart | 1 + .../presentation/pages/player_page.media.dart | 2 ++ .../presentation/pages/player_page.toast.dart | 16 ++++++++++++++++ test/core/player/playback_readiness_test.dart | 11 +++++++++++ 22 files changed, 73 insertions(+) diff --git a/assets/translations/ar.json b/assets/translations/ar.json index a37719b0..56175953 100644 --- a/assets/translations/ar.json +++ b/assets/translations/ar.json @@ -408,6 +408,7 @@ "subtitle_preview_text": "نصٌّ تجريبيٌّ لمعاينة شكل الترجمة", "server": "الخادم", "change_server": "تغيير الخادم", + "cannot_seek_server": "لا يمكن التقديم على هذا الخادم. شاهد من البداية أو اختر خادمًا آخر.", "stream_not_found": "يُخفي هذا المصدر بثّه خلف مشغّل محمي. جرّب مصدراً آخر، أو افتحه في متصفّحك.", "subtitle_translation": "ترجمة النصوص", "auto_translate": "الترجمة التلقائية", diff --git a/assets/translations/ckb.json b/assets/translations/ckb.json index 5073368a..676b6d34 100644 --- a/assets/translations/ckb.json +++ b/assets/translations/ckb.json @@ -422,6 +422,7 @@ "subtitle_preview_text": "ڕێوییە قاوەییە خێراکە بەسەر سەگە تەمەڵەکەدا باز دەدات", "server": "سێرڤەر", "change_server": "گۆڕینی سێرڤەر", + "cannot_seek_server": "لەم سێرڤەرەدا ناتوانرێت بازبدرێت بۆ پێشەوە. لە سەرەتاوە سەیری بکە یان سێرڤەرێکی تر هەڵبژێرە.", "stream_not_found": "ئەم سەرچاوەیە پەخشەکەی لە پشت لێدەرێکی پارێزراو شاردۆتەوە. سەرچاوەیەکی تر تاقیبکەرەوە، یان لە وێبگەڕەکەتدا بیکەرەوە.", "subtitle_translation": "وەرگێڕانی ژێرنووس", "auto_translate": "وەرگێڕانی خۆکار", diff --git a/assets/translations/de.json b/assets/translations/de.json index 78d358cc..70ad67ef 100644 --- a/assets/translations/de.json +++ b/assets/translations/de.json @@ -408,6 +408,7 @@ "subtitle_preview_text": "Franz jagt im komplett verwahrlosten Taxi quer durch Bayern", "server": "Server", "change_server": "Server wechseln", + "cannot_seek_server": "Auf diesem Server kann man nicht vorspulen. Schau von Anfang an oder wähle einen anderen Server.", "stream_not_found": "Diese Quelle versteckt ihren Stream hinter einem geschützten Player. Probiere eine andere Quelle oder öffne sie im Browser.", "subtitle_translation": "Untertitelübersetzung", "auto_translate": "Automatisch übersetzen", diff --git a/assets/translations/en.json b/assets/translations/en.json index c91759f6..bf998d37 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -422,6 +422,7 @@ "subtitle_preview_text": "The quick brown fox jumps over the lazy dog", "server": "Server", "change_server": "Change server", + "cannot_seek_server": "This server can't skip ahead. Play from the start, or pick another server.", "stream_not_found": "This source hides its stream behind a protected player. Try another source, or open it in your browser.", "subtitle_translation": "Subtitle translation", "auto_translate": "Auto-translate", diff --git a/assets/translations/es.json b/assets/translations/es.json index 16ffea50..66ab3752 100644 --- a/assets/translations/es.json +++ b/assets/translations/es.json @@ -408,6 +408,7 @@ "subtitle_preview_text": "El veloz murciélago hindú comía feliz cardillo y kiwi", "server": "Servidor", "change_server": "Cambiar servidor", + "cannot_seek_server": "Este servidor no permite adelantar. Míralo desde el principio o elige otro servidor.", "stream_not_found": "Esta fuente esconde su flujo detrás de un reproductor protegido. Prueba otra fuente o ábrela en el navegador.", "subtitle_translation": "Traducción de subtítulos", "auto_translate": "Traducir automáticamente", diff --git a/assets/translations/fr.json b/assets/translations/fr.json index 5aa0e570..172f5b75 100644 --- a/assets/translations/fr.json +++ b/assets/translations/fr.json @@ -408,6 +408,7 @@ "subtitle_preview_text": "Portez ce vieux whisky au juge blond qui fume", "server": "Serveur", "change_server": "Changer de serveur", + "cannot_seek_server": "Ce serveur ne permet pas d'avancer. Regardez depuis le début ou choisissez un autre serveur.", "stream_not_found": "Cette source cache son flux derrière un lecteur protégé. Essaie une autre source, ou ouvre-la dans ton navigateur.", "subtitle_translation": "Traduction des sous-titres", "auto_translate": "Traduction automatique", diff --git a/assets/translations/hi.json b/assets/translations/hi.json index ccf57325..2d84c664 100644 --- a/assets/translations/hi.json +++ b/assets/translations/hi.json @@ -422,6 +422,7 @@ "subtitle_preview_text": "The quick brown fox jumps over the lazy dog", "server": "सर्वर", "change_server": "सर्वर बदलें", + "cannot_seek_server": "इस सर्वर पर आगे नहीं बढ़ा सकते। शुरू से देखें या कोई दूसरा सर्वर चुनें।", "stream_not_found": "यह सोर्स अपनी स्ट्रीम को एक सुरक्षित प्लेयर के पीछे छिपाता है। कोई दूसरा सोर्स आज़माएँ या इसे अपने ब्राउज़र में खोलें।", "subtitle_translation": "सबटाइटल अनुवाद", "auto_translate": "ऑटो-अनुवाद", diff --git a/assets/translations/id.json b/assets/translations/id.json index f1993c42..67e90c3f 100644 --- a/assets/translations/id.json +++ b/assets/translations/id.json @@ -408,6 +408,7 @@ "subtitle_preview_text": "Muharjo seorang xenofobia universal yang takut pada kelinci", "server": "Server", "change_server": "Ganti server", + "cannot_seek_server": "Server ini tidak bisa dipercepat. Tonton dari awal atau pilih server lain.", "stream_not_found": "Sumber ini menyembunyikan alirannya di balik pemutar terlindungi. Coba sumber lain, atau buka di peramban.", "subtitle_translation": "Terjemahan subtitle", "auto_translate": "Terjemahkan otomatis", diff --git a/assets/translations/ku.json b/assets/translations/ku.json index 5e4e47f1..31a3065f 100644 --- a/assets/translations/ku.json +++ b/assets/translations/ku.json @@ -422,6 +422,7 @@ "subtitle_preview_text": "Rovîyê qehweyî yê bilez di ser kûçikê tembel re diqevze", "server": "Server", "change_server": "Serverê biguherîne", + "cannot_seek_server": "Li ser vê serverê nikare were pêşve birin. Ji destpêkê temaşe bike an serverek din hilbijêre.", "stream_not_found": "Ev çavkanî weşana xwe li pişt lêdarekî parastî vedişêre. Çavkaniyeke din biceribîne, an wê di gerokê de veke.", "subtitle_translation": "Wergera binnivîsê", "auto_translate": "Wergera otomatîk", diff --git a/assets/translations/nl.json b/assets/translations/nl.json index bed20698..67eac8f2 100644 --- a/assets/translations/nl.json +++ b/assets/translations/nl.json @@ -408,6 +408,7 @@ "subtitle_preview_text": "Pa's wijze lynx bezag vroom het fikse aquaduct", "server": "Server", "change_server": "Server wisselen", + "cannot_seek_server": "Op deze server kun je niet vooruitspoelen. Kijk vanaf het begin of kies een andere server.", "stream_not_found": "Deze bron verstopt zijn stream achter een beveiligde speler. Probeer een andere bron of open hem in je browser.", "subtitle_translation": "Ondertitelvertaling", "auto_translate": "Automatisch vertalen", diff --git a/assets/translations/pt.json b/assets/translations/pt.json index 593ce761..725d644c 100644 --- a/assets/translations/pt.json +++ b/assets/translations/pt.json @@ -408,6 +408,7 @@ "subtitle_preview_text": "Um pequeno jabuti xereta viu dez cegonhas felizes", "server": "Servidor", "change_server": "Trocar de servidor", + "cannot_seek_server": "Este servidor não permite avançar. Assista desde o início ou escolha outro servidor.", "stream_not_found": "Esta fonte esconde o stream atrás de um player protegido. Tente outra fonte ou abra no navegador.", "subtitle_translation": "Tradução de legendas", "auto_translate": "Traduzir automaticamente", diff --git a/assets/translations/ru.json b/assets/translations/ru.json index 2e068a8a..c6ae867f 100644 --- a/assets/translations/ru.json +++ b/assets/translations/ru.json @@ -408,6 +408,7 @@ "subtitle_preview_text": "Съешь ещё этих мягких французских булок", "server": "Сервер", "change_server": "Сменить сервер", + "cannot_seek_server": "На этом сервере нельзя перематывать. Смотрите с начала или выберите другой сервер.", "stream_not_found": "Этот источник прячет поток за защищённым плеером. Выберите другой источник или откройте в браузере.", "subtitle_translation": "Перевод субтитров", "auto_translate": "Автоперевод", diff --git a/assets/translations/tr.json b/assets/translations/tr.json index 86029116..b6bc23c4 100644 --- a/assets/translations/tr.json +++ b/assets/translations/tr.json @@ -408,6 +408,7 @@ "subtitle_preview_text": "Pijamalı hasta yağız şoföre çabucak güvendi", "server": "Sunucu", "change_server": "Sunucuyu değiştir", + "cannot_seek_server": "Bu sunucuda ileri sarılamıyor. Baştan izleyin veya başka bir sunucu seçin.", "stream_not_found": "Bu kaynak akışını korumalı bir oynatıcının arkasına saklıyor. Başka bir kaynak dene ya da tarayıcında aç.", "subtitle_translation": "Altyazı çevirisi", "auto_translate": "Otomatik çevir", diff --git a/assets/translations/uz.json b/assets/translations/uz.json index 7eeb8ab6..ec21a8ce 100644 --- a/assets/translations/uz.json +++ b/assets/translations/uz.json @@ -408,6 +408,7 @@ "subtitle_preview_text": "Tez qo’ng’ir tulki yalqov itning ustidan sakraydi", "server": "Server", "change_server": "Serverni almashtirish", + "cannot_seek_server": "Bu serverda oldinga o'tkazib bo'lmaydi. Boshidan ko'ring yoki boshqa server tanlang.", "stream_not_found": "Bu manba oqimini himoyalangan pleyer ortida yashiradi. Boshqa manbani tanlang yoki brauzerda oching.", "subtitle_translation": "Subtitr tarjimasi", "auto_translate": "Avtomatik tarjima", diff --git a/assets/translations/vi.json b/assets/translations/vi.json index c9acbd4f..ce8c89da 100644 --- a/assets/translations/vi.json +++ b/assets/translations/vi.json @@ -422,6 +422,7 @@ "subtitle_preview_text": "Con cáo nâu nhanh nhẹn nhảy qua chú chó lười", "server": "Máy chủ", "change_server": "Đổi máy chủ", + "cannot_seek_server": "Máy chủ này không tua được. Hãy xem từ đầu hoặc chọn máy chủ khác.", "stream_not_found": "Nguồn này giấu luồng phát sau một trình phát được bảo vệ. Hãy thử nguồn khác hoặc mở trong trình duyệt.", "subtitle_translation": "Dịch phụ đề", "auto_translate": "Tự động dịch", diff --git a/assets/translations/yue.json b/assets/translations/yue.json index 633e60af..66525659 100644 --- a/assets/translations/yue.json +++ b/assets/translations/yue.json @@ -422,6 +422,7 @@ "subtitle_preview_text": "敏捷嘅棕色狐狸跳過隻懶狗", "server": "伺服器", "change_server": "更換伺服器", + "cannot_seek_server": "呢個伺服器唔可以快轉。請由頭睇,或者揀另一個伺服器。", "stream_not_found": "呢個來源將串流收埋喺受保護嘅播放器後面。試下另一個來源,或者喺瀏覽器度開。", "subtitle_translation": "字幕翻譯", "auto_translate": "自動翻譯", diff --git a/lib/core/player/media_controller.dart b/lib/core/player/media_controller.dart index 9b5ae7c1..f9a50df7 100644 --- a/lib/core/player/media_controller.dart +++ b/lib/core/player/media_controller.dart @@ -223,6 +223,10 @@ abstract class PlayerController extends ValueNotifier { /// subtitle style, the same way it draws a downloaded subtitle file. ValueListenable get embeddedSubtitleText => _noEmbeddedText; + /// Turns true when the server refused a jump: the stream plays, but only + /// from where it is. mpv only; it never fires on the platform player. + ValueListenable get seekRefused => _neverRefused; + /// Whether this backend can adjust the picture while playing. /// /// libmpv only. The platform player exposes no runtime video equalizer at @@ -438,6 +442,7 @@ class PlayerAudioTrack { } final ValueNotifier _noEmbeddedText = ValueNotifier(''); +final ValueNotifier _neverRefused = ValueNotifier(false); /// One subtitle track inside the stream. class PlayerSubtitleTrack extends PlayerAudioTrack { @@ -696,6 +701,7 @@ class _MediaKitController extends PlayerController { List _subtitleTracks = const []; String? _activeSubtitleTrackId; final ValueNotifier _embeddedText = ValueNotifier(''); + final ValueNotifier _seekRefused = ValueNotifier(false); String? _activeVideoTrackId; final List> _subs = >[]; @@ -1127,6 +1133,13 @@ class _MediaKitController extends PlayerController { ) ..add( _player.stream.error.listen((e) { + // Reported as an error, it reopened the url at 0:00 — "skip 30 s + // starts it from the start" on UHD Movies' Google links, which + // ignore byte ranges. The film itself plays on. + if (isSeekRefusal(e)) { + _seekRefused.value = true; + return; + } // A live stream has no length and no cache to ride out a stall // on: once it plays, a dropped connection is reported at once so // the page reconnects instead of showing a frozen picture. @@ -1494,6 +1507,10 @@ class _MediaKitController extends PlayerController { ValueListenable get embeddedSubtitleText => _fallback != null ? _noEmbeddedText : _embeddedText; + @override + ValueListenable get seekRefused => + _fallback != null ? _neverRefused : _seekRefused; + @override Future setSubtitleTrack(String id) async { if (_fallback != null) return; @@ -1556,6 +1573,7 @@ class _MediaKitController extends PlayerController { Future _disposeOnce() async { if (!_closed.isCompleted) _closed.complete(); + _seekRefused.dispose(); await _teardownMpv(); await _backendSwitches.close(); await _fallback?.dispose(); diff --git a/lib/core/player/playback_readiness.dart b/lib/core/player/playback_readiness.dart index cd0b474d..62c31423 100644 --- a/lib/core/player/playback_readiness.dart +++ b/lib/core/player/playback_readiness.dart @@ -242,3 +242,12 @@ bool isDecodeGlitch(String error) { final l = error.toLowerCase(); return l.startsWith('error decoding') || l.contains('error while decoding'); } + +/// mpv's notice that the server ignores byte ranges: "Cannot seek in this +/// stream." and its "--force-seekable=yes" hint. The file keeps playing; only a +/// jump is refused, so it is not a playback error. +bool isSeekRefusal(String error) { + final l = error.toLowerCase(); + return l.contains('cannot seek in this stream') || + l.contains('force-seekable'); +} diff --git a/lib/features/detail/presentation/pages/player_page.dart b/lib/features/detail/presentation/pages/player_page.dart index 59aa3535..12626be9 100644 --- a/lib/features/detail/presentation/pages/player_page.dart +++ b/lib/features/detail/presentation/pages/player_page.dart @@ -1066,6 +1066,7 @@ class _PlayerPageState extends State final c = _controller; if (c != null) { c.removeListener(_onMajorChange); + c.seekRefused.removeListener(_onSeekRefused); try { c.pause(); } catch (_) {} diff --git a/lib/features/detail/presentation/pages/player_page.media.dart b/lib/features/detail/presentation/pages/player_page.media.dart index 71551e8b..c7f3ddf4 100644 --- a/lib/features/detail/presentation/pages/player_page.media.dart +++ b/lib/features/detail/presentation/pages/player_page.media.dart @@ -1689,6 +1689,7 @@ extension _PlayerMedia on _PlayerPageState { // Preview decoding starts only when the viewer scrubs. Eager warming // opens a second video decoder while a high-resolution stream starts. controller.addListener(_onMajorChange); + controller.seekRefused.addListener(_onSeekRefused); await controller.setLooping(false); if (!mounted || generation != _mediaGeneration) return; if (party != null) { @@ -2687,6 +2688,7 @@ extension _PlayerMedia on _PlayerPageState { unawaited(FramePreviewService.close()); if (c != null) { c.removeListener(_onMajorChange); + c.seekRefused.removeListener(_onSeekRefused); try { await c.pause(); } catch (_) {} diff --git a/lib/features/detail/presentation/pages/player_page.toast.dart b/lib/features/detail/presentation/pages/player_page.toast.dart index 9527d9ca..e638f787 100644 --- a/lib/features/detail/presentation/pages/player_page.toast.dart +++ b/lib/features/detail/presentation/pages/player_page.toast.dart @@ -24,6 +24,22 @@ extension _PlayerToasts on _PlayerPageState { }); } + /// The server ignores byte ranges: the film plays, but a skip cannot land. + /// Said once per stream, with the way out when this title has other servers. + void _onSeekRefused() { + final c = _controller; + if (c == null || !c.seekRefused.value) return; + _plog('stream is not seekable on this server', level: LogLevel.warn); + final servers = _affordances.hasServers; + _toast( + 'player.cannot_seek_server'.tr(), + icon: Icons.fast_forward_rounded, + duration: const Duration(seconds: 6), + action: servers ? 'player.change_server'.tr() : null, + onAction: servers ? _openServerSheet : null, + ); + } + void _dismissToast() { _toastTimer?.cancel(); _toastNote.value = null; diff --git a/test/core/player/playback_readiness_test.dart b/test/core/player/playback_readiness_test.dart index a8cd2c89..da526951 100644 --- a/test/core/player/playback_readiness_test.dart +++ b/test/core/player/playback_readiness_test.dart @@ -388,4 +388,15 @@ void main() { expect(isDecodeGlitch('Could not open codec.'), isFalse); expect(isDecodeGlitch('tcp: ffurl_read returned 0xffffff92'), isFalse); }); + + test('a refused seek is a notice, not a playback failure', () { + // UHD Movies' Google links ignore byte ranges (support ticket, 2026-11-09). + expect(isSeekRefusal('Cannot seek in this stream.'), isTrue); + expect( + isSeekRefusal("You can force it with '--force-seekable=yes'."), + isTrue, + ); + expect(isSeekRefusal('tcp: ffurl_read returned 0xffffff92'), isFalse); + expect(isSeekRefusal('Failed to open https://example.com/a.mkv.'), isFalse); + }); }