fix(player): surface a persistent HTTP 503 at open instead of retrying forever
ffmpeg's reconnect loop deliberately retries 503 without bound (#1520), so a server that keeps refusing the stream at open time left a silent black screen: ExoPlayer fell back to MPV, MPV reconnected forever, and no error ever reached the screen. A new open-phase watchdog arms on the first 503 seen before any frame renders and, after 20s without one, synthesizes a server-http-503 error that shows an actionable dialog. Mid-stream 503s and live TV keep their existing ride-out paths. close #1830
This commit is contained in:
@@ -744,6 +744,8 @@
|
|||||||
"serverLimitBody": "Server xətası (HTTP 500). Məhdudiyyət bu seansı rədd etdi.",
|
"serverLimitBody": "Server xətası (HTTP 500). Məhdudiyyət bu seansı rədd etdi.",
|
||||||
"mediaUnreadableTitle": "Fayl əlçatan deyil",
|
"mediaUnreadableTitle": "Fayl əlçatan deyil",
|
||||||
"mediaUnreadableBody": "Server bu elementi tapdı, lakin onun faylını oxuya bilmədi (HTTP 404). Fayl yəqin ki, köçürülüb, silinib və ya onun saxlanma yeri oflayndır. Server sahibindən faylı yoxlamasını və kitabxananı yenidən skan etməsini xahiş edin.",
|
"mediaUnreadableBody": "Server bu elementi tapdı, lakin onun faylını oxuya bilmədi (HTTP 404). Fayl yəqin ki, köçürülüb, silinib və ya onun saxlanma yeri oflayndır. Server sahibindən faylı yoxlamasını və kitabxananı yenidən skan etməsini xahiş edin.",
|
||||||
|
"serverBusyTitle": "",
|
||||||
|
"serverBusyBody": "",
|
||||||
"logsUploaded": "Jurnallar yükləndi",
|
"logsUploaded": "Jurnallar yükləndi",
|
||||||
"logsUploadFailed": "Jurnallar yüklənə bilmədi",
|
"logsUploadFailed": "Jurnallar yüklənə bilmədi",
|
||||||
"logId": "Jurnal ID-si"
|
"logId": "Jurnal ID-si"
|
||||||
|
|||||||
@@ -744,6 +744,8 @@
|
|||||||
"serverLimitBody": "Грешка на сървъра (HTTP 500). Вероятно лимит за пропускателна способност/транскодиране е отхвърлил тази сесия. Помолете собственика да го коригира.",
|
"serverLimitBody": "Грешка на сървъра (HTTP 500). Вероятно лимит за пропускателна способност/транскодиране е отхвърлил тази сесия. Помолете собственика да го коригира.",
|
||||||
"mediaUnreadableTitle": "Файлът е недостъпен",
|
"mediaUnreadableTitle": "Файлът е недостъпен",
|
||||||
"mediaUnreadableBody": "Сървърът намери този елемент, но не можа да прочете файла му (HTTP 404). Файлът вероятно е бил преместен, изтрит или хранилището му е офлайн. Помолете собственика на сървъра да провери файла и да сканира отново библиотеката.",
|
"mediaUnreadableBody": "Сървърът намери този елемент, но не можа да прочете файла му (HTTP 404). Файлът вероятно е бил преместен, изтрит или хранилището му е офлайн. Помолете собственика на сървъра да провери файла и да сканира отново библиотеката.",
|
||||||
|
"serverBusyTitle": "",
|
||||||
|
"serverBusyBody": "",
|
||||||
"logsUploaded": "Логовете са качени",
|
"logsUploaded": "Логовете са качени",
|
||||||
"logsUploadFailed": "Неуспешно качване на логовете",
|
"logsUploadFailed": "Неуспешно качване на логовете",
|
||||||
"logId": "ID на лога"
|
"logId": "ID на лога"
|
||||||
|
|||||||
@@ -744,6 +744,8 @@
|
|||||||
"serverLimitBody": "Serverfejl (HTTP 500). En båndbredde- eller transkodningsgrænse afviste sandsynligvis sessionen. Bed ejeren om at justere den.",
|
"serverLimitBody": "Serverfejl (HTTP 500). En båndbredde- eller transkodningsgrænse afviste sandsynligvis sessionen. Bed ejeren om at justere den.",
|
||||||
"mediaUnreadableTitle": "Fil utilgængelig",
|
"mediaUnreadableTitle": "Fil utilgængelig",
|
||||||
"mediaUnreadableBody": "Serveren fandt dette emne, men kunne ikke læse filen (HTTP 404). Filen er sandsynligvis flyttet, slettet, eller dens lager er offline. Bed serverejeren om at kontrollere filen og scanne biblioteket igen.",
|
"mediaUnreadableBody": "Serveren fandt dette emne, men kunne ikke læse filen (HTTP 404). Filen er sandsynligvis flyttet, slettet, eller dens lager er offline. Bed serverejeren om at kontrollere filen og scanne biblioteket igen.",
|
||||||
|
"serverBusyTitle": "",
|
||||||
|
"serverBusyBody": "",
|
||||||
"logsUploaded": "Logfilerne blev uploadet",
|
"logsUploaded": "Logfilerne blev uploadet",
|
||||||
"logsUploadFailed": "Logfilerne kunne ikke uploades",
|
"logsUploadFailed": "Logfilerne kunne ikke uploades",
|
||||||
"logId": "Log-ID"
|
"logId": "Log-ID"
|
||||||
|
|||||||
@@ -744,6 +744,8 @@
|
|||||||
"serverLimitBody": "Serverfehler (HTTP 500). Vermutlich hat ein Bandbreiten- oder Transkodierungslimit diese Sitzung abgelehnt. Bitte den Besitzer, das Limit anzupassen.",
|
"serverLimitBody": "Serverfehler (HTTP 500). Vermutlich hat ein Bandbreiten- oder Transkodierungslimit diese Sitzung abgelehnt. Bitte den Besitzer, das Limit anzupassen.",
|
||||||
"mediaUnreadableTitle": "Datei nicht verfügbar",
|
"mediaUnreadableTitle": "Datei nicht verfügbar",
|
||||||
"mediaUnreadableBody": "Der Server hat dieses Element gefunden, konnte seine Datei aber nicht lesen (HTTP 404). Die Datei wurde wahrscheinlich verschoben oder gelöscht, oder ihr Speicher ist offline. Bitte den Serverbesitzer, die Datei zu prüfen und die Mediathek neu zu scannen.",
|
"mediaUnreadableBody": "Der Server hat dieses Element gefunden, konnte seine Datei aber nicht lesen (HTTP 404). Die Datei wurde wahrscheinlich verschoben oder gelöscht, oder ihr Speicher ist offline. Bitte den Serverbesitzer, die Datei zu prüfen und die Mediathek neu zu scannen.",
|
||||||
|
"serverBusyTitle": "",
|
||||||
|
"serverBusyBody": "",
|
||||||
"logsUploaded": "Protokolle hochgeladen",
|
"logsUploaded": "Protokolle hochgeladen",
|
||||||
"logsUploadFailed": "Protokolle konnten nicht hochgeladen werden",
|
"logsUploadFailed": "Protokolle konnten nicht hochgeladen werden",
|
||||||
"logId": "Protokoll-ID"
|
"logId": "Protokoll-ID"
|
||||||
|
|||||||
@@ -744,6 +744,8 @@
|
|||||||
"serverLimitBody": "Server error (HTTP 500). A bandwidth/transcoding limit likely rejected this session. Ask the owner to adjust it.",
|
"serverLimitBody": "Server error (HTTP 500). A bandwidth/transcoding limit likely rejected this session. Ask the owner to adjust it.",
|
||||||
"mediaUnreadableTitle": "File unavailable",
|
"mediaUnreadableTitle": "File unavailable",
|
||||||
"mediaUnreadableBody": "The server found this item but could not read its file (HTTP 404). The file was probably moved, deleted, or its storage is offline. Ask the server owner to check the file and rescan the library.",
|
"mediaUnreadableBody": "The server found this item but could not read its file (HTTP 404). The file was probably moved, deleted, or its storage is offline. Ask the server owner to check the file and rescan the library.",
|
||||||
|
"serverBusyTitle": "Stream unavailable",
|
||||||
|
"serverBusyBody": "The server kept refusing to stream this file (HTTP 503). It may be restarting, busy, or the file's storage may be offline. Try again in a moment — if it keeps happening, ask the server owner to check the server and the file's storage.",
|
||||||
"logsUploaded": "Logs uploaded",
|
"logsUploaded": "Logs uploaded",
|
||||||
"logsUploadFailed": "Failed to upload logs",
|
"logsUploadFailed": "Failed to upload logs",
|
||||||
"logId": "Log ID"
|
"logId": "Log ID"
|
||||||
|
|||||||
@@ -744,6 +744,8 @@
|
|||||||
"serverLimitBody": "Error del servidor (HTTP 500). Un límite de ancho de banda/transcodificación probablemente rechazó esta sesión. Pide al propietario que lo ajuste.",
|
"serverLimitBody": "Error del servidor (HTTP 500). Un límite de ancho de banda/transcodificación probablemente rechazó esta sesión. Pide al propietario que lo ajuste.",
|
||||||
"mediaUnreadableTitle": "Archivo no disponible",
|
"mediaUnreadableTitle": "Archivo no disponible",
|
||||||
"mediaUnreadableBody": "El servidor encontró este elemento pero no pudo leer su archivo (HTTP 404). Es probable que el archivo se haya movido o eliminado, o que su almacenamiento esté sin conexión. Pide al propietario del servidor que revise el archivo y vuelva a escanear la biblioteca.",
|
"mediaUnreadableBody": "El servidor encontró este elemento pero no pudo leer su archivo (HTTP 404). Es probable que el archivo se haya movido o eliminado, o que su almacenamiento esté sin conexión. Pide al propietario del servidor que revise el archivo y vuelva a escanear la biblioteca.",
|
||||||
|
"serverBusyTitle": "",
|
||||||
|
"serverBusyBody": "",
|
||||||
"logsUploaded": "Registros subidos",
|
"logsUploaded": "Registros subidos",
|
||||||
"logsUploadFailed": "Error al subir registros",
|
"logsUploadFailed": "Error al subir registros",
|
||||||
"logId": "ID de registro"
|
"logId": "ID de registro"
|
||||||
|
|||||||
@@ -744,6 +744,8 @@
|
|||||||
"serverLimitBody": "Erreur serveur (HTTP 500). Une limite de bande passante/transcodage a probablement rejeté cette session. Demandez au propriétaire de l'ajuster.",
|
"serverLimitBody": "Erreur serveur (HTTP 500). Une limite de bande passante/transcodage a probablement rejeté cette session. Demandez au propriétaire de l'ajuster.",
|
||||||
"mediaUnreadableTitle": "Fichier indisponible",
|
"mediaUnreadableTitle": "Fichier indisponible",
|
||||||
"mediaUnreadableBody": "Le serveur a trouvé cet élément mais n'a pas pu lire son fichier (HTTP 404). Le fichier a probablement été déplacé, supprimé, ou son stockage est hors ligne. Demandez au propriétaire du serveur de vérifier le fichier et de relancer l'analyse de la bibliothèque.",
|
"mediaUnreadableBody": "Le serveur a trouvé cet élément mais n'a pas pu lire son fichier (HTTP 404). Le fichier a probablement été déplacé, supprimé, ou son stockage est hors ligne. Demandez au propriétaire du serveur de vérifier le fichier et de relancer l'analyse de la bibliothèque.",
|
||||||
|
"serverBusyTitle": "",
|
||||||
|
"serverBusyBody": "",
|
||||||
"logsUploaded": "Journaux envoyés",
|
"logsUploaded": "Journaux envoyés",
|
||||||
"logsUploadFailed": "Échec de l’envoi des journaux",
|
"logsUploadFailed": "Échec de l’envoi des journaux",
|
||||||
"logId": "Identifiant du journal"
|
"logId": "Identifiant du journal"
|
||||||
|
|||||||
@@ -744,6 +744,8 @@
|
|||||||
"serverLimitBody": "Szerverhiba (HTTP 500). A munkamenetet valószínűleg egy sávszélességi vagy átkódolási korlát utasította el. Kérd meg a tulajdonost a korlát módosítására.",
|
"serverLimitBody": "Szerverhiba (HTTP 500). A munkamenetet valószínűleg egy sávszélességi vagy átkódolási korlát utasította el. Kérd meg a tulajdonost a korlát módosítására.",
|
||||||
"mediaUnreadableTitle": "A fájl nem érhető el",
|
"mediaUnreadableTitle": "A fájl nem érhető el",
|
||||||
"mediaUnreadableBody": "A szerver megtalálta ezt az elemet, de nem tudta beolvasni a fájlját (HTTP 404). A fájl valószínűleg áthelyezésre vagy törlésre került, vagy a tárhelye offline. Kérje meg a szerver tulajdonosát, hogy ellenőrizze a fájlt, és vizsgálja újra a könyvtárat.",
|
"mediaUnreadableBody": "A szerver megtalálta ezt az elemet, de nem tudta beolvasni a fájlját (HTTP 404). A fájl valószínűleg áthelyezésre vagy törlésre került, vagy a tárhelye offline. Kérje meg a szerver tulajdonosát, hogy ellenőrizze a fájlt, és vizsgálja újra a könyvtárat.",
|
||||||
|
"serverBusyTitle": "",
|
||||||
|
"serverBusyBody": "",
|
||||||
"logsUploaded": "Naplók feltöltve",
|
"logsUploaded": "Naplók feltöltve",
|
||||||
"logsUploadFailed": "Nem sikerült a naplók feltöltése",
|
"logsUploadFailed": "Nem sikerült a naplók feltöltése",
|
||||||
"logId": "Naplóazonosító"
|
"logId": "Naplóazonosító"
|
||||||
|
|||||||
@@ -744,6 +744,8 @@
|
|||||||
"serverLimitBody": "Errore del server (HTTP 500). È probabile che un limite di banda o transcodifica abbia impedito questa sessione. Chiedi al proprietario di modificare il limite.",
|
"serverLimitBody": "Errore del server (HTTP 500). È probabile che un limite di banda o transcodifica abbia impedito questa sessione. Chiedi al proprietario di modificare il limite.",
|
||||||
"mediaUnreadableTitle": "File non disponibile",
|
"mediaUnreadableTitle": "File non disponibile",
|
||||||
"mediaUnreadableBody": "Il server ha trovato questo elemento ma non ha potuto leggere il suo file (HTTP 404). Il file è stato probabilmente spostato o eliminato, oppure il suo storage è offline. Chiedi al proprietario del server di controllare il file e di riscansionare la libreria.",
|
"mediaUnreadableBody": "Il server ha trovato questo elemento ma non ha potuto leggere il suo file (HTTP 404). Il file è stato probabilmente spostato o eliminato, oppure il suo storage è offline. Chiedi al proprietario del server di controllare il file e di riscansionare la libreria.",
|
||||||
|
"serverBusyTitle": "",
|
||||||
|
"serverBusyBody": "",
|
||||||
"logsUploaded": "Log caricati",
|
"logsUploaded": "Log caricati",
|
||||||
"logsUploadFailed": "Impossibile caricare i log",
|
"logsUploadFailed": "Impossibile caricare i log",
|
||||||
"logId": "ID log"
|
"logId": "ID log"
|
||||||
|
|||||||
@@ -740,6 +740,8 @@
|
|||||||
"serverLimitBody": "サーバーエラー(HTTP 500)。帯域幅/トランスコード制限により拒否された可能性があります。所有者に調整を依頼してください。",
|
"serverLimitBody": "サーバーエラー(HTTP 500)。帯域幅/トランスコード制限により拒否された可能性があります。所有者に調整を依頼してください。",
|
||||||
"mediaUnreadableTitle": "ファイルを利用できません",
|
"mediaUnreadableTitle": "ファイルを利用できません",
|
||||||
"mediaUnreadableBody": "サーバーはこのアイテムを見つけましたが、そのファイルを読み取れませんでした(HTTP 404)。ファイルが移動・削除されたか、ストレージがオフラインの可能性があります。サーバーの管理者にファイルの確認とライブラリの再スキャンを依頼してください。",
|
"mediaUnreadableBody": "サーバーはこのアイテムを見つけましたが、そのファイルを読み取れませんでした(HTTP 404)。ファイルが移動・削除されたか、ストレージがオフラインの可能性があります。サーバーの管理者にファイルの確認とライブラリの再スキャンを依頼してください。",
|
||||||
|
"serverBusyTitle": "",
|
||||||
|
"serverBusyBody": "",
|
||||||
"logsUploaded": "ログをアップロードしました",
|
"logsUploaded": "ログをアップロードしました",
|
||||||
"logsUploadFailed": "ログのアップロードに失敗しました",
|
"logsUploadFailed": "ログのアップロードに失敗しました",
|
||||||
"logId": "ログID"
|
"logId": "ログID"
|
||||||
|
|||||||
@@ -744,6 +744,8 @@
|
|||||||
"serverLimitBody": "Сервер қатесі (HTTP 500). Шектеу бұл сеансты қабылдамады.",
|
"serverLimitBody": "Сервер қатесі (HTTP 500). Шектеу бұл сеансты қабылдамады.",
|
||||||
"mediaUnreadableTitle": "Файл қолжетімсіз",
|
"mediaUnreadableTitle": "Файл қолжетімсіз",
|
||||||
"mediaUnreadableBody": "Сервер бұл элементті тапты, бірақ оның файлын оқи алмады (HTTP 404). Файл жылжытылған, өшірілген немесе оның сақтау орны қолжетімсіз болуы мүмкін. Сервер иесінен файлды тексеріп, кітапхананы қайта сканерлеуді сұраңыз.",
|
"mediaUnreadableBody": "Сервер бұл элементті тапты, бірақ оның файлын оқи алмады (HTTP 404). Файл жылжытылған, өшірілген немесе оның сақтау орны қолжетімсіз болуы мүмкін. Сервер иесінен файлды тексеріп, кітапхананы қайта сканерлеуді сұраңыз.",
|
||||||
|
"serverBusyTitle": "",
|
||||||
|
"serverBusyBody": "",
|
||||||
"logsUploaded": "Журналдар жүктелді",
|
"logsUploaded": "Журналдар жүктелді",
|
||||||
"logsUploadFailed": "Журналдарды жүктеу мүмкін болмады",
|
"logsUploadFailed": "Журналдарды жүктеу мүмкін болмады",
|
||||||
"logId": "Журнал ID-сі"
|
"logId": "Журнал ID-сі"
|
||||||
|
|||||||
@@ -740,6 +740,8 @@
|
|||||||
"serverLimitBody": "서버 오류(HTTP 500). 대역폭/트랜스코딩 제한으로 세션이 거부된 것 같습니다. 소유자에게 조정을 요청하세요.",
|
"serverLimitBody": "서버 오류(HTTP 500). 대역폭/트랜스코딩 제한으로 세션이 거부된 것 같습니다. 소유자에게 조정을 요청하세요.",
|
||||||
"mediaUnreadableTitle": "파일을 사용할 수 없음",
|
"mediaUnreadableTitle": "파일을 사용할 수 없음",
|
||||||
"mediaUnreadableBody": "서버에서 이 항목을 찾았지만 파일을 읽을 수 없습니다(HTTP 404). 파일이 이동되거나 삭제되었거나 저장소가 오프라인 상태일 수 있습니다. 서버 관리자에게 파일 확인과 라이브러리 재스캔을 요청하세요.",
|
"mediaUnreadableBody": "서버에서 이 항목을 찾았지만 파일을 읽을 수 없습니다(HTTP 404). 파일이 이동되거나 삭제되었거나 저장소가 오프라인 상태일 수 있습니다. 서버 관리자에게 파일 확인과 라이브러리 재스캔을 요청하세요.",
|
||||||
|
"serverBusyTitle": "",
|
||||||
|
"serverBusyBody": "",
|
||||||
"logsUploaded": "로그 업로드 완료",
|
"logsUploaded": "로그 업로드 완료",
|
||||||
"logsUploadFailed": "로그 업로드 실패",
|
"logsUploadFailed": "로그 업로드 실패",
|
||||||
"logId": "로그 ID"
|
"logId": "로그 ID"
|
||||||
|
|||||||
@@ -744,6 +744,8 @@
|
|||||||
"serverLimitBody": "Serverfeil (HTTP 500). En båndbredde-/transkodingsgrense avviste trolig økten. Be eieren justere den.",
|
"serverLimitBody": "Serverfeil (HTTP 500). En båndbredde-/transkodingsgrense avviste trolig økten. Be eieren justere den.",
|
||||||
"mediaUnreadableTitle": "Fil utilgjengelig",
|
"mediaUnreadableTitle": "Fil utilgjengelig",
|
||||||
"mediaUnreadableBody": "Serveren fant dette elementet, men kunne ikke lese filen (HTTP 404). Filen er sannsynligvis flyttet, slettet, eller lagringen er frakoblet. Be serverens eier om å sjekke filen og skanne biblioteket på nytt.",
|
"mediaUnreadableBody": "Serveren fant dette elementet, men kunne ikke lese filen (HTTP 404). Filen er sannsynligvis flyttet, slettet, eller lagringen er frakoblet. Be serverens eier om å sjekke filen og skanne biblioteket på nytt.",
|
||||||
|
"serverBusyTitle": "",
|
||||||
|
"serverBusyBody": "",
|
||||||
"logsUploaded": "Logger lastet opp",
|
"logsUploaded": "Logger lastet opp",
|
||||||
"logsUploadFailed": "Kunne ikke laste opp logger",
|
"logsUploadFailed": "Kunne ikke laste opp logger",
|
||||||
"logId": "Logg-ID"
|
"logId": "Logg-ID"
|
||||||
|
|||||||
@@ -744,6 +744,8 @@
|
|||||||
"serverLimitBody": "Serverfout (HTTP 500). Waarschijnlijk weigerde een bandbreedte-/transcodeerlimiet deze sessie. Vraag de eigenaar dit aan te passen.",
|
"serverLimitBody": "Serverfout (HTTP 500). Waarschijnlijk weigerde een bandbreedte-/transcodeerlimiet deze sessie. Vraag de eigenaar dit aan te passen.",
|
||||||
"mediaUnreadableTitle": "Bestand niet beschikbaar",
|
"mediaUnreadableTitle": "Bestand niet beschikbaar",
|
||||||
"mediaUnreadableBody": "De server heeft dit item gevonden maar kon het bestand niet lezen (HTTP 404). Het bestand is waarschijnlijk verplaatst of verwijderd, of de opslag is offline. Vraag de serverbeheerder om het bestand te controleren en de bibliotheek opnieuw te scannen.",
|
"mediaUnreadableBody": "De server heeft dit item gevonden maar kon het bestand niet lezen (HTTP 404). Het bestand is waarschijnlijk verplaatst of verwijderd, of de opslag is offline. Vraag de serverbeheerder om het bestand te controleren en de bibliotheek opnieuw te scannen.",
|
||||||
|
"serverBusyTitle": "",
|
||||||
|
"serverBusyBody": "",
|
||||||
"logsUploaded": "Logbestanden geüpload",
|
"logsUploaded": "Logbestanden geüpload",
|
||||||
"logsUploadFailed": "Uploaden van logbestanden mislukt",
|
"logsUploadFailed": "Uploaden van logbestanden mislukt",
|
||||||
"logId": "Logboek-ID"
|
"logId": "Logboek-ID"
|
||||||
|
|||||||
@@ -752,6 +752,8 @@
|
|||||||
"serverLimitBody": "Błąd serwera (HTTP 500). Limit przepustowości/transkodowania prawdopodobnie odrzucił tę sesję. Poproś właściciela o zmianę.",
|
"serverLimitBody": "Błąd serwera (HTTP 500). Limit przepustowości/transkodowania prawdopodobnie odrzucił tę sesję. Poproś właściciela o zmianę.",
|
||||||
"mediaUnreadableTitle": "Plik niedostępny",
|
"mediaUnreadableTitle": "Plik niedostępny",
|
||||||
"mediaUnreadableBody": "Serwer znalazł ten element, ale nie mógł odczytać jego pliku (HTTP 404). Plik został prawdopodobnie przeniesiony, usunięty albo jego pamięć masowa jest niedostępna. Poproś właściciela serwera o sprawdzenie pliku i ponowne przeskanowanie biblioteki.",
|
"mediaUnreadableBody": "Serwer znalazł ten element, ale nie mógł odczytać jego pliku (HTTP 404). Plik został prawdopodobnie przeniesiony, usunięty albo jego pamięć masowa jest niedostępna. Poproś właściciela serwera o sprawdzenie pliku i ponowne przeskanowanie biblioteki.",
|
||||||
|
"serverBusyTitle": "",
|
||||||
|
"serverBusyBody": "",
|
||||||
"logsUploaded": "Logi przesłane",
|
"logsUploaded": "Logi przesłane",
|
||||||
"logsUploadFailed": "Nie udało się przesłać logów",
|
"logsUploadFailed": "Nie udało się przesłać logów",
|
||||||
"logId": "ID logu"
|
"logId": "ID logu"
|
||||||
|
|||||||
@@ -744,6 +744,8 @@
|
|||||||
"serverLimitBody": "Erro do servidor (HTTP 500). Um limite de largura de banda ou transcodificação provavelmente rejeitou esta sessão. Peça ao proprietário do servidor para ajustá-lo.",
|
"serverLimitBody": "Erro do servidor (HTTP 500). Um limite de largura de banda ou transcodificação provavelmente rejeitou esta sessão. Peça ao proprietário do servidor para ajustá-lo.",
|
||||||
"mediaUnreadableTitle": "Arquivo indisponível",
|
"mediaUnreadableTitle": "Arquivo indisponível",
|
||||||
"mediaUnreadableBody": "O servidor encontrou este item, mas não conseguiu ler o arquivo (HTTP 404). O arquivo provavelmente foi movido, excluído ou o armazenamento está offline. Peça ao responsável pelo servidor para verificar o arquivo e reexaminar a biblioteca.",
|
"mediaUnreadableBody": "O servidor encontrou este item, mas não conseguiu ler o arquivo (HTTP 404). O arquivo provavelmente foi movido, excluído ou o armazenamento está offline. Peça ao responsável pelo servidor para verificar o arquivo e reexaminar a biblioteca.",
|
||||||
|
"serverBusyTitle": "",
|
||||||
|
"serverBusyBody": "",
|
||||||
"logsUploaded": "Logs enviados",
|
"logsUploaded": "Logs enviados",
|
||||||
"logsUploadFailed": "Falha ao enviar logs",
|
"logsUploadFailed": "Falha ao enviar logs",
|
||||||
"logId": "ID do log"
|
"logId": "ID do log"
|
||||||
|
|||||||
@@ -752,6 +752,8 @@
|
|||||||
"serverLimitBody": "Ошибка сервера (HTTP 500). Лимит пропускной способности/транскодирования, вероятно, отклонил сессию. Попросите владельца изменить настройки.",
|
"serverLimitBody": "Ошибка сервера (HTTP 500). Лимит пропускной способности/транскодирования, вероятно, отклонил сессию. Попросите владельца изменить настройки.",
|
||||||
"mediaUnreadableTitle": "Файл недоступен",
|
"mediaUnreadableTitle": "Файл недоступен",
|
||||||
"mediaUnreadableBody": "Сервер нашёл этот элемент, но не смог прочитать его файл (HTTP 404). Вероятно, файл был перемещён, удалён или его хранилище недоступно. Попросите владельца сервера проверить файл и пересканировать библиотеку.",
|
"mediaUnreadableBody": "Сервер нашёл этот элемент, но не смог прочитать его файл (HTTP 404). Вероятно, файл был перемещён, удалён или его хранилище недоступно. Попросите владельца сервера проверить файл и пересканировать библиотеку.",
|
||||||
|
"serverBusyTitle": "",
|
||||||
|
"serverBusyBody": "",
|
||||||
"logsUploaded": "Логи загружены",
|
"logsUploaded": "Логи загружены",
|
||||||
"logsUploadFailed": "Не удалось загрузить логи",
|
"logsUploadFailed": "Не удалось загрузить логи",
|
||||||
"logId": "ID лога"
|
"logId": "ID лога"
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
/// To regenerate, run: `dart run slang`
|
/// To regenerate, run: `dart run slang`
|
||||||
///
|
///
|
||||||
/// Locales: 22
|
/// Locales: 22
|
||||||
/// Strings: 38874 (1767 per locale)
|
/// Strings: 38876 (1767 per locale)
|
||||||
|
|
||||||
// coverage:ignore-file
|
// coverage:ignore-file
|
||||||
// ignore_for_file: type=lint, unused_import
|
// ignore_for_file: type=lint, unused_import
|
||||||
|
|||||||
@@ -2209,6 +2209,12 @@ class Translations$messages$en {
|
|||||||
/// en: 'The server found this item but could not read its file (HTTP 404). The file was probably moved, deleted, or its storage is offline. Ask the server owner to check the file and rescan the library.'
|
/// en: 'The server found this item but could not read its file (HTTP 404). The file was probably moved, deleted, or its storage is offline. Ask the server owner to check the file and rescan the library.'
|
||||||
String get mediaUnreadableBody => 'The server found this item but could not read its file (HTTP 404). The file was probably moved, deleted, or its storage is offline. Ask the server owner to check the file and rescan the library.';
|
String get mediaUnreadableBody => 'The server found this item but could not read its file (HTTP 404). The file was probably moved, deleted, or its storage is offline. Ask the server owner to check the file and rescan the library.';
|
||||||
|
|
||||||
|
/// en: 'Stream unavailable'
|
||||||
|
String get serverBusyTitle => 'Stream unavailable';
|
||||||
|
|
||||||
|
/// en: 'The server kept refusing to stream this file (HTTP 503). It may be restarting, busy, or the file's storage may be offline. Try again in a moment — if it keeps happening, ask the server owner to check the server and the file's storage.'
|
||||||
|
String get serverBusyBody => 'The server kept refusing to stream this file (HTTP 503). It may be restarting, busy, or the file\'s storage may be offline. Try again in a moment — if it keeps happening, ask the server owner to check the server and the file\'s storage.';
|
||||||
|
|
||||||
/// en: 'Logs uploaded'
|
/// en: 'Logs uploaded'
|
||||||
String get logsUploaded => 'Logs uploaded';
|
String get logsUploaded => 'Logs uploaded';
|
||||||
|
|
||||||
@@ -6818,6 +6824,8 @@ extension on Translations {
|
|||||||
'messages.serverLimitBody' => 'Server error (HTTP 500). A bandwidth/transcoding limit likely rejected this session. Ask the owner to adjust it.',
|
'messages.serverLimitBody' => 'Server error (HTTP 500). A bandwidth/transcoding limit likely rejected this session. Ask the owner to adjust it.',
|
||||||
'messages.mediaUnreadableTitle' => 'File unavailable',
|
'messages.mediaUnreadableTitle' => 'File unavailable',
|
||||||
'messages.mediaUnreadableBody' => 'The server found this item but could not read its file (HTTP 404). The file was probably moved, deleted, or its storage is offline. Ask the server owner to check the file and rescan the library.',
|
'messages.mediaUnreadableBody' => 'The server found this item but could not read its file (HTTP 404). The file was probably moved, deleted, or its storage is offline. Ask the server owner to check the file and rescan the library.',
|
||||||
|
'messages.serverBusyTitle' => 'Stream unavailable',
|
||||||
|
'messages.serverBusyBody' => 'The server kept refusing to stream this file (HTTP 503). It may be restarting, busy, or the file\'s storage may be offline. Try again in a moment — if it keeps happening, ask the server owner to check the server and the file\'s storage.',
|
||||||
'messages.logsUploaded' => 'Logs uploaded',
|
'messages.logsUploaded' => 'Logs uploaded',
|
||||||
'messages.logsUploadFailed' => 'Failed to upload logs',
|
'messages.logsUploadFailed' => 'Failed to upload logs',
|
||||||
'messages.logId' => 'Log ID',
|
'messages.logId' => 'Log ID',
|
||||||
@@ -7145,10 +7153,10 @@ extension on Translations {
|
|||||||
'explore.badge.partiallyAvailable' => 'Partly available',
|
'explore.badge.partiallyAvailable' => 'Partly available',
|
||||||
'explore.badge.availableIn4k' => '4K available',
|
'explore.badge.availableIn4k' => '4K available',
|
||||||
'explore.badge.requested' => 'Requested',
|
'explore.badge.requested' => 'Requested',
|
||||||
'explore.badge.pendingApproval' => 'Pending approval',
|
|
||||||
'explore.badge.processing' => 'Processing',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'explore.badge.pendingApproval' => 'Pending approval',
|
||||||
|
'explore.badge.processing' => 'Processing',
|
||||||
'explore.badge.declined' => 'Declined',
|
'explore.badge.declined' => 'Declined',
|
||||||
'explore.badge.requestFailed' => 'Request failed',
|
'explore.badge.requestFailed' => 'Request failed',
|
||||||
'explore.badge.requested4k' => '4K requested',
|
'explore.badge.requested4k' => '4K requested',
|
||||||
@@ -7659,10 +7667,10 @@ extension on Translations {
|
|||||||
'performanceOverlay.displayFps' => 'Display FPS',
|
'performanceOverlay.displayFps' => 'Display FPS',
|
||||||
'performanceOverlay.avSync' => 'A/V Sync',
|
'performanceOverlay.avSync' => 'A/V Sync',
|
||||||
'performanceOverlay.dropped' => 'Dropped',
|
'performanceOverlay.dropped' => 'Dropped',
|
||||||
'performanceOverlay.dvRpus' => 'DV RPUs',
|
|
||||||
'performanceOverlay.dvRpuAverage' => 'DV RPU Avg',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'performanceOverlay.dvRpus' => 'DV RPUs',
|
||||||
|
'performanceOverlay.dvRpuAverage' => 'DV RPU Avg',
|
||||||
'performanceOverlay.dvSampleAverage' => 'DV Sample Avg',
|
'performanceOverlay.dvSampleAverage' => 'DV Sample Avg',
|
||||||
'performanceOverlay.maxLuma' => 'Max Luma',
|
'performanceOverlay.maxLuma' => 'Max Luma',
|
||||||
'performanceOverlay.minLuma' => 'Min Luma',
|
'performanceOverlay.minLuma' => 'Min Luma',
|
||||||
|
|||||||
@@ -744,6 +744,8 @@
|
|||||||
"serverLimitBody": "Serverfel (HTTP 500). En bandbredds-/transkodningsgräns avvisade troligen sessionen. Be ägaren justera den.",
|
"serverLimitBody": "Serverfel (HTTP 500). En bandbredds-/transkodningsgräns avvisade troligen sessionen. Be ägaren justera den.",
|
||||||
"mediaUnreadableTitle": "Filen är otillgänglig",
|
"mediaUnreadableTitle": "Filen är otillgänglig",
|
||||||
"mediaUnreadableBody": "Servern hittade objektet men kunde inte läsa dess fil (HTTP 404). Filen har troligen flyttats, tagits bort eller så är dess lagring offline. Be serverägaren kontrollera filen och skanna om biblioteket.",
|
"mediaUnreadableBody": "Servern hittade objektet men kunde inte läsa dess fil (HTTP 404). Filen har troligen flyttats, tagits bort eller så är dess lagring offline. Be serverägaren kontrollera filen och skanna om biblioteket.",
|
||||||
|
"serverBusyTitle": "",
|
||||||
|
"serverBusyBody": "",
|
||||||
"logsUploaded": "Loggarna har laddats upp",
|
"logsUploaded": "Loggarna har laddats upp",
|
||||||
"logsUploadFailed": "Det gick inte att ladda upp loggarna",
|
"logsUploadFailed": "Det gick inte att ladda upp loggarna",
|
||||||
"logId": "Logg-ID"
|
"logId": "Logg-ID"
|
||||||
|
|||||||
@@ -744,6 +744,8 @@
|
|||||||
"serverLimitBody": "Sunucu hatası (HTTP 500). Muhtemelen bir bant genişliği/kod dönüştürme sınırı bu oturumu reddetti. Sunucu sahibinden bunu ayarlamasını isteyin.",
|
"serverLimitBody": "Sunucu hatası (HTTP 500). Muhtemelen bir bant genişliği/kod dönüştürme sınırı bu oturumu reddetti. Sunucu sahibinden bunu ayarlamasını isteyin.",
|
||||||
"mediaUnreadableTitle": "Dosya kullanılamıyor",
|
"mediaUnreadableTitle": "Dosya kullanılamıyor",
|
||||||
"mediaUnreadableBody": "Sunucu bu ögeyi buldu ancak dosyasını okuyamadı (HTTP 404). Dosya büyük olasılıkla taşındı, silindi veya depolama alanı çevrimdışı. Sunucu sahibinden dosyayı kontrol edip kitaplığı yeniden taramasını isteyin.",
|
"mediaUnreadableBody": "Sunucu bu ögeyi buldu ancak dosyasını okuyamadı (HTTP 404). Dosya büyük olasılıkla taşındı, silindi veya depolama alanı çevrimdışı. Sunucu sahibinden dosyayı kontrol edip kitaplığı yeniden taramasını isteyin.",
|
||||||
|
"serverBusyTitle": "",
|
||||||
|
"serverBusyBody": "",
|
||||||
"logsUploaded": "Günlükler yüklendi",
|
"logsUploaded": "Günlükler yüklendi",
|
||||||
"logsUploadFailed": "Günlükler yüklenemedi",
|
"logsUploadFailed": "Günlükler yüklenemedi",
|
||||||
"logId": "Günlük Kimliği (ID)"
|
"logId": "Günlük Kimliği (ID)"
|
||||||
|
|||||||
@@ -744,6 +744,8 @@
|
|||||||
"serverLimitBody": "Server xatoligi (HTTP 500). Cheklov ushbu seansni rad etdi.",
|
"serverLimitBody": "Server xatoligi (HTTP 500). Cheklov ushbu seansni rad etdi.",
|
||||||
"mediaUnreadableTitle": "Fayl mavjud emas",
|
"mediaUnreadableTitle": "Fayl mavjud emas",
|
||||||
"mediaUnreadableBody": "Server bu elementni topdi, lekin uning faylini oʻqiy olmadi (HTTP 404). Fayl koʻchirilgan, oʻchirilgan yoki uning saqlash joyi oflayn boʻlishi mumkin. Server egasidan faylni tekshirishni va kutubxonani qayta skanerlashni soʻrang.",
|
"mediaUnreadableBody": "Server bu elementni topdi, lekin uning faylini oʻqiy olmadi (HTTP 404). Fayl koʻchirilgan, oʻchirilgan yoki uning saqlash joyi oflayn boʻlishi mumkin. Server egasidan faylni tekshirishni va kutubxonani qayta skanerlashni soʻrang.",
|
||||||
|
"serverBusyTitle": "",
|
||||||
|
"serverBusyBody": "",
|
||||||
"logsUploaded": "Jurnallar yuklandi",
|
"logsUploaded": "Jurnallar yuklandi",
|
||||||
"logsUploadFailed": "Jurnallarni yuklab boʻlmadi",
|
"logsUploadFailed": "Jurnallarni yuklab boʻlmadi",
|
||||||
"logId": "Jurnal ID-si"
|
"logId": "Jurnal ID-si"
|
||||||
|
|||||||
@@ -740,6 +740,8 @@
|
|||||||
"serverLimitBody": "伺服器錯誤(HTTP 500)。伺服器的頻寬或轉碼限制可能拒絕此播放要求。請聯絡伺服器擁有者調整設定。",
|
"serverLimitBody": "伺服器錯誤(HTTP 500)。伺服器的頻寬或轉碼限制可能拒絕此播放要求。請聯絡伺服器擁有者調整設定。",
|
||||||
"mediaUnreadableTitle": "檔案無法使用",
|
"mediaUnreadableTitle": "檔案無法使用",
|
||||||
"mediaUnreadableBody": "伺服器找到了此項目,但無法讀取其檔案(HTTP 404)。檔案可能已移動、刪除,或其儲存空間離線。請聯絡伺服器擁有者檢查檔案並重新掃描媒體庫。",
|
"mediaUnreadableBody": "伺服器找到了此項目,但無法讀取其檔案(HTTP 404)。檔案可能已移動、刪除,或其儲存空間離線。請聯絡伺服器擁有者檢查檔案並重新掃描媒體庫。",
|
||||||
|
"serverBusyTitle": "",
|
||||||
|
"serverBusyBody": "",
|
||||||
"logsUploaded": "日誌已上傳",
|
"logsUploaded": "日誌已上傳",
|
||||||
"logsUploadFailed": "上傳日誌失敗",
|
"logsUploadFailed": "上傳日誌失敗",
|
||||||
"logId": "日誌 ID"
|
"logId": "日誌 ID"
|
||||||
|
|||||||
@@ -740,6 +740,8 @@
|
|||||||
"serverLimitBody": "服务器错误(HTTP 500)。此次会话可能因带宽或转码限制而被拒绝。请联系服务器所有者调整限制。",
|
"serverLimitBody": "服务器错误(HTTP 500)。此次会话可能因带宽或转码限制而被拒绝。请联系服务器所有者调整限制。",
|
||||||
"mediaUnreadableTitle": "文件不可用",
|
"mediaUnreadableTitle": "文件不可用",
|
||||||
"mediaUnreadableBody": "服务器找到了此项目,但无法读取其文件(HTTP 404)。文件可能已被移动、删除,或其存储已离线。请联系服务器管理员检查文件并重新扫描媒体库。",
|
"mediaUnreadableBody": "服务器找到了此项目,但无法读取其文件(HTTP 404)。文件可能已被移动、删除,或其存储已离线。请联系服务器管理员检查文件并重新扫描媒体库。",
|
||||||
|
"serverBusyTitle": "",
|
||||||
|
"serverBusyBody": "",
|
||||||
"logsUploaded": "日志已上传",
|
"logsUploaded": "日志已上传",
|
||||||
"logsUploadFailed": "上传日志失败",
|
"logsUploadFailed": "上传日志失败",
|
||||||
"logId": "日志 ID"
|
"logId": "日志 ID"
|
||||||
|
|||||||
+10
-1
@@ -12,7 +12,9 @@ final RegExp _httpStatusPattern = RegExp(r'\b(?:HTTP error |Response code: )(\d{
|
|||||||
|
|
||||||
/// Server statuses that end playback outright: nothing client-side recovers a
|
/// Server statuses that end playback outright: nothing client-side recovers a
|
||||||
/// transcoding-limit rejection or a file the server cannot read. Everything
|
/// transcoding-limit rejection or a file the server cannot read. Everything
|
||||||
/// else (notably the 503 the reconnect path retries) is transient.
|
/// else is transient — notably 503, which the reconnect path retries
|
||||||
|
/// mid-stream (#1520) and the player screen's open-phase watchdog bounds at
|
||||||
|
/// open time instead of latching here (#1830).
|
||||||
const Set<int> fatalPlaybackHttpStatuses = {404, 500};
|
const Set<int> fatalPlaybackHttpStatuses = {404, 500};
|
||||||
|
|
||||||
/// [cause] is an optional machine-readable tag (e.g. `server-http-500`),
|
/// [cause] is an optional machine-readable tag (e.g. `server-http-500`),
|
||||||
@@ -32,6 +34,13 @@ sealed class PlayerError with _$PlayerError {
|
|||||||
/// on unavailable storage), so no retry or backend switch can recover it.
|
/// on unavailable storage), so no retry or backend switch can recover it.
|
||||||
static const String serverHttp404 = 'server-http-404';
|
static const String serverHttp404 = 'server-http-404';
|
||||||
|
|
||||||
|
/// Cause tag for a persistent HTTP 503 on the media stream during the open
|
||||||
|
/// phase. Mid-stream 503 is transient — ffmpeg reconnects through server
|
||||||
|
/// restarts (#1520) — so this tag is never derived from a raw status: only
|
||||||
|
/// the player screen's open-phase watchdog synthesizes it, after a refused
|
||||||
|
/// open has produced no frame for the whole patience window (#1830).
|
||||||
|
static const String serverHttp503 = 'server-http-503';
|
||||||
|
|
||||||
/// HTTP status [logText] reports, or null when it names none.
|
/// HTTP status [logText] reports, or null when it names none.
|
||||||
///
|
///
|
||||||
/// A [PlayerError] carries no status field: mpv only ever tells us the
|
/// A [PlayerError] carries no status field: mpv only ever tells us the
|
||||||
|
|||||||
@@ -1344,8 +1344,10 @@ abstract class PlayerBase with PlayerStreamControllersMixin implements Player {
|
|||||||
/// stream with [status]. Used by the in-player debug buttons to preview the
|
/// stream with [status]. Used by the in-player debug buttons to preview the
|
||||||
/// end-to-end detection path without needing a real misbehaving server: 500
|
/// end-to-end detection path without needing a real misbehaving server: 500
|
||||||
/// is a shared-user bandwidth/transcoding limit, 404 a file the server can no
|
/// is a shared-user bandwidth/transcoding limit, 404 a file the server can no
|
||||||
/// longer read. The warn-level log mirrors ffmpeg's real wording, which is
|
/// longer read, 503 a server that keeps refusing the stream (the error event
|
||||||
/// what [PlayerError.httpStatusFromLog] parses.
|
/// stands in for the open-phase watchdog, which cannot arm once playback has
|
||||||
|
/// a frame). The warn-level log mirrors ffmpeg's real wording, which is what
|
||||||
|
/// [PlayerError.httpStatusFromLog] parses.
|
||||||
void debugSimulateServerHttpError(int status) {
|
void debugSimulateServerHttpError(int status) {
|
||||||
if (_disposed) return;
|
if (_disposed) return;
|
||||||
logController.add(
|
logController.add(
|
||||||
@@ -1354,6 +1356,7 @@ abstract class PlayerBase with PlayerStreamControllersMixin implements Player {
|
|||||||
final cause = switch (status) {
|
final cause = switch (status) {
|
||||||
500 => PlayerError.serverHttp500,
|
500 => PlayerError.serverHttp500,
|
||||||
404 => PlayerError.serverHttp404,
|
404 => PlayerError.serverHttp404,
|
||||||
|
503 => PlayerError.serverHttp503,
|
||||||
_ => null,
|
_ => null,
|
||||||
};
|
};
|
||||||
errorController.add(PlayerError('HTTP $status', cause: cause));
|
errorController.add(PlayerError('HTTP $status', cause: cause));
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
/// How long an open gets to produce a first frame after the server starts
|
||||||
|
/// answering the stream with HTTP 503 before the failure is surfaced.
|
||||||
|
///
|
||||||
|
/// ffmpeg's reconnect loop retries 503 with exponential backoff (0s, 1s, 3s,
|
||||||
|
/// 7s, ...), so this window covers several attempts — enough for the short
|
||||||
|
/// server restart #1520 rides out mid-stream, while a server still refusing
|
||||||
|
/// after all of them is treated as down for this item (#1830).
|
||||||
|
const Duration openHttp503Patience = Duration(seconds: 20);
|
||||||
|
|
||||||
|
/// Bounds a media open the server answers with HTTP 503.
|
||||||
|
///
|
||||||
|
/// Mid-stream, ffmpeg deliberately reconnects on 503 forever (#1520) and mpv
|
||||||
|
/// just reports buffering. At open time — before any frame has rendered —
|
||||||
|
/// that same loop is a silent black screen with no error event (#1830):
|
||||||
|
/// nothing client-side ever fails. This watchdog arms on the first 503
|
||||||
|
/// observed while the open is still frameless and calls [onPersistent] if
|
||||||
|
/// [disarm] has not been called within [patience].
|
||||||
|
///
|
||||||
|
/// The caller decides which opens are eligible (live TV stays out — its
|
||||||
|
/// fallback ladder owns retries there) and owns the synthesized error. Only
|
||||||
|
/// the timer bookkeeping lives here so it is testable with `fakeAsync`.
|
||||||
|
class OpenHttp503Watchdog {
|
||||||
|
OpenHttp503Watchdog({required this.onPersistent, this.patience = openHttp503Patience});
|
||||||
|
|
||||||
|
final Duration patience;
|
||||||
|
final void Function() onPersistent;
|
||||||
|
|
||||||
|
Timer? _timer;
|
||||||
|
|
||||||
|
bool get isArmed => _timer != null;
|
||||||
|
|
||||||
|
/// A 503 was observed while the current open has no frame yet. Arms once;
|
||||||
|
/// later 503s from the same reconnect loop keep the original deadline.
|
||||||
|
void onOpenPhase503() {
|
||||||
|
_timer ??= Timer(patience, _fire);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The open produced output, was replaced, or the screen is going away:
|
||||||
|
/// any further 503s belong to the reconnect path (or the next open).
|
||||||
|
void disarm() {
|
||||||
|
_timer?.cancel();
|
||||||
|
_timer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _fire() {
|
||||||
|
_timer = null;
|
||||||
|
onPersistent();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -771,6 +771,8 @@ extension _VideoPlayerEpisodeNavigationMethods on VideoPlayerScreenState {
|
|||||||
shouldContinue: isCurrentReload,
|
shouldContinue: isCurrentReload,
|
||||||
onOpening: () {
|
onOpening: () {
|
||||||
_hasRenderedFirstFrame = false;
|
_hasRenderedFirstFrame = false;
|
||||||
|
// 503s observed from here on belong to the replacement open.
|
||||||
|
_http503Watchdog.disarm();
|
||||||
},
|
},
|
||||||
onOpened: () {
|
onOpened: () {
|
||||||
// The player now owns the new file — publish the session at the
|
// The player now owns the new file — publish the session at the
|
||||||
|
|||||||
@@ -37,6 +37,10 @@ extension _VideoPlayerErrorMethods on VideoPlayerScreenState {
|
|||||||
_hasFatalPlaybackError = true;
|
_hasFatalPlaybackError = true;
|
||||||
_progressTracker?.stopTracking();
|
_progressTracker?.stopTracking();
|
||||||
unawaited(_showMediaUnreadableDialog());
|
unawaited(_showMediaUnreadableDialog());
|
||||||
|
case PlaybackFailureAction.serverBusyDialog:
|
||||||
|
_hasFatalPlaybackError = true;
|
||||||
|
_progressTracker?.stopTracking();
|
||||||
|
unawaited(_showServerBusyDialog());
|
||||||
// The bounded retry operation owns errors raised while applying/opening
|
// The bounded retry operation owns errors raised while applying/opening
|
||||||
// its replacement stream. Do not let the same error close the route.
|
// its replacement stream. Do not let the same error close the route.
|
||||||
case PlaybackFailureAction.ignore:
|
case PlaybackFailureAction.ignore:
|
||||||
@@ -59,12 +63,33 @@ extension _VideoPlayerErrorMethods on VideoPlayerScreenState {
|
|||||||
void _onPlayerLog(PlayerLog log) {
|
void _onPlayerLog(PlayerLog log) {
|
||||||
final status = PlayerError.httpStatusFromLog(log.text);
|
final status = PlayerError.httpStatusFromLog(log.text);
|
||||||
if (status != null && fatalPlaybackHttpStatuses.contains(status)) _fatalHttpStatuses.add(status);
|
if (status != null && fatalPlaybackHttpStatuses.contains(status)) _fatalHttpStatuses.add(status);
|
||||||
|
// An open the server answers with 503 never fails on its own: ffmpeg's
|
||||||
|
// reconnect loop retries 503 forever and mpv just reports buffering
|
||||||
|
// (#1830). Bound it. Live TV stays out — its ladder owns retries there.
|
||||||
|
// A sidecar subtitle fetch shares this log stream and could arm the
|
||||||
|
// watchdog too, but a first frame disarms it, so that only matters when
|
||||||
|
// the primary media is itself stuck.
|
||||||
|
if (status == 503 && !widget.isLive && !_hasRenderedFirstFrame && !_hasFatalPlaybackError) {
|
||||||
|
_http503Watchdog.onOpenPhase503();
|
||||||
|
}
|
||||||
if (log.level == PlayerLogLevel.error || log.level == PlayerLogLevel.fatal) {
|
if (log.level == PlayerLogLevel.error || log.level == PlayerLogLevel.fatal) {
|
||||||
appLogger.e('[Player LOG ERROR] [${log.prefix}] ${log.text}');
|
appLogger.e('[Player LOG ERROR] [${log.prefix}] ${log.text}');
|
||||||
_lastLogError = _redactPlayerError(log.text.trim());
|
_lastLogError = _redactPlayerError(log.text.trim());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The open-phase 503 watchdog's deadline passed with no first frame: the
|
||||||
|
/// server is still refusing the stream. Synthesize the error the reconnect
|
||||||
|
/// loop will never raise on its own so the normal failure policy runs.
|
||||||
|
void _onOpenHttp503Persistent() {
|
||||||
|
if (!mounted || _isExiting.value || _hasRenderedFirstFrame || _hasFatalPlaybackError) return;
|
||||||
|
appLogger.w(
|
||||||
|
'Server kept answering the stream with HTTP 503 for '
|
||||||
|
'${openHttp503Patience.inSeconds}s without a first frame — giving up on this open',
|
||||||
|
);
|
||||||
|
_onPlayerError(const PlayerError('HTTP 503', cause: PlayerError.serverHttp503));
|
||||||
|
}
|
||||||
|
|
||||||
String _redactPlayerError(String message) => LogRedactionManager.redact(message);
|
String _redactPlayerError(String message) => LogRedactionManager.redact(message);
|
||||||
|
|
||||||
Future<void> _showServerLimitDialog() async {
|
Future<void> _showServerLimitDialog() async {
|
||||||
@@ -79,6 +104,16 @@ extension _VideoPlayerErrorMethods on VideoPlayerScreenState {
|
|||||||
if (mounted) unawaited(_handleBackButton());
|
if (mounted) unawaited(_handleBackButton());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _showServerBusyDialog() async {
|
||||||
|
if (!mounted) return;
|
||||||
|
// The reconnect loop is still running behind the modal; pause so a server
|
||||||
|
// that recovers mid-dialog cannot start playing under it. Best-effort —
|
||||||
|
// the route is left on dialog close either way.
|
||||||
|
unawaited(player?.pause().catchError((_) {}));
|
||||||
|
await showServerBusyDialog(context);
|
||||||
|
if (mounted) unawaited(_handleBackButton());
|
||||||
|
}
|
||||||
|
|
||||||
/// Handle notification when native player switched from ExoPlayer to MPV
|
/// Handle notification when native player switched from ExoPlayer to MPV
|
||||||
Future<void> _onBackendSwitched() async {
|
Future<void> _onBackendSwitched() async {
|
||||||
_playerBackendLabel = 'mpv';
|
_playerBackendLabel = 'mpv';
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ extension _VideoPlayerPlaybackServiceMethods on VideoPlayerScreenState {
|
|||||||
|
|
||||||
_hasRenderedFirstFrame = true;
|
_hasRenderedFirstFrame = true;
|
||||||
_hasFirstFrame.value = true;
|
_hasFirstFrame.value = true;
|
||||||
|
_http503Watchdog.disarm();
|
||||||
unawaited(Sentry.addBreadcrumb(Breadcrumb(message: 'First frame ready', category: 'player')));
|
unawaited(Sentry.addBreadcrumb(Breadcrumb(message: 'First frame ready', category: 'player')));
|
||||||
final progressTracker = _progressTracker;
|
final progressTracker = _progressTracker;
|
||||||
if (progressTracker != null && currentPlayer.state.isActive) {
|
if (progressTracker != null && currentPlayer.state.isActive) {
|
||||||
@@ -323,6 +324,7 @@ extension _VideoPlayerPlaybackServiceMethods on VideoPlayerScreenState {
|
|||||||
_isBuffering.value = false;
|
_isBuffering.value = false;
|
||||||
_hasFirstFrame.value = false;
|
_hasFirstFrame.value = false;
|
||||||
_hasRenderedFirstFrame = false;
|
_hasRenderedFirstFrame = false;
|
||||||
|
_http503Watchdog.disarm();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ extension _VideoPlayerPlaybackStartMethods on VideoPlayerScreenState {
|
|||||||
final attempt = _beginPlaybackAttempt(currentPlayer);
|
final attempt = _beginPlaybackAttempt(currentPlayer);
|
||||||
_hasRenderedFirstFrame = false;
|
_hasRenderedFirstFrame = false;
|
||||||
_hasFatalPlaybackError = false;
|
_hasFatalPlaybackError = false;
|
||||||
|
// 503s observed from here on belong to this attempt's open.
|
||||||
|
_http503Watchdog.disarm();
|
||||||
|
|
||||||
// Live TV mode: bypass standard playback initialization
|
// Live TV mode: bypass standard playback initialization
|
||||||
if (widget.isLive) {
|
if (widget.isLive) {
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ enum PlaybackFailureAction {
|
|||||||
/// Server could not read the file behind the item (HTTP 404).
|
/// Server could not read the file behind the item (HTTP 404).
|
||||||
mediaUnreadableDialog,
|
mediaUnreadableDialog,
|
||||||
|
|
||||||
|
/// Server kept refusing the stream with HTTP 503 for the whole open phase.
|
||||||
|
serverBusyDialog,
|
||||||
|
|
||||||
/// A live retry already owns the player and its error UI.
|
/// A live retry already owns the player and its error UI.
|
||||||
ignore,
|
ignore,
|
||||||
|
|
||||||
@@ -34,7 +37,10 @@ enum PlaybackFailureAction {
|
|||||||
/// playlist, or a transcode session restarting under us, answers 404 mid-stream,
|
/// playlist, or a transcode session restarting under us, answers 404 mid-stream,
|
||||||
/// and the bounded ladder exists to ride that out. Only on-demand playback
|
/// and the bounded ladder exists to ride that out. Only on-demand playback
|
||||||
/// treats 404 as terminal, where it does mean the file is unreadable. 500 stays
|
/// treats 404 as terminal, where it does mean the file is unreadable. 500 stays
|
||||||
/// terminal for both — a limit rejection is not something a retry clears.
|
/// terminal for both — a limit rejection is not something a retry clears. 503
|
||||||
|
/// arrives only as the open-phase watchdog's cause tag (it never latches into
|
||||||
|
/// [fatalHttpStatuses]); by then the reconnect loop has had its chances, so
|
||||||
|
/// on-demand playback surfaces it while live TV keeps its ladder.
|
||||||
PlaybackFailureAction resolvePlaybackFailureAction({
|
PlaybackFailureAction resolvePlaybackFailureAction({
|
||||||
required String? cause,
|
required String? cause,
|
||||||
required Set<int> fatalHttpStatuses,
|
required Set<int> fatalHttpStatuses,
|
||||||
@@ -51,6 +57,10 @@ PlaybackFailureAction resolvePlaybackFailureAction({
|
|||||||
return PlaybackFailureAction.mediaUnreadableDialog;
|
return PlaybackFailureAction.mediaUnreadableDialog;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isLive && cause == PlayerError.serverHttp503) {
|
||||||
|
return PlaybackFailureAction.serverBusyDialog;
|
||||||
|
}
|
||||||
|
|
||||||
if (isLive) {
|
if (isLive) {
|
||||||
if (liveRetrying) return PlaybackFailureAction.ignore;
|
if (liveRetrying) return PlaybackFailureAction.ignore;
|
||||||
if (liveFallbackLevel < maxLiveFallbackLevel) return PlaybackFailureAction.liveRetry;
|
if (liveFallbackLevel < maxLiveFallbackLevel) return PlaybackFailureAction.liveRetry;
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ import 'video_player/live_stream_retry.dart';
|
|||||||
import 'video_player/live_timeline_report.dart';
|
import 'video_player/live_timeline_report.dart';
|
||||||
import 'video_player/wakelock_controller.dart';
|
import 'video_player/wakelock_controller.dart';
|
||||||
import 'video_player/playback_failure_action.dart';
|
import 'video_player/playback_failure_action.dart';
|
||||||
|
import 'video_player/open_http_503_watchdog.dart';
|
||||||
import 'video_player/live_tv_session_args.dart';
|
import 'video_player/live_tv_session_args.dart';
|
||||||
import 'video_player/live_tv_session_state.dart';
|
import 'video_player/live_tv_session_state.dart';
|
||||||
import 'video_player/tv_background_suspend_policy.dart';
|
import 'video_player/tv_background_suspend_policy.dart';
|
||||||
@@ -1765,6 +1766,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
|
|||||||
|
|
||||||
_autoPlayTimer?.cancel();
|
_autoPlayTimer?.cancel();
|
||||||
_tvBackgroundPlayerSuspendTimer?.cancel();
|
_tvBackgroundPlayerSuspendTimer?.cancel();
|
||||||
|
_http503Watchdog.disarm();
|
||||||
|
|
||||||
_stillWatchingTimer?.cancel();
|
_stillWatchingTimer?.cancel();
|
||||||
|
|
||||||
@@ -1977,6 +1979,11 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
|
|||||||
/// transient status must never mask the fatal one that follows.
|
/// transient status must never mask the fatal one that follows.
|
||||||
final Set<int> _fatalHttpStatuses = <int>{};
|
final Set<int> _fatalHttpStatuses = <int>{};
|
||||||
|
|
||||||
|
/// Bounds an open the server keeps answering with HTTP 503 — that loop
|
||||||
|
/// otherwise never raises an error (#1830). Armed from [_onPlayerLog],
|
||||||
|
/// disarmed on first frame, on every new-open reset, and in [dispose].
|
||||||
|
late final OpenHttp503Watchdog _http503Watchdog = OpenHttp503Watchdog(onPersistent: _onOpenHttp503Persistent);
|
||||||
|
|
||||||
// OS Media Controls Integration
|
// OS Media Controls Integration
|
||||||
|
|
||||||
/// Navigate to a specific queue item (called from QueueSheet)
|
/// Navigate to a specific queue item (called from QueueSheet)
|
||||||
|
|||||||
@@ -152,6 +152,29 @@ Future<void> showMediaUnreadableDialog(BuildContext context) async {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Shows the server-side 503 modal: the server kept refusing to serve the
|
||||||
|
/// stream for the whole open-phase watchdog window, so the reconnect loop is
|
||||||
|
/// not going to start this playback (#1830).
|
||||||
|
Future<void> showServerBusyDialog(BuildContext context) async {
|
||||||
|
await showScopedDialog<void>(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
|
builder: (ctx) => AlertDialog(
|
||||||
|
title: Text(t.messages.serverBusyTitle),
|
||||||
|
content: Text(t.messages.serverBusyBody),
|
||||||
|
actions: [
|
||||||
|
DialogActionButton(
|
||||||
|
autofocus: true,
|
||||||
|
onPressed: () => Navigator.of(ctx).pop(),
|
||||||
|
label: t.common.close,
|
||||||
|
isPrimary: true,
|
||||||
|
style: FilledButton.styleFrom(padding: _buttonPadding, shape: _buttonShape),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Shows a delete confirmation dialog.
|
/// Shows a delete confirmation dialog.
|
||||||
/// Convenience wrapper around [showConfirmDialog] with destructive styling.
|
/// Convenience wrapper around [showConfirmDialog] with destructive styling.
|
||||||
Future<bool> showDeleteConfirmation(
|
Future<bool> showDeleteConfirmation(
|
||||||
|
|||||||
@@ -727,7 +727,7 @@ class _VideoSettingsSheetState extends State<VideoSettingsSheet> {
|
|||||||
),
|
),
|
||||||
|
|
||||||
if (kDebugMode)
|
if (kDebugMode)
|
||||||
for (final status in const [500, 404])
|
for (final status in const [500, 404, 503])
|
||||||
FocusableListTile(
|
FocusableListTile(
|
||||||
leading: AppIcon(Symbols.bug_report_rounded, fill: 1, color: tokens(context).textMuted),
|
leading: AppIcon(Symbols.bug_report_rounded, fill: 1, color: tokens(context).textMuted),
|
||||||
title: Text('Simulate HTTP $status from server'),
|
title: Text('Simulate HTTP $status from server'),
|
||||||
|
|||||||
@@ -42,10 +42,11 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
group('fatalPlaybackHttpStatuses', () {
|
group('fatalPlaybackHttpStatuses', () {
|
||||||
|
|
||||||
test('excludes the 503 the reconnect path deliberately retries', () {
|
test('excludes the 503 the reconnect path deliberately retries', () {
|
||||||
// stream-lavf-o sets reconnect_on_http_error=503, so a 503 is expected
|
// stream-lavf-o sets reconnect_on_http_error=503, so a 503 is expected
|
||||||
// mid-playback and must not latch as fatal.
|
// mid-playback and must not latch as fatal. At open time the player
|
||||||
|
// screen's OpenHttp503Watchdog bounds the loop instead — only its
|
||||||
|
// synthesized cause tag, never a raw latched status, ends playback.
|
||||||
expect(fatalPlaybackHttpStatuses.contains(503), isFalse);
|
expect(fatalPlaybackHttpStatuses.contains(503), isFalse);
|
||||||
expect(PlayerError.httpStatusFromLog('http: HTTP error 503 Service Unavailable'), 503);
|
expect(PlayerError.httpStatusFromLog('http: HTTP error 503 Service Unavailable'), 503);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import 'package:fake_async/fake_async.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:plezy/screens/video_player/open_http_503_watchdog.dart';
|
||||||
|
|
||||||
|
// An open the server answers with 503 never fails on its own: ffmpeg's
|
||||||
|
// reconnect loop retries 503 forever (#1520) and mpv just reports buffering,
|
||||||
|
// so a server that keeps refusing leaves a silent black screen (#1830). The
|
||||||
|
// watchdog is the only thing that ends that open; these tests pin its arming,
|
||||||
|
// deadline, and disarm contract.
|
||||||
|
void main() {
|
||||||
|
test('fires once patience elapses after the first open-phase 503', () {
|
||||||
|
fakeAsync((async) {
|
||||||
|
var fired = 0;
|
||||||
|
final watchdog = OpenHttp503Watchdog(onPersistent: () => fired++);
|
||||||
|
|
||||||
|
watchdog.onOpenPhase503();
|
||||||
|
expect(watchdog.isArmed, isTrue);
|
||||||
|
|
||||||
|
async.elapse(openHttp503Patience - const Duration(milliseconds: 1));
|
||||||
|
expect(fired, 0);
|
||||||
|
|
||||||
|
async.elapse(const Duration(milliseconds: 1));
|
||||||
|
expect(fired, 1);
|
||||||
|
expect(watchdog.isArmed, isFalse);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('repeated 503s from the reconnect loop keep the original deadline', () {
|
||||||
|
fakeAsync((async) {
|
||||||
|
var fired = 0;
|
||||||
|
final watchdog = OpenHttp503Watchdog(onPersistent: () => fired++);
|
||||||
|
|
||||||
|
watchdog.onOpenPhase503();
|
||||||
|
async.elapse(openHttp503Patience - const Duration(seconds: 1));
|
||||||
|
// ffmpeg retries with backoff; each retry logs another 503. None of
|
||||||
|
// them may push the deadline out, or a permanently refusing server
|
||||||
|
// keeps the watchdog pending forever — the exact bug it exists to end.
|
||||||
|
watchdog.onOpenPhase503();
|
||||||
|
watchdog.onOpenPhase503();
|
||||||
|
|
||||||
|
async.elapse(const Duration(seconds: 1));
|
||||||
|
expect(fired, 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('disarming before the deadline prevents the failure', () {
|
||||||
|
fakeAsync((async) {
|
||||||
|
var fired = 0;
|
||||||
|
final watchdog = OpenHttp503Watchdog(onPersistent: () => fired++);
|
||||||
|
|
||||||
|
watchdog.onOpenPhase503();
|
||||||
|
async.elapse(openHttp503Patience - const Duration(seconds: 1));
|
||||||
|
// First frame rendered: the open succeeded, later 503s are the
|
||||||
|
// reconnect path's business.
|
||||||
|
watchdog.disarm();
|
||||||
|
expect(watchdog.isArmed, isFalse);
|
||||||
|
|
||||||
|
async.elapse(openHttp503Patience * 2);
|
||||||
|
expect(fired, 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a 503 after disarm re-arms with a fresh deadline', () {
|
||||||
|
fakeAsync((async) {
|
||||||
|
var fired = 0;
|
||||||
|
final watchdog = OpenHttp503Watchdog(onPersistent: () => fired++);
|
||||||
|
|
||||||
|
watchdog.onOpenPhase503();
|
||||||
|
async.elapse(openHttp503Patience - const Duration(seconds: 1));
|
||||||
|
watchdog.disarm();
|
||||||
|
|
||||||
|
// A new open starts 503ing: the old open's elapsed time must not count
|
||||||
|
// against it.
|
||||||
|
watchdog.onOpenPhase503();
|
||||||
|
async.elapse(openHttp503Patience - const Duration(seconds: 1));
|
||||||
|
expect(fired, 0);
|
||||||
|
|
||||||
|
async.elapse(const Duration(seconds: 1));
|
||||||
|
expect(fired, 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('never fires without an observed 503', () {
|
||||||
|
fakeAsync((async) {
|
||||||
|
var fired = 0;
|
||||||
|
OpenHttp503Watchdog(onPersistent: () => fired++);
|
||||||
|
|
||||||
|
async.elapse(openHttp503Patience * 3);
|
||||||
|
expect(fired, 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -59,6 +59,26 @@ void main() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('HTTP 503', () {
|
||||||
|
test('the open-phase watchdog tag is terminal for on-demand playback', () {
|
||||||
|
// The tag only exists once the watchdog has already waited out the
|
||||||
|
// reconnect loop's chances (#1830) — no further retry recovers it.
|
||||||
|
expect(resolve(cause: PlayerError.serverHttp503), PlaybackFailureAction.serverBusyDialog);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('live TV keeps its fallback ladder', () {
|
||||||
|
// The watchdog never arms for live opens, but a defensively passed tag
|
||||||
|
// must still ride the ladder rather than kill a recoverable stream.
|
||||||
|
expect(resolve(cause: PlayerError.serverHttp503, isLive: true), PlaybackFailureAction.liveRetry);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a latched fatal status outranks the watchdog tag', () {
|
||||||
|
// A 500/404 seen on the same open is the more specific diagnosis.
|
||||||
|
expect(resolve(cause: PlayerError.serverHttp503, statuses: {500}), PlaybackFailureAction.serverLimitDialog);
|
||||||
|
expect(resolve(cause: PlayerError.serverHttp503, statuses: {404}), PlaybackFailureAction.mediaUnreadableDialog);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
group('live fallback ladder', () {
|
group('live fallback ladder', () {
|
||||||
test('climbs every rung below the bound', () {
|
test('climbs every rung below the bound', () {
|
||||||
for (var level = 0; level < maxLiveFallbackLevel; level++) {
|
for (var level = 0; level < maxLiveFallbackLevel; level++) {
|
||||||
|
|||||||
Reference in New Issue
Block a user