5 Commits
Author SHA1 Message Date
AZiZ b981880d09 Update version badge to 0.4.5 2026-10-18 07:15:32 +01:00
AZiZ cf91edcf1e Update README with VP9 and BD-J menu status 2026-10-18 06:39:57 +01:00
AZiZ 36d8450e37 Add files via upload 2026-10-18 06:38:38 +01:00
AZiZ f109ef6410 hardware encoding fixes 2026-10-18 06:36:28 +01:00
AZiZ 71306c9256 v0.4.5 update 2026-10-18 06:35:33 +01:00
42 changed files with 2924 additions and 186 deletions

No files matched your search

+3 -3
View File
@@ -10,7 +10,7 @@
</p>
<p align="center">
<img alt="Version" src="https://img.shields.io/badge/version-0.4-f37a2b">
<img alt="Version" src="https://img.shields.io/badge/version-0.4.5-f37a2b">
<img alt="Platform" src="https://img.shields.io/badge/platform-PS5-1f1f1f">
<img alt="VLC" src="https://img.shields.io/badge/libvlc-3.0.24-ff8800">
<img alt="Renderer" src="https://img.shields.io/badge/renderer-Vulkan%20(RADV)-c0561b">
@@ -107,7 +107,7 @@ Optional full access. With etaHEN, OnionHEN or the Helper, VLC can lift its sand
| Hardware decoding (H.264 / HEVC) | ✅ |
| HDR → SDR tone mapping | ✅ |
| HDR10 output to the TV | ✅ |
| Hardware decoding (VP9 up to 4K) | Not yet |
| Hardware decoding (VP9 up to 4K) | ✅ |
| Upscaling up to 4K (FSR 1, Lanczos, Anime4K etc) | Not yet |
### 🔊 Audio
@@ -192,7 +192,7 @@ Optional full access. With etaHEN, OnionHEN or the Helper, VLC can lift its sand
|---|---|
| DVD folders and ISOs with menus | ✅ |
| Blu-ray folders and ISOs (unprotected) | ✅ |
| Blu-ray Java (BD-J) menus | ❌ needs Java, which the PS5 hasn't got: those discs play without menus |
| Blu-ray Java (BD-J) menus | ❌ Java Blu-ray menus not supported |
| ZIP, RAR and 7z archives open like folders | ✅ |
## Building
+18
View File
@@ -551,3 +551,21 @@ Sign in to the share = تسجيل الدخول إلى المشاركة
A Windows PC takes its own account; a NAS, the user you made on it. = كمبيوتر ويندوز يقبل حسابه نفسه، وجهاز NAS يقبل المستخدم الذي أنشأته عليه.
Track %d = المسار %d
Track = مسار
Disc menu = قائمة القرص
Pop-up menu = القائمة المنبثقة
Blu-ray region = منطقة Blu-ray
Region %s = المنطقة %s
Blu-ray region: from the next disc you open = منطقة Blu-ray: من القرص التالي الذي تفتحه
A: the Americas and East Asia. B: Europe, Africa and Oceania. C: China, Russia and India = A: الأمريكتان وشرق آسيا. B: أوروبا وأفريقيا وأوقيانوسيا. C: الصين وروسيا والهند
Add a subtitle file = إضافة ملف ترجمة
Pick a subtitle file = اختر ملف ترجمة
Subtitles: %s = الترجمة: %s
Couldn't load that subtitle file = تعذّر تحميل ملف الترجمة هذا
That isn't a subtitle file (.srt, .ass, .vtt...) = هذا ليس ملف ترجمة (.srt و.ass و.vtt...)
Java Blu-ray discs (BD-J) aren't supported on the PS5 = أقراص Blu-ray التي تعمل بـ Java (BD-J) غير مدعومة على PS5
Hardware decoding = فك الترميز بالعتاد
The console's video chip. Off: the processor decodes, slower for 4K = شريحة الفيديو في الجهاز. عند الإيقاف: يفك المعالج الترميز، وهو أبطأ مع 4K
Looking for channels and stations… = جارٍ البحث عن القنوات والمحطات…
Hardware decoding: from the next video = فك الترميز بالعتاد: من الفيديو التالي
Match frame rate = مطابقة معدل الإطارات
Films play at their own 24, 25 or 50 Hz: smoother pans. Some TVs go black a moment = تُعرض الأفلام بترددها الأصلي 24 أو 25 أو 50 هرتز: حركة أنعم. بعض أجهزة التلفاز تسودّ لحظة
+18
View File
@@ -551,3 +551,21 @@ Sign in to the share = Bei der Freigabe anmelden
A Windows PC takes its own account; a NAS, the user you made on it. = Ein Windows-PC nimmt sein eigenes Konto, ein NAS den Benutzer, den du darauf angelegt hast.
Track %d = Spur %d
Track = Spur
Disc menu = Disc-Menü
Pop-up menu = Pop-up-Menü
Blu-ray region = Blu-ray-Region
Region %s = Region %s
Blu-ray region: from the next disc you open = Blu-ray-Region: ab der nächsten Disc, die du öffnest
A: the Americas and East Asia. B: Europe, Africa and Oceania. C: China, Russia and India = A: Amerika und Ostasien. B: Europa, Afrika und Ozeanien. C: China, Russland und Indien
Add a subtitle file = Untertiteldatei hinzufügen
Pick a subtitle file = Untertiteldatei wählen
Subtitles: %s = Untertitel: %s
Couldn't load that subtitle file = Diese Untertiteldatei ließ sich nicht laden
That isn't a subtitle file (.srt, .ass, .vtt...) = Das ist keine Untertiteldatei (.srt, .ass, .vtt...)
Java Blu-ray discs (BD-J) aren't supported on the PS5 = Java-Blu-rays (BD-J) werden auf der PS5 nicht unterstützt
Hardware decoding = Hardware-Dekodierung
The console's video chip. Off: the processor decodes, slower for 4K = Der Videochip der Konsole. Aus: Der Prozessor dekodiert, langsamer bei 4K
Looking for channels and stations… = Sender und Radiosender werden gesucht…
Hardware decoding: from the next video = Hardware-Dekodierung: ab dem nächsten Video
Match frame rate = Bildrate anpassen
Films play at their own 24, 25 or 50 Hz: smoother pans. Some TVs go black a moment = Filme laufen mit ihren eigenen 24, 25 oder 50 Hz: ruhigere Schwenks. Manche Fernseher werden kurz schwarz
+18
View File
@@ -551,3 +551,21 @@ Sign in to the share = Iniciar sesión en la carpeta compartida
A Windows PC takes its own account; a NAS, the user you made on it. = Un PC con Windows usa su propia cuenta; un NAS, el usuario que creaste en él.
Track %d = Pista %d
Track = Pista
Disc menu = Menú del disco
Pop-up menu = Menú emergente
Blu-ray region = Región Blu-ray
Region %s = Región %s
Blu-ray region: from the next disc you open = Región Blu-ray: desde el próximo disco que abras
A: the Americas and East Asia. B: Europe, Africa and Oceania. C: China, Russia and India = A: América y Asia oriental. B: Europa, África y Oceanía. C: China, Rusia e India
Add a subtitle file = Añadir un archivo de subtítulos
Pick a subtitle file = Elige un archivo de subtítulos
Subtitles: %s = Subtítulos: %s
Couldn't load that subtitle file = No se pudo cargar ese archivo de subtítulos
That isn't a subtitle file (.srt, .ass, .vtt...) = Eso no es un archivo de subtítulos (.srt, .ass, .vtt...)
Java Blu-ray discs (BD-J) aren't supported on the PS5 = Los Blu-ray con Java (BD-J) no son compatibles con la PS5
Hardware decoding = Decodificación por hardware
The console's video chip. Off: the processor decodes, slower for 4K = El chip de vídeo de la consola. Desactivado: decodifica el procesador, más lento en 4K
Looking for channels and stations… = Buscando canales y emisoras…
Hardware decoding: from the next video = Decodificación por hardware: desde el próximo vídeo
Match frame rate = Igualar la frecuencia de imagen
Films play at their own 24, 25 or 50 Hz: smoother pans. Some TVs go black a moment = Las películas se ven a sus propios 24, 25 o 50 Hz: paneos más suaves. Algunas TV se ponen en negro un momento
+18
View File
@@ -552,3 +552,21 @@ Sign in to the share = Se connecter au partage
A Windows PC takes its own account; a NAS, the user you made on it. = Un PC Windows prend son propre compte ; un NAS, l'utilisateur que vous y avez créé.
Track %d = Piste %d
Track = Piste
Disc menu = Menu du disque
Pop-up menu = Menu contextuel
Blu-ray region = Zone Blu-ray
Region %s = Zone %s
Blu-ray region: from the next disc you open = Zone Blu-ray : à partir du prochain disque ouvert
A: the Americas and East Asia. B: Europe, Africa and Oceania. C: China, Russia and India = A : Amériques et Asie de l'Est. B : Europe, Afrique et Océanie. C : Chine, Russie et Inde
Add a subtitle file = Ajouter un fichier de sous-titres
Pick a subtitle file = Choisissez un fichier de sous-titres
Subtitles: %s = Sous-titres : %s
Couldn't load that subtitle file = Impossible de charger ce fichier de sous-titres
That isn't a subtitle file (.srt, .ass, .vtt...) = Ce n'est pas un fichier de sous-titres (.srt, .ass, .vtt...)
Java Blu-ray discs (BD-J) aren't supported on the PS5 = Les Blu-ray Java (BD-J) ne sont pas pris en charge sur PS5
Hardware decoding = Décodage matériel
The console's video chip. Off: the processor decodes, slower for 4K = La puce vidéo de la console. Désactivé : le processeur décode, plus lent en 4K
Looking for channels and stations… = Recherche de chaînes et de stations…
Hardware decoding: from the next video = Décodage matériel : à partir de la prochaine vidéo
Match frame rate = Adapter la fréquence d'images
Films play at their own 24, 25 or 50 Hz: smoother pans. Some TVs go black a moment = Les films passent à leurs propres 24, 25 ou 50 Hz : panoramiques plus fluides. Certaines TV deviennent noires un instant
+18
View File
@@ -551,3 +551,21 @@ Sign in to the share = Masuk ke folder berbagi
A Windows PC takes its own account; a NAS, the user you made on it. = PC Windows memakai akunnya sendiri; NAS memakai pengguna yang Anda buat di sana.
Track %d = Trek %d
Track = Trek
Disc menu = Menu disk
Pop-up menu = Menu pop-up
Blu-ray region = Region Blu-ray
Region %s = Region %s
Blu-ray region: from the next disc you open = Region Blu-ray: mulai dari disk berikutnya yang kamu buka
A: the Americas and East Asia. B: Europe, Africa and Oceania. C: China, Russia and India = A: Amerika dan Asia Timur. B: Eropa, Afrika, dan Oseania. C: Tiongkok, Rusia, dan India
Add a subtitle file = Tambahkan file subtitle
Pick a subtitle file = Pilih file subtitle
Subtitles: %s = Subtitle: %s
Couldn't load that subtitle file = File subtitle itu tidak bisa dimuat
That isn't a subtitle file (.srt, .ass, .vtt...) = Itu bukan file subtitle (.srt, .ass, .vtt...)
Java Blu-ray discs (BD-J) aren't supported on the PS5 = Blu-ray Java (BD-J) tidak didukung di PS5
Hardware decoding = Dekode perangkat keras
The console's video chip. Off: the processor decodes, slower for 4K = Cip video konsol. Mati: prosesor yang mendekode, lebih lambat untuk 4K
Looking for channels and stations… = Mencari saluran dan stasiun…
Hardware decoding: from the next video = Dekode perangkat keras: mulai video berikutnya
Match frame rate = Samakan laju bingkai
Films play at their own 24, 25 or 50 Hz: smoother pans. Some TVs go black a moment = Film diputar pada 24, 25 atau 50 Hz aslinya: gerakan lebih halus. Beberapa TV gelap sesaat
+18
View File
@@ -551,3 +551,21 @@ Sign in to the share = Accedi alla condivisione
A Windows PC takes its own account; a NAS, the user you made on it. = Un PC Windows usa il proprio account; un NAS, l'utente che hai creato su di esso.
Track %d = Traccia %d
Track = Traccia
Disc menu = Menu del disco
Pop-up menu = Menu pop-up
Blu-ray region = Area Blu-ray
Region %s = Area %s
Blu-ray region: from the next disc you open = Area Blu-ray: dal prossimo disco che apri
A: the Americas and East Asia. B: Europe, Africa and Oceania. C: China, Russia and India = A: Americhe e Asia orientale. B: Europa, Africa e Oceania. C: Cina, Russia e India
Add a subtitle file = Aggiungi un file di sottotitoli
Pick a subtitle file = Scegli un file di sottotitoli
Subtitles: %s = Sottotitoli: %s
Couldn't load that subtitle file = Impossibile caricare quel file di sottotitoli
That isn't a subtitle file (.srt, .ass, .vtt...) = Non è un file di sottotitoli (.srt, .ass, .vtt...)
Java Blu-ray discs (BD-J) aren't supported on the PS5 = I Blu-ray Java (BD-J) non sono supportati su PS5
Hardware decoding = Decodifica hardware
The console's video chip. Off: the processor decodes, slower for 4K = Il chip video della console. Disattivato: decodifica il processore, più lento in 4K
Looking for channels and stations… = Ricerca di canali e stazioni…
Hardware decoding: from the next video = Decodifica hardware: dal prossimo video
Match frame rate = Adatta frequenza fotogrammi
Films play at their own 24, 25 or 50 Hz: smoother pans. Some TVs go black a moment = I film vanno ai loro 24, 25 o 50 Hz: panoramiche più fluide. Alcune TV diventano nere per un attimo
+18
View File
@@ -551,3 +551,21 @@ Sign in to the share = 共有フォルダーにサインイン
A Windows PC takes its own account; a NAS, the user you made on it. = Windows PC ならその PC のアカウント、NAS なら NAS で作ったユーザーを使います。
Track %d = トラック %d
Track = トラック
Disc menu = ディスクメニュー
Pop-up menu = ポップアップメニュー
Blu-ray region = Blu-ray リージョン
Region %s = リージョン %s
Blu-ray region: from the next disc you open = Blu-ray リージョン: 次に開くディスクから
A: the Americas and East Asia. B: Europe, Africa and Oceania. C: China, Russia and India = A: 南北アメリカと東アジア。B: ヨーロッパ、アフリカ、オセアニア。C: 中国、ロシア、インド
Add a subtitle file = 字幕ファイルを追加
Pick a subtitle file = 字幕ファイルを選択
Subtitles: %s = 字幕: %s
Couldn't load that subtitle file = その字幕ファイルを読み込めませんでした
That isn't a subtitle file (.srt, .ass, .vtt...) = 字幕ファイルではありません (.srt、.ass、.vtt など)
Java Blu-ray discs (BD-J) aren't supported on the PS5 = Java の Blu-ray (BD-J) は PS5 では再生できません
Hardware decoding = ハードウェアデコード
The console's video chip. Off: the processor decodes, slower for 4K = 本体のビデオチップ。オフ:プロセッサーでデコード(4Kでは遅くなります)
Looking for channels and stations… = チャンネルと局を検索中…
Hardware decoding: from the next video = ハードウェアデコード:次のビデオから
Match frame rate = フレームレートを合わせる
Films play at their own 24, 25 or 50 Hz: smoother pans. Some TVs go black a moment = 映画を本来の24・25・50Hzで再生し、パンが滑らかになります。一部のテレビは一瞬暗くなります
+18
View File
@@ -551,3 +551,21 @@ Sign in to the share = 공유 폴더에 로그인
A Windows PC takes its own account; a NAS, the user you made on it. = Windows PC는 그 PC의 계정을, NAS는 NAS에서 만든 사용자를 사용합니다.
Track %d = 트랙 %d
Track = 트랙
Disc menu = 디스크 메뉴
Pop-up menu = 팝업 메뉴
Blu-ray region = Blu-ray 지역
Region %s = 지역 %s
Blu-ray region: from the next disc you open = Blu-ray 지역: 다음에 여는 디스크부터
A: the Americas and East Asia. B: Europe, Africa and Oceania. C: China, Russia and India = A: 아메리카와 동아시아. B: 유럽, 아프리카, 오세아니아. C: 중국, 러시아, 인도
Add a subtitle file = 자막 파일 추가
Pick a subtitle file = 자막 파일 선택
Subtitles: %s = 자막: %s
Couldn't load that subtitle file = 그 자막 파일을 불러올 수 없습니다
That isn't a subtitle file (.srt, .ass, .vtt...) = 자막 파일이 아닙니다 (.srt, .ass, .vtt...)
Java Blu-ray discs (BD-J) aren't supported on the PS5 = Java 블루레이(BD-J)는 PS5에서 지원되지 않습니다
Hardware decoding = 하드웨어 디코딩
The console's video chip. Off: the processor decodes, slower for 4K = 본체의 비디오 칩. 끄면 프로세서가 디코딩하며 4K에서는 느립니다
Looking for channels and stations… = 채널과 방송국을 찾는 중…
Hardware decoding: from the next video = 하드웨어 디코딩: 다음 동영상부터
Match frame rate = 프레임 속도 맞추기
Films play at their own 24, 25 or 50 Hz: smoother pans. Some TVs go black a moment = 영화를 원래의 24, 25, 50Hz로 재생해 화면 이동이 부드럽습니다. 일부 TV는 잠시 검게 됩니다
+18
View File
@@ -551,3 +551,21 @@ Sign in to the share = Aanmelden bij de share
A Windows PC takes its own account; a NAS, the user you made on it. = Een Windows-pc gebruikt zijn eigen account; een NAS de gebruiker die je erop hebt gemaakt.
Track %d = Track %d
Track = Track
Disc menu = Schijfmenu
Pop-up menu = Pop-upmenu
Blu-ray region = Blu-ray-regio
Region %s = Regio %s
Blu-ray region: from the next disc you open = Blu-ray-regio: vanaf de volgende schijf die je opent
A: the Americas and East Asia. B: Europe, Africa and Oceania. C: China, Russia and India = A: Amerika en Oost-Azië. B: Europa, Afrika en Oceanië. C: China, Rusland en India
Add a subtitle file = Ondertitelbestand toevoegen
Pick a subtitle file = Kies een ondertitelbestand
Subtitles: %s = Ondertitels: %s
Couldn't load that subtitle file = Dat ondertitelbestand kon niet worden geladen
That isn't a subtitle file (.srt, .ass, .vtt...) = Dat is geen ondertitelbestand (.srt, .ass, .vtt...)
Java Blu-ray discs (BD-J) aren't supported on the PS5 = Java-Blu-rays (BD-J) worden niet ondersteund op de PS5
Hardware decoding = Hardwaredecodering
The console's video chip. Off: the processor decodes, slower for 4K = De videochip van de console. Uit: de processor decodeert, trager bij 4K
Looking for channels and stations… = Zenders en stations zoeken…
Hardware decoding: from the next video = Hardwaredecodering: vanaf de volgende video
Match frame rate = Beeldfrequentie aanpassen
Films play at their own 24, 25 or 50 Hz: smoother pans. Some TVs go black a moment = Films spelen op hun eigen 24, 25 of 50 Hz: vloeiendere pans. Sommige tv's worden even zwart
+18
View File
@@ -551,3 +551,21 @@ Sign in to the share = Zaloguj się do udziału
A Windows PC takes its own account; a NAS, the user you made on it. = Komputer z Windows przyjmuje własne konto, a NAS użytkownika, którego na nim utworzono.
Track %d = Ścieżka %d
Track = Ścieżka
Disc menu = Menu płyty
Pop-up menu = Menu podręczne
Blu-ray region = Region Blu-ray
Region %s = Region %s
Blu-ray region: from the next disc you open = Region Blu-ray: od następnej otwartej płyty
A: the Americas and East Asia. B: Europe, Africa and Oceania. C: China, Russia and India = A: Ameryki i Azja Wschodnia. B: Europa, Afryka i Oceania. C: Chiny, Rosja i Indie
Add a subtitle file = Dodaj plik napisów
Pick a subtitle file = Wybierz plik napisów
Subtitles: %s = Napisy: %s
Couldn't load that subtitle file = Nie udało się wczytać tego pliku napisów
That isn't a subtitle file (.srt, .ass, .vtt...) = To nie jest plik napisów (.srt, .ass, .vtt...)
Java Blu-ray discs (BD-J) aren't supported on the PS5 = Płyty Blu-ray z Javą (BD-J) nie są obsługiwane na PS5
Hardware decoding = Dekodowanie sprzętowe
The console's video chip. Off: the processor decodes, slower for 4K = Układ wideo konsoli. Wył.: dekoduje procesor, wolniej przy 4K
Looking for channels and stations… = Szukanie kanałów i stacji…
Hardware decoding: from the next video = Dekodowanie sprzętowe: od następnego filmu
Match frame rate = Dopasuj liczbę klatek
Films play at their own 24, 25 or 50 Hz: smoother pans. Some TVs go black a moment = Filmy grają we własnych 24, 25 lub 50 Hz: płynniejsze panoramy. Niektóre telewizory na chwilę gasną
+18
View File
@@ -551,3 +551,21 @@ Sign in to the share = Entrar no compartilhamento
A Windows PC takes its own account; a NAS, the user you made on it. = Um PC com Windows usa a própria conta; um NAS, o usuário que você criou nele.
Track %d = Faixa %d
Track = Faixa
Disc menu = Menu do disco
Pop-up menu = Menu pop-up
Blu-ray region = Região do Blu-ray
Region %s = Região %s
Blu-ray region: from the next disc you open = Região do Blu-ray: a partir do próximo disco que você abrir
A: the Americas and East Asia. B: Europe, Africa and Oceania. C: China, Russia and India = A: Américas e Leste Asiático. B: Europa, África e Oceania. C: China, Rússia e Índia
Add a subtitle file = Adicionar um arquivo de legenda
Pick a subtitle file = Escolha um arquivo de legenda
Subtitles: %s = Legendas: %s
Couldn't load that subtitle file = Não foi possível carregar esse arquivo de legenda
That isn't a subtitle file (.srt, .ass, .vtt...) = Isso não é um arquivo de legenda (.srt, .ass, .vtt...)
Java Blu-ray discs (BD-J) aren't supported on the PS5 = Blu-rays com Java (BD-J) não são compatíveis com o PS5
Hardware decoding = Decodificação por hardware
The console's video chip. Off: the processor decodes, slower for 4K = O chip de vídeo do console. Desligado: o processador decodifica, mais lento em 4K
Looking for channels and stations… = Procurando canais e estações…
Hardware decoding: from the next video = Decodificação por hardware: a partir do próximo vídeo
Match frame rate = Igualar taxa de quadros
Films play at their own 24, 25 or 50 Hz: smoother pans. Some TVs go black a moment = Filmes rodam nos seus próprios 24, 25 ou 50 Hz: movimentos mais suaves. Algumas TVs ficam pretas por um instante
+18
View File
@@ -551,3 +551,21 @@ Sign in to the share = Вход в общую папку
A Windows PC takes its own account; a NAS, the user you made on it. = Компьютер с Windows принимает свою учётную запись, NAS — пользователя, которого вы на нём создали.
Track %d = Дорожка %d
Track = Дорожка
Disc menu = Меню диска
Pop-up menu = Всплывающее меню
Blu-ray region = Регион Blu-ray
Region %s = Регион %s
Blu-ray region: from the next disc you open = Регион Blu-ray: со следующего открытого диска
A: the Americas and East Asia. B: Europe, Africa and Oceania. C: China, Russia and India = A: Америка и Восточная Азия. B: Европа, Африка и Океания. C: Китай, Россия и Индия
Add a subtitle file = Добавить файл субтитров
Pick a subtitle file = Выберите файл субтитров
Subtitles: %s = Субтитры: %s
Couldn't load that subtitle file = Не удалось загрузить этот файл субтитров
That isn't a subtitle file (.srt, .ass, .vtt...) = Это не файл субтитров (.srt, .ass, .vtt...)
Java Blu-ray discs (BD-J) aren't supported on the PS5 = Blu-ray с Java (BD-J) не поддерживаются на PS5
Hardware decoding = Аппаратное декодирование
The console's video chip. Off: the processor decodes, slower for 4K = Видеочип консоли. Выкл.: декодирует процессор, медленнее для 4K
Looking for channels and stations… = Поиск каналов и станций…
Hardware decoding: from the next video = Аппаратное декодирование: со следующего видео
Match frame rate = Подстройка частоты кадров
Films play at their own 24, 25 or 50 Hz: smoother pans. Some TVs go black a moment = Фильмы идут со своей частотой 24, 25 или 50 Гц: плавнее панорамы. Некоторые ТВ на миг гаснут
+18
View File
@@ -551,3 +551,21 @@ Sign in to the share = Paylaşıma giriş yap
A Windows PC takes its own account; a NAS, the user you made on it. = Windows PC kendi hesabını, NAS ise üzerinde oluşturduğunuz kullanıcıyı kabul eder.
Track %d = Parça %d
Track = Parça
Disc menu = Disk menüsü
Pop-up menu = Açılır menü
Blu-ray region = Blu-ray bölgesi
Region %s = Bölge %s
Blu-ray region: from the next disc you open = Blu-ray bölgesi: açacağın bir sonraki diskten itibaren
A: the Americas and East Asia. B: Europe, Africa and Oceania. C: China, Russia and India = A: Amerika ve Doğu Asya. B: Avrupa, Afrika ve Okyanusya. C: Çin, Rusya ve Hindistan
Add a subtitle file = Altyazı dosyası ekle
Pick a subtitle file = Bir altyazı dosyası seç
Subtitles: %s = Altyazı: %s
Couldn't load that subtitle file = Bu altyazı dosyası yüklenemedi
That isn't a subtitle file (.srt, .ass, .vtt...) = Bu bir altyazı dosyası değil (.srt, .ass, .vtt...)
Java Blu-ray discs (BD-J) aren't supported on the PS5 = Java Blu-ray diskleri (BD-J) PS5'te desteklenmiyor
Hardware decoding = Donanımla kod çözme
The console's video chip. Off: the processor decodes, slower for 4K = Konsolun video yongası. Kapalı: işlemci çözer, 4K'da daha yavaş
Looking for channels and stations… = Kanallar ve istasyonlar aranıyor…
Hardware decoding: from the next video = Donanımla kod çözme: sonraki videodan itibaren
Match frame rate = Kare hızını eşle
Films play at their own 24, 25 or 50 Hz: smoother pans. Some TVs go black a moment = Filmler kendi 24, 25 veya 50 Hz'lerinde oynar: daha akıcı kaydırmalar. Bazı TV'ler bir an kararır
+18
View File
@@ -551,3 +551,21 @@ Sign in to the share = 登录共享文件夹
A Windows PC takes its own account; a NAS, the user you made on it. = Windows 电脑用它自己的账户;NAS 用你在上面创建的用户。
Track %d = 音轨 %d
Track = 音轨
Disc menu = 光盘菜单
Pop-up menu = 弹出菜单
Blu-ray region = 蓝光区域
Region %s = 区域 %s
Blu-ray region: from the next disc you open = 蓝光区域:从下一张打开的光盘起生效
A: the Americas and East Asia. B: Europe, Africa and Oceania. C: China, Russia and India = A:美洲和东亚。B:欧洲、非洲和大洋洲。C:中国、俄罗斯和印度
Add a subtitle file = 添加字幕文件
Pick a subtitle file = 选择字幕文件
Subtitles: %s = 字幕:%s
Couldn't load that subtitle file = 无法加载该字幕文件
That isn't a subtitle file (.srt, .ass, .vtt...) = 这不是字幕文件(.srt、.ass、.vtt…)
Java Blu-ray discs (BD-J) aren't supported on the PS5 = PS5 不支持 Java 蓝光光盘 (BD-J)
Hardware decoding = 硬件解码
The console's video chip. Off: the processor decodes, slower for 4K = 主机的视频芯片。关闭:由处理器解码,4K 较慢
Looking for channels and stations… = 正在查找频道和电台…
Hardware decoding: from the next video = 硬件解码:从下一个视频开始
Match frame rate = 匹配帧率
Films play at their own 24, 25 or 50 Hz: smoother pans. Some TVs go black a moment = 电影以其本身的 24、25 或 50 Hz 播放:平移更流畅。部分电视会黑屏片刻
+18
View File
@@ -551,3 +551,21 @@ Sign in to the share = 登入共用資料夾
A Windows PC takes its own account; a NAS, the user you made on it. = Windows 電腦用它自己的帳戶;NAS 用你在上面建立的使用者。
Track %d = 音軌 %d
Track = 音軌
Disc menu = 光碟選單
Pop-up menu = 彈出式選單
Blu-ray region = 藍光區碼
Region %s = 區碼 %s
Blu-ray region: from the next disc you open = 藍光區碼:從下一張開啟的光碟起生效
A: the Americas and East Asia. B: Europe, Africa and Oceania. C: China, Russia and India = A:美洲與東亞。B:歐洲、非洲與大洋洲。C:中國、俄羅斯與印度
Add a subtitle file = 加入字幕檔
Pick a subtitle file = 選擇字幕檔
Subtitles: %s = 字幕:%s
Couldn't load that subtitle file = 無法載入該字幕檔
That isn't a subtitle file (.srt, .ass, .vtt...) = 這不是字幕檔(.srt、.ass、.vtt…)
Java Blu-ray discs (BD-J) aren't supported on the PS5 = PS5 不支援 Java 藍光光碟 (BD-J)
Hardware decoding = 硬體解碼
The console's video chip. Off: the processor decodes, slower for 4K = 主機的視訊晶片。關閉:由處理器解碼,4K 較慢
Looking for channels and stations… = 正在尋找頻道和電台…
Hardware decoding: from the next video = 硬體解碼:從下一個影片開始
Match frame rate = 配合影格率
Films play at their own 24, 25 or 50 Hz: smoother pans. Some TVs go black a moment = 電影以其原本的 24、25 或 50 Hz 播放:平移更流暢。部分電視會黑屏片刻
+38
View File
@@ -0,0 +1,38 @@
--- a/modules/video_output/vmem.c
+++ b/modules/video_output/vmem.c
@@ -145,6 +145,8 @@
var_InheritAddress(vd, "vmem-colour");
if (colour != NULL)
colour(sys->opaque, &fmt);
+ /* VLC-PS5: the decoder's own format, for its visible part below */
+ const video_format_t source = fmt;
if (setup != NULL) {
char chroma[5];
@@ -178,9 +180,23 @@
}
sys->cleanup = NULL;
}
- fmt.i_x_offset = fmt.i_y_offset = 0;
- fmt.i_visible_width = fmt.i_width;
- fmt.i_visible_height = fmt.i_height;
+ /* VLC-PS5: a buffer at the decoder's own size keeps the decoder's
+ * visible part (1280x720 in 1280x736; 3840x2160 in 3840x2176): called
+ * all visible, it made VLC scale every picture on the CPU to fill it
+ * (swscale, a whole 4K picture per frame in software decoding). The
+ * app crops it when drawing. */
+ if (fmt.i_width == source.i_width && fmt.i_height == source.i_height &&
+ source.i_x_offset == 0 && source.i_y_offset == 0 &&
+ source.i_visible_width > 0 && source.i_visible_height > 0 &&
+ source.i_visible_width <= fmt.i_width && source.i_visible_height <= fmt.i_height) {
+ fmt.i_x_offset = fmt.i_y_offset = 0;
+ fmt.i_visible_width = source.i_visible_width;
+ fmt.i_visible_height = source.i_visible_height;
+ } else {
+ fmt.i_x_offset = fmt.i_y_offset = 0;
+ fmt.i_visible_width = fmt.i_width;
+ fmt.i_visible_height = fmt.i_height;
+ }
if (!fmt.i_chroma) {
msg_Err(vd, "vmem-chroma should be 4 characters long");
@@ -0,0 +1,41 @@
--- a/modules/codec/avcodec/video.c
+++ b/modules/codec/avcodec/video.c
@@ -84,6 +84,9 @@
/* Hack to force display of still pictures */
bool b_first_frame;
+ /* VLC-PS5: pictures refused in a row (see DecodeBlock) */
+ int ps5_refused;
+
bool b_draining;
/* */
@@ -1244,6 +1247,19 @@
}
ret = avcodec_send_packet(p_context, pkt);
+ if( ret == AVERROR(ENOMEM) && p_sys->ps5_refused < 200 )
+ {
+ /* VLC-PS5: no picture for a moment (the pictures are cancelled
+ * at a live stream's break, while the decoder's threads still
+ * ask for one): that packet is lost, and the video goes on
+ * from the next keyframe instead of ending (BBC America's
+ * picture stopped for good at a segment break, 2026-10-13) */
+ if (p_sys->ps5_refused++ == 0)
+ msg_Warn(p_dec, "avcodec_send_packet: no picture for now, going on from the next keyframe");
+ avcodec_flush_buffers(p_context);
+ av_packet_free( &pkt );
+ break;
+ }
if( ret != 0 && ret != AVERROR(EAGAIN) )
{
if (ret == AVERROR(ENOMEM) || ret == AVERROR(EINVAL))
@@ -1283,6 +1299,8 @@
break;
}
bool not_received_frame = ret;
+ if (!not_received_frame)
+ p_sys->ps5_refused = 0;
wait_mt( p_sys );
@@ -0,0 +1,18 @@
--- a/modules/demux/adaptive/PlaylistManager.cpp
+++ b/modules/demux/adaptive/PlaylistManager.cpp
@@ -685,8 +685,14 @@
}
case DEMUX_GET_PTS_DELAY:
- *va_arg (args, int64_t *) = 1000 * INT64_C(1000);
+ {
+ /* VLC-PS5: the player's network caching, not always 1 s: a
+ * channel whose server sends barely in real time stalled and
+ * rebuffered each time (BBC America, 2026-10-18) */
+ int64_t ms = var_InheritInteger(p_demux, "network-caching");
+ *va_arg (args, int64_t *) = (ms > 1000 ? ms : 1000) * INT64_C(1000);
break;
+ }
default:
return VLC_EGENERIC;
@@ -0,0 +1,58 @@
--- a/modules/demux/adaptive/http/HTTPConnection.cpp
+++ b/modules/demux/adaptive/http/HTTPConnection.cpp
@@ -42,6 +42,19 @@
using namespace adaptive::http;
+/* VLC-PS5: the player stopped this video (src/player.cc). Without this,
+ * after a stop the next segment was still fetched, by VLC's own http once
+ * the console's refused it, and leaving a slow channel took 10 s; the next
+ * video waited for it (2026-10-18). */
+extern "C" int vlc_ps5_stream_cancelled(long) __attribute__((weak));
+static bool ps5_cancelled(vlc_object_t *obj)
+{
+ if (vlc_ps5_stream_cancelled == nullptr)
+ return false;
+ long serial = (long)var_InheritInteger(obj, "ps5http-serial");
+ return serial > 0 && vlc_ps5_stream_cancelled(serial);
+}
+
AbstractConnection::AbstractConnection(vlc_object_t *p_object_)
{
p_object = p_object_;
@@ -295,7 +308,7 @@
RequestStatus LibVLCHTTPConnection::request(const std::string &path,
const BytesRange &range)
{
- if(source->http_mgr == nullptr)
+ if(source->http_mgr == nullptr || ps5_cancelled(p_object))
return RequestStatus::GenericError;
reset();
@@ -414,6 +427,8 @@
ssize_t LibVLCHTTPConnection::read(void *p_buffer, size_t len)
{
+ if (ps5_cancelled(p_object))
+ return 0;
ssize_t read = vlc_stream_Read(stream, p_buffer, len);
bytesRead = source->totalRead;
return read;
@@ -462,6 +477,8 @@
RequestStatus StreamUrlConnection::request(const std::string &path,
const BytesRange &range)
{
+ if (ps5_cancelled(p_object))
+ return RequestStatus::GenericError;
reset();
/* Set new path for this query */
@@ -509,6 +526,8 @@
{
if( !p_streamurl )
return VLC_EGENERIC;
+ if (ps5_cancelled(p_object))
+ return 0;
if(len == 0)
return VLC_SUCCESS;
+16
View File
@@ -0,0 +1,16 @@
--- a/src/input/clock.c
+++ b/src/input/clock.c
@@ -80,7 +80,12 @@
*****************************************************************************/
/* Maximum gap allowed between two CRs. */
-#define CR_MAX_GAP (60 * CLOCK_FREQ)
+/* VLC-PS5: 10 s, not 60. Between two clock references of one stream
+ * (sent every 100 ms or so) a jump over 10 s is a new start; at 60 s a free
+ * TV channel's ad break that moved its times by 35 s was taken as playing
+ * on, every picture and sound counted 35 s late and thrown away, and the
+ * picture froze (a Pluto channel, 2026-10-18). */
+#define CR_MAX_GAP (10 * CLOCK_FREQ)
/* Latency introduced on DVDs with CR == 0 on chapter change - this is from
* my dice --Meuuh */
@@ -0,0 +1,15 @@
--- a/modules/demux/adaptive/logic/BufferingLogic.cpp
+++ b/modules/demux/adaptive/logic/BufferingLogic.cpp
@@ -38,7 +38,11 @@
using namespace adaptive::logic;
const vlc_tick_t AbstractBufferingLogic::BUFFERING_LOWEST_LIMIT = CLOCK_FREQ * 2;
-const vlc_tick_t AbstractBufferingLogic::DEFAULT_MIN_BUFFERING = CLOCK_FREQ * 6;
+/* VLC-PS5: 3 s before a live channel starts, not 6: on the console's Wi-Fi
+ * (about 700 KiB/s to far servers) 6 s of an 8 Mbit/s channel took 4-5 s to
+ * come in, and channels were slow to show (2026-10-18). It still fills up
+ * to its live delay while it plays. */
+const vlc_tick_t AbstractBufferingLogic::DEFAULT_MIN_BUFFERING = CLOCK_FREQ * 3;
const vlc_tick_t AbstractBufferingLogic::DEFAULT_MAX_BUFFERING = CLOCK_FREQ * 30;
const vlc_tick_t AbstractBufferingLogic::DEFAULT_LIVE_BUFFERING = CLOCK_FREQ * 15;
+36 -1
View File
@@ -47,7 +47,7 @@
/* The display and its modes: one each per process, as VideoOut is. */
struct wsi_videoout_mode {
uint32_t refresh_millihertz;
@@ -399,6 +437,54 @@
@@ -399,6 +437,89 @@
return VK_SUCCESS;
}
@@ -98,6 +98,41 @@
+ mtx_unlock(&out->lock);
+ return result;
+}
+
+/*
+ * VLC-PS5: the output's refresh for films, as VideoOut's output modes number
+ * it: 2 (23.976 Hz), 3 (24 Hz), 9 (50 Hz), 1 (back to the default). Asked
+ * first whether the output takes it; then the vblank period measured, in
+ * microseconds, into *period_us (0 if not changed). 0 when VideoOut took it.
+ */
+int vlcps5_wsi_videoout_set_refresh(int mode, int *period_us);
+
+int
+vlcps5_wsi_videoout_set_refresh(int mode, int *period_us)
+{
+ struct wsi_videoout_output *out = &videoout_output;
+ int result = -1;
+ if (period_us)
+ *period_us = 0;
+ mtx_lock(&out->lock);
+ if (videoout_open_locked()) {
+ const int supported = mode == (int)PS5_VIDEO_OUT_MODE_RESTORE
+ ? 1
+ : sceVideoOutIsOutputSupported(out->handle, (uint32_t)mode, NULL, NULL, NULL);
+ result = supported > 0 ? sceVideoOutConfigureOutput(out->handle, (uint32_t)mode, NULL, NULL, NULL)
+ : (supported < 0 ? supported : -2);
+ int measured = 0;
+ if (result == 0)
+ measured = (int)(videoout_vblank_period_ns(out->handle) / 1000);
+ if (period_us)
+ *period_us = measured;
+ fprintf(stderr, "wsi/videoout: output mode %d: %s (0x%08x), a vblank every %.3f ms\n", mode,
+ result == 0 ? "taken" : supported > 0 ? "refused" : "not supported", (unsigned)result,
+ measured / 1000.0);
+ }
+ mtx_unlock(&out->lock);
+ return result;
+}
+
static int64_t
videoout_shown_argument_locked(void)
+7 -3
View File
@@ -226,7 +226,7 @@ int vlc_ps5_big_free(void *addr)
for (int i = 0; i < BIG_MAX && !found; i++) {
if (big_maps[i].addr && big_maps[i].addr == addr) {
if (big_maps[i].start < 0) {
sceKernelReleaseFlexibleMemory(addr, big_maps[i].len);
sceKernelMunmap(addr, big_maps[i].len); /* (released, it never came back) */
big_maps[i].addr = NULL;
found = 1;
break;
@@ -339,7 +339,7 @@ void *__wrap_mmap(void *addr, size_t len, int prot, int flags, int fd,
return MAP_FAILED;
}
if ((flags & MAP_FIXED) && result != addr) {
sceKernelReleaseFlexibleMemory(result, len);
sceKernelMunmap(result, len);
errno = EINVAL;
return MAP_FAILED;
}
@@ -352,7 +352,11 @@ int __wrap_munmap(void *addr, size_t len)
if (unmap_direct(addr, len)) {
return 0;
}
if (sceKernelReleaseFlexibleMemory(addr, len) == 0) {
/* Flexible memory comes back with munmap, not with
* sceKernelReleaseFlexibleMemory: released, it stayed taken (the app's
* self-test on the console, 2026-10-13: 32 MiB released, 0 back; 32 MiB
* unmapped, all back). Every big free VLC and FFmpeg made leaked so. */
if (sceKernelMunmap(addr, len) == 0) {
return 0;
}
/* A reserved range, or a file mapping. */
+498 -7
View File
@@ -17,7 +17,12 @@
#include <errno.h>
#include <inttypes.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <netinet/in.h>
#include <pthread.h>
#include <sys/socket.h>
#include <time.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
@@ -27,6 +32,7 @@
#include <vlc_common.h>
#include <vlc_access.h>
#include <vlc_interrupt.h>
#include <stdatomic.h>
#include <vlc_plugin.h>
#include <vlc_url.h>
@@ -69,13 +75,76 @@ static int http_template = -1;
/* The console refused a request outright (run 15: 0x8095f00c on every https
* request): VLC's own http module does the rest of the session's. */
static bool service_refused;
/* failures in a row that no answer came between: the service itself only
* after a few (one cancelled search turned it off for the whole session and
* the next channel went to VLC's own http, 2026-10-18) */
static atomic_int service_failures;
static atomic_int https_name_refusals;
/* Servers the console's SSL turned down: straight to VLC's own http next
* time (every segment of a channel paid a refused attempt first). After
* several different ones it refuses https as a whole this session (a log of
* 2026-10-13: every https server refused, GitHub too). */
#define REFUSED_MAX 64
static vlc_mutex_t refused_lock = VLC_STATIC_MUTEX;
static char *refused_hosts[REFUSED_MAX];
static unsigned refused_count;
static bool https_refused;
static char *url_host(const char *url)
{
const char *p = strstr(url, "://");
if (!p)
return NULL;
p += 3;
size_t n = strcspn(p, "/?#");
return strndup(p, n);
}
static bool host_refused(const char *url)
{
if (strncasecmp(url, "https:", 6))
return false;
char *host = url_host(url);
bool refused = false;
vlc_mutex_lock(&refused_lock);
refused = https_refused;
for (unsigned i = 0; host && !refused && i < refused_count; i++)
refused = !strcasecmp(refused_hosts[i], host);
vlc_mutex_unlock(&refused_lock);
free(host);
return refused;
}
static unsigned ssl_refused_count;
static void refuse_host(const char *url, bool ssl)
{
char *host = url_host(url);
if (!host)
return;
vlc_mutex_lock(&refused_lock);
bool known = false;
for (unsigned i = 0; i < refused_count; i++)
known = known || !strcasecmp(refused_hosts[i], host);
if (!known && refused_count < REFUSED_MAX) {
refused_hosts[refused_count++] = host;
host = NULL;
if (ssl && ++ssl_refused_count == 4) {
https_refused = true;
fprintf(stderr, "ps5http: SSL refused 4 different servers: VLC's own http takes every https link\n");
}
}
vlc_mutex_unlock(&refused_lock);
free(host);
}
static void start_service(void)
{
sceNetInit();
int pool = sceNetPoolCreate("vlcps5_http", 1024 * 1024, 0);
int pool = sceNetPoolCreate("vlcps5_http", 4 * 1024 * 1024, 0); /* room for parallel pieces */
int ssl = sceSslInit(320 * 1024);
int http = pool >= 0 && ssl >= 0 ? sceHttpInit(pool, ssl, 4 * 1024 * 1024) : -1;
int http = pool >= 0 && ssl >= 0 ? sceHttpInit(pool, ssl, 8 * 1024 * 1024) : -1;
if (http < 0) {
fprintf(stderr, "ps5http: the console's HTTP service didn't start (net %d, ssl %d, http %d)\n",
pool, ssl, http);
@@ -106,8 +175,24 @@ typedef struct ps5http_sys {
char *content_type;
char *redirect; /* Location of a 3xx */
int status;
long serial; /* the player's open this belongs to (0: none) */
bool logged_direct;
mtime_t opened_at; /* for the speed in the log */
uint64_t read_bytes;
mtime_t read_wait; /* time spent inside the console's read */
bool cancel_logged;
struct parallel *par; /* a segment fetched over several connections, or NULL */
} ps5http_sys;
/* The player (src/player.cc): its open number was stopped */
extern int vlc_ps5_stream_cancelled(long serial) __attribute__((weak));
extern int vlc_ps5_log_details(void) __attribute__((weak));
static bool cancelled(ps5http_sys *sys)
{
return sys->serial && vlc_ps5_stream_cancelled && vlc_ps5_stream_cancelled(sys->serial);
}
static void drop_request(ps5http_sys *sys)
{
if (sys->req >= 0)
@@ -147,6 +232,303 @@ static void abort_on_interrupt(void *data)
sceHttpAbortRequest(*req);
}
/* The app's own name lookup (ps5/compat/vlc_resolver.c): the console's
* DNS can be one that blocks (a PS5 set up to keep away from updates sends
* names to an address that refuses): every named server failed with
* 0x8041013d while bare addresses worked (2026-10-18). */
extern int vlcps5_getaddrinfo(const char *node, const char *service, const struct addrinfo *hints,
struct addrinfo **res);
extern void vlcps5_freeaddrinfo(struct addrinfo *ai);
static atomic_bool direct_broken; /* a server answered an address-only request wrongly */
/* An http:// link with a name: the same link with the address our lookup
* gives, and the name for the Host header ("name" or "name:port"). NULL: as
* it is (https: the secure connection needs the name; an address already). */
static char *direct_url(const char *url, char **host_header)
{
*host_header = NULL;
if (atomic_load(&direct_broken) || strncasecmp(url, "http://", 7))
return NULL;
const char *h = url + 7;
size_t hlen = strcspn(h, "/?#");
if (!hlen || h[0] == '[' || memchr(h, '@', hlen))
return NULL;
char *hostport = strndup(h, hlen);
if (!hostport)
return NULL;
char *colon = strchr(hostport, ':');
char name[256];
snprintf(name, sizeof(name), "%.*s", colon ? (int)(colon - hostport) : (int)hlen, hostport);
struct in_addr ip;
if (inet_pton(AF_INET, name, &ip) == 1) {
free(hostport);
return NULL;
}
struct addrinfo hints = { .ai_family = AF_INET, .ai_socktype = SOCK_STREAM }, *res = NULL;
if (vlcps5_getaddrinfo(name, NULL, &hints, &res) != 0 || !res) {
free(hostport);
return NULL;
}
char addr[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &((struct sockaddr_in *)res->ai_addr)->sin_addr, addr, sizeof(addr));
vlcps5_freeaddrinfo(res);
char *out = NULL;
if (asprintf(&out, "http://%s%s%s", addr, colon ? colon : "", h + hlen) < 0) {
free(hostport);
return NULL;
}
*host_header = hostport;
return out;
}
/* ---- a whole segment over several connections ------------------------------
*
* One connection to a far server is often slower than the line: the console
* read BBC America at a steady 700 KiB/s, a 4 s segment in 5 s, and the
* channel froze while the same server played on a PC (2026-10-18). A file of
* a few MiB served with byte ranges (a live channel's segment) is fetched by
* PAR_WORKERS connections at once, PAR_CHUNK at a time, into memory; VLC
* reads it in order as the pieces come. */
#define PAR_CHUNK ((uint64_t)512 << 10)
#define PAR_WORKERS 4
#define PAR_MIN ((uint64_t)1 << 20) /* smaller: one connection is enough */
#define PAR_MAX ((uint64_t)24 << 20) /* bigger (films): one connection, as before */
typedef struct parallel {
char *url, *ua, *ref;
long serial;
uint8_t *buf;
uint64_t size;
unsigned chunks, next;
uint32_t *filled; /* bytes in so far, per chunk */
bool *failed;
bool stop;
int reqs[PAR_WORKERS]; /* each worker's request, for an abort */
pthread_t threads[PAR_WORKERS];
int nthreads;
uint64_t fetched;
pthread_mutex_t lock;
pthread_cond_t cond;
} parallel;
static void add_headers(int req, const char *host_header, const char *ua, const char *ref)
{
if (host_header)
sceHttpAddRequestHeader(req, "Host", host_header, HEADER_OVERWRITE);
if (ua && *ua)
sceHttpAddRequestHeader(req, "User-Agent", ua, HEADER_OVERWRITE);
if (ref && *ref)
sceHttpAddRequestHeader(req, "Referer", ref, HEADER_OVERWRITE);
sceHttpAddRequestHeader(req, "Accept", "*/*", HEADER_OVERWRITE);
sceHttpAddRequestHeader(req, "Cache-Control", "no-cache", HEADER_OVERWRITE);
sceHttpAddRequestHeader(req, "Pragma", "no-cache", HEADER_OVERWRITE);
}
static bool par_cancelled(parallel *p)
{
return p->serial && vlc_ps5_stream_cancelled && vlc_ps5_stream_cancelled(p->serial);
}
/* One piece, from where it got to: true when it's all in. */
static bool par_fetch(parallel *p, int w, unsigned idx)
{
uint64_t start = idx * PAR_CHUNK, end = start + PAR_CHUNK;
if (end > p->size)
end = p->size;
pthread_mutex_lock(&p->lock);
uint64_t from = start + p->filled[idx];
pthread_mutex_unlock(&p->lock);
if (from >= end)
return true;
char *host_header = NULL;
char *direct = direct_url(p->url, &host_header);
const char *url = direct ? direct : p->url;
int conn = sceHttpCreateConnectionWithURL(http_template, url, 1);
int req = conn >= 0 ? sceHttpCreateRequestWithURL(conn, METHOD_GET, url, 0) : -1;
free(direct);
bool ok = false;
if (req >= 0) {
add_headers(req, host_header, p->ua, p->ref);
char range[64];
snprintf(range, sizeof(range), "bytes=%" PRIu64 "-%" PRIu64, from, end - 1);
sceHttpAddRequestHeader(req, "Range", range, HEADER_OVERWRITE);
pthread_mutex_lock(&p->lock);
p->reqs[w] = p->stop ? -1 : req;
bool stop = p->stop;
pthread_mutex_unlock(&p->lock);
int status = 0;
if (!stop && sceHttpSendRequest(req, NULL, 0) >= 0 && sceHttpGetStatusCode(req, &status) >= 0 &&
(status == 206 || (status == 200 && from == 0))) {
ok = true;
while (from < end) {
size_t want = end - from > (64u << 10) ? (64u << 10) : (size_t)(end - from);
int n = sceHttpReadData(req, p->buf + from, want);
if (n <= 0) {
ok = false;
break;
}
from += (uint64_t)n;
pthread_mutex_lock(&p->lock);
p->filled[idx] += (uint32_t)n;
p->fetched += (uint64_t)n;
stop = p->stop;
pthread_cond_broadcast(&p->cond);
pthread_mutex_unlock(&p->lock);
if (stop || par_cancelled(p)) {
ok = false;
break;
}
}
}
pthread_mutex_lock(&p->lock);
p->reqs[w] = -1;
pthread_mutex_unlock(&p->lock);
sceHttpDeleteRequest(req);
}
if (conn >= 0)
sceHttpDeleteConnection(conn);
free(host_header);
return ok;
}
struct par_worker {
parallel *p;
int w;
};
static void *par_run(void *arg)
{
struct par_worker *pw = arg;
parallel *p = pw->p;
int w = pw->w;
free(pw);
for (;;) {
pthread_mutex_lock(&p->lock);
if (p->stop || p->next >= p->chunks) {
pthread_mutex_unlock(&p->lock);
break;
}
unsigned idx = p->next++;
pthread_mutex_unlock(&p->lock);
bool ok = false;
for (int tries = 0; tries < 3 && !ok && !p->stop && !par_cancelled(p); tries++)
ok = par_fetch(p, w, idx);
if (!ok) {
pthread_mutex_lock(&p->lock);
p->failed[idx] = true;
pthread_cond_broadcast(&p->cond);
pthread_mutex_unlock(&p->lock);
}
}
return NULL;
}
static void par_free(parallel *p)
{
if (!p)
return;
pthread_mutex_lock(&p->lock);
p->stop = true;
for (int w = 0; w < PAR_WORKERS; w++)
if (p->reqs[w] >= 0)
sceHttpAbortRequest(p->reqs[w]);
pthread_cond_broadcast(&p->cond);
pthread_mutex_unlock(&p->lock);
for (int i = 0; i < p->nthreads; i++)
pthread_join(p->threads[i], NULL);
pthread_mutex_destroy(&p->lock);
pthread_cond_destroy(&p->cond);
free(p->buf);
free(p->filled);
free(p->failed);
free(p->url);
free(p->ua);
free(p->ref);
free(p);
}
static parallel *par_start(stream_t *access, uint64_t size, long serial)
{
parallel *p = calloc(1, sizeof(*p));
if (!p)
return NULL;
p->size = size;
p->chunks = (unsigned)((size + PAR_CHUNK - 1) / PAR_CHUNK);
p->buf = malloc(size);
p->filled = calloc(p->chunks, sizeof(*p->filled));
p->failed = calloc(p->chunks, sizeof(*p->failed));
p->url = strdup(access->psz_url);
p->ua = var_InheritString(access, "http-user-agent");
p->ref = var_InheritString(access, "http-referrer");
p->serial = serial;
for (int w = 0; w < PAR_WORKERS; w++)
p->reqs[w] = -1;
pthread_mutex_init(&p->lock, NULL);
pthread_cond_init(&p->cond, NULL);
if (!p->buf || !p->filled || !p->failed || !p->url) {
par_free(p);
return NULL;
}
int workers = p->chunks < PAR_WORKERS ? (int)p->chunks : PAR_WORKERS;
for (int w = 0; w < workers; w++) {
struct par_worker *pw = malloc(sizeof(*pw));
if (!pw)
break;
pw->p = p;
pw->w = w;
if (pthread_create(&p->threads[p->nthreads], NULL, par_run, pw) != 0) {
free(pw);
break;
}
p->nthreads++;
}
if (!p->nthreads) {
par_free(p);
return NULL;
}
return p;
}
/* VLC's read from the pieces: what's in at `pos`, waiting for it if needed. */
static ssize_t par_read(stream_t *access, ps5http_sys *sys, void *buf, size_t len)
{
parallel *p = sys->par;
if (sys->pos >= p->size)
return 0;
unsigned idx = (unsigned)(sys->pos / PAR_CHUNK);
uint64_t within = sys->pos % PAR_CHUNK;
pthread_mutex_lock(&p->lock);
while (p->filled[idx] <= within && !p->failed[idx] && !p->stop) {
pthread_mutex_unlock(&p->lock);
if (vlc_killed() || cancelled(sys))
return 0;
pthread_mutex_lock(&p->lock);
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
ts.tv_nsec += 50 * 1000000;
if (ts.tv_nsec >= 1000000000) {
ts.tv_sec++;
ts.tv_nsec -= 1000000000;
}
if (p->filled[idx] <= within && !p->failed[idx])
pthread_cond_timedwait(&p->cond, &p->lock, &ts);
}
size_t have = p->filled[idx] > within ? (size_t)(p->filled[idx] - within) : 0;
bool failed = !have && p->failed[idx];
pthread_mutex_unlock(&p->lock);
if (failed) {
PLOG("a piece at %" PRIu64 " couldn't be fetched", sys->pos);
return 0;
}
if (len > have)
len = have;
memcpy(buf, p->buf + sys->pos, len);
sys->pos += len;
sys->read_bytes += len;
(void)access;
return (ssize_t)len;
}
/* Sends a GET from `offset`; fills status, size, type, redirect. */
static int send_request(stream_t *access, uint64_t offset)
{
@@ -156,17 +538,26 @@ static int send_request(stream_t *access, uint64_t offset)
sys->redirect = NULL;
sys->eof = false;
sys->conn = sceHttpCreateConnectionWithURL(http_template, access->psz_url, 1);
char *host_header = NULL;
char *direct = direct_url(access->psz_url, &host_header);
const char *url = direct ? direct : access->psz_url;
sys->conn = sceHttpCreateConnectionWithURL(http_template, url, 1);
if (sys->conn < 0) {
PLOG("connection failed (%#x)", (unsigned)sys->conn);
free(direct);
free(host_header);
return VLC_EGENERIC;
}
sys->req = sceHttpCreateRequestWithURL(sys->conn, METHOD_GET, access->psz_url, 0);
sys->req = sceHttpCreateRequestWithURL(sys->conn, METHOD_GET, url, 0);
free(direct);
if (sys->req < 0) {
PLOG("request failed (%#x)", (unsigned)sys->req);
free(host_header);
drop_request(sys);
return VLC_EGENERIC;
}
if (host_header)
sceHttpAddRequestHeader(sys->req, "Host", host_header, HEADER_OVERWRITE);
char *ua = var_InheritString(access, "http-user-agent");
char *ref = var_InheritString(access, "http-referrer");
if (ua && *ua)
@@ -176,6 +567,11 @@ static int send_request(stream_t *access, uint64_t offset)
free(ua);
free(ref);
sceHttpAddRequestHeader(sys->req, "Accept", "*/*", HEADER_OVERWRITE);
/* never a cached copy: a live playlist read again came back the same
* (BBC, 2026-10-13: 20 s on, the same ten segments), VLC ran out of
* segments and the channel stopped */
sceHttpAddRequestHeader(sys->req, "Cache-Control", "no-cache", HEADER_OVERWRITE);
sceHttpAddRequestHeader(sys->req, "Pragma", "no-cache", HEADER_OVERWRITE);
if (offset > 0) {
char range[48];
snprintf(range, sizeof(range), "bytes=%" PRIu64 "-", offset);
@@ -190,16 +586,57 @@ static int send_request(stream_t *access, uint64_t offset)
* certificate, its ciphers). VLC's own http takes this link; the
* service stays for the others. Anything else: the service itself. */
bool ssl = ((unsigned)r & 0xFFFF0000u) == 0x80950000u;
/* 0x8043xxxx: this server's connection (down, refused, timed out):
* this link only too. Taken as the service's, one dead channel sent
* every stream after it to VLC's own http (2026-10-13). */
bool net = ((unsigned)r & 0xFFFF0000u) == 0x80430000u ||
((unsigned)r & 0xFFFF0000u) == 0x80410000u; /* 0x8041: the network's errno */
/* stopped on purpose (the player left, a search was typed over):
* nothing wrong with anything */
bool stopped = vlc_killed() || cancelled(sys);
int in_row = stopped || ssl || net ? 0 : atomic_fetch_add(&service_failures, 1) + 1;
PLOG("sending failed (%#x): %s", (unsigned)r,
ssl ? "SSL refused this server, VLC's own http takes this link" : "VLC's own http is used from now on");
if (!ssl)
stopped ? "stopped"
: ssl ? "SSL refused this server, VLC's own http takes this link"
: net ? "connection failed, VLC's own http tries this link"
: in_row >= 3 ? "third time in a row: VLC's own http is used from now on"
: "VLC's own http takes this link");
if (in_row >= 3)
service_refused = true;
if (ssl)
refuse_host(access->psz_url, true);
/* https by name refused (the console's DNS): this one's https links
* straight to VLC's own http from now on, no failed try first */
if (net && !strncasecmp(access->psz_url, "https://", 8) &&
((unsigned)r & 0xFFFF0000u) == 0x80410000u && atomic_fetch_add(&https_name_refusals, 1) + 1 == 3) {
vlc_mutex_lock(&refused_lock);
https_refused = true;
vlc_mutex_unlock(&refused_lock);
PLOG("https refused 3 times by name (the console's DNS?): VLC's own http takes every https link");
}
free(host_header);
drop_request(sys);
return VLC_EGENERIC;
}
atomic_store(&service_failures, 0);
int status = 0;
sceHttpGetStatusCode(sys->req, &status);
sys->status = status;
if (host_header && (status == 400 || status == 403 || status == 404 || status == 421)) {
/* the server wanted its name, not just its address (the Host header
* didn't get through): no more address-only requests this session */
PLOG("HTTP %d to %s by address: VLC's own http takes it, no more by address", status, host_header);
atomic_store(&direct_broken, true);
free(host_header);
drop_request(sys);
return VLC_EGENERIC;
}
if (host_header && !sys->logged_direct) {
sys->logged_direct = true;
PLOG("%s by the app's own name lookup", host_header);
}
free(host_header);
host_header = NULL;
char *headers = NULL;
size_t hlen = 0;
@@ -226,11 +663,32 @@ static int send_request(stream_t *access, uint64_t offset)
static ssize_t Read(stream_t *access, void *buf, size_t len)
{
ps5http_sys *sys = access->p_sys;
if (sys->par)
return par_read(access, sys, buf, len);
if (sys->eof || sys->req < 0)
return 0;
if (cancelled(sys)) {
/* the player left this video: no waiting for the rest of a slow
* server's segment (the next video waited 10 s, 2026-10-18) */
if (!sys->cancel_logged)
PLOG("stopped by the player: connection ended");
sys->cancel_logged = true;
sys->eof = true;
return 0;
}
/* A little at a time: VLC's prefetch asks for 16 MiB at the start, and a
* live channel (no size, 4-8 Mbit/s) took 15-33 s to fill that before the
* first picture (issue #11's log). */
size_t most = sys->size && sys->can_seek ? ((size_t)1 << 20) : ((size_t)64 << 10);
if (len > most)
len = most;
vlc_interrupt_register(abort_on_interrupt, &sys->req);
mtime_t t0 = mdate();
int n = sceHttpReadData(sys->req, buf, len);
sys->read_wait += mdate() - t0;
vlc_interrupt_unregister();
if (n > 0)
sys->read_bytes += (uint64_t)n;
if (n <= 0) {
if (n < 0)
PLOG("read failed (%#x)", (unsigned)n);
@@ -244,6 +702,10 @@ static ssize_t Read(stream_t *access, void *buf, size_t len)
static int Seek(stream_t *access, uint64_t offset)
{
ps5http_sys *sys = access->p_sys;
if (sys->par) {
sys->pos = offset < sys->size ? offset : sys->size;
return VLC_SUCCESS;
}
if (!sys->can_seek)
return VLC_EGENERIC;
if (sys->size && offset >= sys->size) {
@@ -300,6 +762,19 @@ static void Close(vlc_object_t *obj)
{
stream_t *access = (stream_t *)obj;
ps5http_sys *sys = access->p_sys;
/* each download's speed: a live channel's segments must come faster than
* they play (a 4 s piece in under 4 s), or it freezes */
if (sys->read_bytes >= (256u << 10) && vlc_ps5_log_details && vlc_ps5_log_details()) {
double secs = (mdate() - sys->opened_at) / 1e6, wait = sys->read_wait / 1e6;
if (sys->par)
PLOG("read %" PRIu64 " KiB in %.2f s (%.0f KiB/s; %d connections)", sys->read_bytes >> 10, secs,
secs > 0 ? (sys->read_bytes >> 10) / secs : 0.0, sys->par->nthreads);
else
PLOG("read %" PRIu64 " KiB in %.2f s (%.0f KiB/s; %.2f s waiting for the network)",
sys->read_bytes >> 10, secs, secs > 0 ? (sys->read_bytes >> 10) / secs : 0.0, wait);
}
par_free(sys->par);
sys->par = NULL;
drop_request(sys);
free(sys->content_type);
free(sys->redirect);
@@ -310,7 +785,7 @@ static int Open(vlc_object_t *obj)
{
stream_t *access = (stream_t *)obj;
pthread_once(&service_once, start_service);
if (http_template < 0 || service_refused)
if (http_template < 0 || service_refused || host_refused(access->psz_url))
return VLC_EGENERIC; /* VLC's own http module takes over */
ps5http_sys *sys = calloc(1, sizeof(*sys));
@@ -318,6 +793,13 @@ static int Open(vlc_object_t *obj)
return VLC_ENOMEM;
sys->conn = sys->req = -1;
access->p_sys = sys;
sys->serial = (long)var_InheritInteger(access, "ps5http-serial");
sys->opened_at = mdate();
if (cancelled(sys)) {
free(sys);
access->p_sys = NULL;
return VLC_EGENERIC; /* a segment for a video already stopped */
}
PLOG("open %s", access->psz_url);
if (send_request(access, 0) != VLC_SUCCESS)
@@ -346,6 +828,11 @@ static int Open(vlc_object_t *obj)
PLOG("HTTP %d", sys->status);
goto error;
}
if (sys->status == 200 && sys->can_seek && sys->size >= PAR_MIN && sys->size <= PAR_MAX) {
sys->par = par_start(access, sys->size, sys->serial);
if (sys->par)
drop_request(sys); /* the pieces have their own connections */
}
access->pf_read = Read;
access->pf_block = NULL;
access->pf_seek = Seek;
@@ -365,5 +852,9 @@ vlc_module_begin()
set_subcategory(SUBCAT_INPUT_ACCESS)
set_capability("access", 300)
add_shortcut("http", "https")
/* the player's number for the open (see vlc_ps5_stream_cancelled) */
add_integer("ps5http-serial", 0, "Player open", NULL, true)
change_private()
change_safe()
set_callbacks(Open, Close)
vlc_module_end()
+840 -63
View File
File diff suppressed because it is too large. Load diff
+3 -1
View File
@@ -7,7 +7,9 @@
# bash ps5/radv-patches.sh
#
# patches/mesa/0001: HDR10 output (the framebuffers switched in place, the
# output's dynamic range read back) for src/player.cc.
# output's dynamic range read back) for src/player.cc, and the output's
# refresh for films (23.976 / 24 / 50 Hz) for src/main.cc. One patch per file:
# each run starts the file again from the fork's original.
set -euo pipefail
here=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
repo=$(dirname "$here")
+1 -1
View File
@@ -10,7 +10,7 @@
"conceptId": "85300",
"contentBadgeType": 1,
"contentId": "UP9000-PPSA85300_00-VLCFORPS50000001",
"contentVersion": "01.000.005",
"contentVersion": "01.000.006",
"downloadDataSize": 256,
"gameIntent": {
"permittedIntents": [
+1 -1
View File
@@ -10,7 +10,7 @@
"conceptId": "85300",
"contentBadgeType": 2,
"contentId": "UP9000-PPSA85300_00-VLCFORPS50000001",
"contentVersion": "01.000.005",
"contentVersion": "01.000.006",
"downloadDataSize": 256,
"localizedParameters": {
"defaultLanguage": "en-US",
+38 -1
View File
@@ -28,6 +28,7 @@
/* The console's Vulkan driver, with patches/mesa/0001 (absent elsewhere). */
extern "C" int vlcps5_wsi_videoout_set_hdr(int enable) __attribute__((weak));
extern "C" int vlcps5_wsi_videoout_dynamic_range(void) __attribute__((weak));
extern "C" int vlcps5_wsi_videoout_set_refresh(int mode, int *period_us) __attribute__((weak));
Gfx gfx;
@@ -617,6 +618,23 @@ bool gfx_set_hdr(bool on)
return true;
}
bool gfx_set_refresh(int mode, int *period_us)
{
*period_us = 0;
if (!vlcps5_wsi_videoout_set_refresh)
return false;
/* nothing in flight while the output changes */
vkDeviceWaitIdle(gfx.device);
return vlcps5_wsi_videoout_set_refresh(mode, period_us) == 0;
}
int gfx_output_range()
{
if (vlcps5_wsi_videoout_dynamic_range)
return vlcps5_wsi_videoout_dynamic_range();
return gfx.offscreen && getenv("VLCPS5_HDR_TEST") ? atoi(getenv("VLCPS5_HDR_TEST")) : -1;
}
void gfx_begin_ui_pass(VkCommandBuffer cmd)
{
begin(cmd, hdr.ui_pass, hdr.ui_fb, 0.0f); /* clear: transparent */
@@ -817,7 +835,26 @@ bool gfx_image(uint32_t w, uint32_t h, VkFormat format, VkImageUsageFlags usage,
ai.memoryTypeIndex = gfx_memory_type(req.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
if (ai.memoryTypeIndex == UINT32_MAX)
ai.memoryTypeIndex = gfx_memory_type(req.memoryTypeBits, 0);
CHECK(vkAllocateMemory(gfx.device, &ai, nullptr, memory));
VkResult r = vkAllocateMemory(gfx.device, &ai, nullptr, memory);
/* The video memory full (a 4K HDR film's decoder holds 840 MiB of it):
* any other kind the image can live in. Failing, a 4K video stayed
* black for the whole of its first open (2026-10-18). */
for (uint32_t t = 0; r != VK_SUCCESS && t < 32; t++) {
if (!(req.memoryTypeBits & (1u << t)) || t == ai.memoryTypeIndex)
continue;
VkMemoryAllocateInfo other = ai;
other.memoryTypeIndex = t;
r = vkAllocateMemory(gfx.device, &other, nullptr, memory);
if (r == VK_SUCCESS)
fprintf(stderr, "gfx: %ux%u image: video memory full, memory type %u instead\n", w, h, t);
}
if (r != VK_SUCCESS) {
fprintf(stderr, "gfx: no memory for a %ux%u image (%llu KiB): %d\n", w, h,
(unsigned long long)(req.size >> 10), r);
vkDestroyImage(gfx.device, *image, nullptr);
*image = VK_NULL_HANDLE;
return false;
}
CHECK(vkBindImageMemory(gfx.device, *image, *memory, 0));
VkImageViewCreateInfo vi = { VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO };
vi.image = *image;
+5
View File
@@ -46,6 +46,11 @@ void gfx_begin_pass(VkCommandBuffer cmd);
void gfx_end_frame(VkCommandBuffer cmd);
/* HDR10 output on or off (waits for the GPU); false if it couldn't change. */
bool gfx_set_hdr(bool on);
/* What the console sends to the TV now: 1 SDR, 2 HDR, other unknown. */
int gfx_output_range();
/* The output's refresh (VideoOut modes: 2 = 23.976 Hz, 3 = 24, 9 = 50,
* 1 = the default); the vblank period measured after, in microseconds. */
bool gfx_set_refresh(int mode, int *period_us);
/* With HDR10 output on, a frame is: the UI pass (ImGui into its own layer),
* then the HDR pass (the video, then gfx_draw_ui_layer), then gfx_end_frame. */
void gfx_begin_ui_pass(VkCommandBuffer cmd);
+58 -1
View File
@@ -8,7 +8,9 @@
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include <math.h>
#include <stdio.h>
#include <string>
#include <stdlib.h>
#include "backends/imgui_impl_vulkan.h"
@@ -97,13 +99,68 @@ int main(void)
/* HDR10 output while an HDR video shows (once a refusal, not again
* until the wish changes) */
/* The wish has to hold a moment first: while a video starts (its
* picture set up, set up again) it came and went, and each switch
* blanks the screen (on, off, on, off in a second: a 2026-10-13 log). */
static bool hdr_refused;
static double wish_since;
static bool wish_last;
bool want_hdr = video_wants_hdr_output();
if (want_hdr != gfx.hdr && !(want_hdr && hdr_refused))
if (want_hdr != wish_last) {
wish_last = want_hdr;
wish_since = plat_time();
}
double hold = want_hdr ? 0.4 : 1.0;
if (want_hdr != gfx.hdr && !(want_hdr && hdr_refused) && plat_time() - wish_since >= hold)
hdr_refused = !gfx_set_hdr(want_hdr);
if (!want_hdr)
hdr_refused = false;
/* Match frame rate: the output's refresh follows the film playing
* (23.976 / 24 / 50 Hz), and goes back to the default after. Asked
* once a picture is up (the rate is known then); a mode the TV
* doesn't really run at (the vblanks say) is put back and not asked
* again for that video. */
{
static int mode_now = 1;
static std::string refused_for;
int want = 1;
const VideoInfo &vi = player_video_info();
if (player_match_rate() && player_active() && video_has_picture() && vi.fps > 0 &&
refused_for != player_current_path()) {
float f = vi.fps;
if (fabsf(f - 23.976f) < 0.01f)
want = 2;
else if (fabsf(f - 24.0f) < 0.01f)
want = 3;
else if (fabsf(f - 25.0f) < 0.05f || fabsf(f - 50.0f) < 0.1f)
want = 9;
}
if (want != mode_now) {
int period = 0;
bool ok = gfx_set_refresh(want, &period);
static const struct { int mode, us; } expect[] = { { 1, 16683 }, { 2, 41708 }, { 3, 41667 }, { 9, 20000 } };
int us = 0;
for (const auto &e : expect)
if (e.mode == want)
us = e.us;
if (ok && period > 0 && fabs(period - us) > us * 0.05) {
fprintf(stderr, "main: output mode %d taken but a vblank every %d us (not %d): default again\n",
want, period, us);
gfx_set_refresh(1, &period);
ok = false;
}
if (ok || want == 1) {
mode_now = ok ? want : 1;
} else {
refused_for = player_current_path();
if (mode_now != 1 && gfx_set_refresh(1, &period))
mode_now = 1;
}
fprintf(stderr, "main: refresh %s for %.3f fps\n", ok ? "matched" : "not matched", vi.fps);
}
}
VkCommandBuffer cmd = gfx_begin_frame();
if (!cmd)
continue;
+249
View File
@@ -15,6 +15,7 @@
#include <vlc/vlc.h>
#include "osub.h"
#include "platform.h"
#include "player.h"
@@ -391,6 +392,199 @@ bool list_menu(const std::string &url)
return true;
}
/* ---- search: every free TV channel and radio stations by name, fetched in
* the background while Search is typed into (not through Browse) ---- */
struct Fetch {
libvlc_media_t *m = nullptr;
bool loading = false;
std::string url;
std::vector<NetEntry> got;
};
Fetch radio_found;
int search_serial; /* changes when search results arrive */
/* Every free TV channel for Search: index.m3u downloaded and read here, as
* names, addresses and options only. Kept as VLC media (11,000 of them, each
* with its locks), it and the same list open in Browse ran the console out
* of mutexes: VLC aborts when it can't make one (crash, 2026-10-13). */
std::vector<NetEntry> tv_list; /* main thread */
std::mutex tv_lock;
std::vector<NetEntry> tv_arrived; /* from the download thread */
bool tv_has_arrived, tv_loading, tv_started;
std::thread tv_thread;
std::vector<NetEntry> parse_m3u(const std::string &text)
{
std::vector<NetEntry> out;
std::string name;
std::vector<std::string> opts;
size_t at = 0;
while (at < text.size()) {
size_t eol = text.find('\n', at);
if (eol == std::string::npos)
eol = text.size();
std::string line = text.substr(at, eol - at);
at = eol + 1;
while (!line.empty() && (line.back() == '\r' || line.back() == ' '))
line.pop_back();
if (line.empty())
continue;
if (line.compare(0, 8, "#EXTINF:") == 0) {
/* the name: after the first comma outside the attributes' quotes */
bool quoted = false;
size_t comma = std::string::npos;
for (size_t i = 8; i < line.size(); i++) {
if (line[i] == '"')
quoted = !quoted;
else if (line[i] == ',' && !quoted) {
comma = i;
break;
}
}
name = comma == std::string::npos ? "" : line.substr(comma + 1);
while (!name.empty() && name[0] == ' ')
name.erase(0, 1);
} else if (line.compare(0, 11, "#EXTVLCOPT:") == 0) {
opts.push_back(":" + line.substr(11));
} else if (line[0] != '#') {
NetEntry e;
e.url = line;
e.name = name.empty() ? line : name;
e.dir = false;
e.options.swap(opts);
out.push_back(std::move(e));
name.clear();
opts.clear();
}
}
return out;
}
void tv_start_download()
{
if (tv_started)
return;
tv_started = true;
tv_loading = true;
search_serial++;
std::string url = std::string(TV_LISTS) + "index.m3u";
fprintf(stderr, "net: search fetching %s\n", url.c_str());
tv_thread = std::thread([url] {
std::string body, error;
std::vector<NetEntry> got;
if (web_https_get(url, body, error))
got = parse_m3u(body);
fprintf(stderr, "net: search got %zu channels (%zu bytes)%s%s\n", got.size(), body.size(),
error.empty() ? "" : ": ", error.c_str());
std::lock_guard<std::mutex> g(tv_lock);
tv_arrived.swap(got);
tv_has_arrived = true;
});
}
std::string search_query, radio_query; /* typed; last asked of radio-browser */
double search_typed_at;
void fetch_clear(Fetch &f)
{
if (f.m) {
if (f.loading)
libvlc_media_parse_stop(f.m);
libvlc_media_release(f.m);
f.m = nullptr;
}
f.loading = false;
for (NetEntry &e : f.got)
if (e.media)
libvlc_media_release(e.media);
f.got.clear();
}
void fetch_start(Fetch &f, const std::string &url)
{
fetch_clear(f);
libvlc_instance_t *vlc = player_vlc();
f.url = url;
f.m = vlc ? libvlc_media_new_location(vlc, url.c_str()) : nullptr;
if (f.m && libvlc_media_parse_with_options(f.m, libvlc_media_parse_network, 20000) == 0) {
f.loading = true;
fprintf(stderr, "net: search fetching %s\n", url.c_str());
}
search_serial++;
}
void fetch_poll(Fetch &f)
{
if (!f.loading || libvlc_media_get_parsed_status(f.m) == 0)
return;
f.loading = false;
if (libvlc_media_list_t *subs = libvlc_media_subitems(f.m)) {
libvlc_media_list_lock(subs);
for (int i = 0; i < libvlc_media_list_count(subs); i++) {
libvlc_media_t *m = libvlc_media_list_item_at_index(subs, i);
if (!m)
continue;
char *mrl = libvlc_media_get_mrl(m), *title = libvlc_media_get_meta(m, libvlc_meta_Title);
if (mrl && libvlc_media_get_type(m) != libvlc_media_type_directory) {
NetEntry e;
e.url = mrl;
e.name = title ? title : e.url.substr(e.url.rfind('/') + 1);
e.dir = false;
e.media = m;
libvlc_media_retain(m);
f.got.push_back(e);
}
free(mrl);
free(title);
libvlc_media_release(m);
}
libvlc_media_list_unlock(subs);
libvlc_media_list_release(subs);
}
fprintf(stderr, "net: search got %zu from %s\n", f.got.size(), f.url.c_str());
search_serial++;
}
std::string url_encode(const std::string &s)
{
std::string out;
for (unsigned char c : s) {
if (isalnum(c) || c == '-' || c == '.' || c == '_') {
out += (char)c;
} else {
char h[4];
snprintf(h, sizeof(h), "%%%02X", c);
out += h;
}
}
return out;
}
void search_update()
{
{
std::lock_guard<std::mutex> g(tv_lock);
if (tv_has_arrived) {
tv_has_arrived = false;
tv_list.swap(tv_arrived);
tv_loading = false;
if (tv_list.empty())
tv_started = false; /* failed: asked again the next time Search is used */
search_serial++;
}
}
if (!tv_loading && tv_thread.joinable())
tv_thread.join();
fetch_poll(radio_found);
/* radio-browser is asked once the typing pauses */
if (search_query.size() >= 2 && search_query != radio_query && plat_time() - search_typed_at > 0.6) {
radio_query = search_query;
fetch_start(radio_found, std::string(RADIO_LISTS) + "byname/" + url_encode(search_query) +
"?limit=40&order=clickcount&reverse=true&hidebroken=true");
}
}
} // namespace
void net_init()
@@ -430,6 +624,10 @@ void net_shutdown()
upnp_starter.join();
net_list_stop();
clear_entries();
if (tv_thread.joinable())
tv_thread.join();
tv_list.clear();
fetch_clear(radio_found);
if (upnp) {
libvlc_media_discoverer_stop(upnp);
libvlc_media_discoverer_release(upnp);
@@ -455,6 +653,7 @@ void net_update()
libvlc_dialog_dismiss(id);
if (list_state == NET_LOADING && libvlc_media_get_parsed_status(listing) != 0)
finish_listing();
search_update();
}
std::vector<NetServer> net_servers()
@@ -497,6 +696,56 @@ void net_forget_server(const std::string &url)
store_saved();
}
void net_search_typed(const std::string &query)
{
std::string q = query;
while (!q.empty() && q.back() == ' ')
q.pop_back();
while (!q.empty() && q[0] == ' ')
q.erase(0, 1);
tv_start_download(); /* once a session (again if it failed) */
if (q != search_query) {
search_query = q;
search_typed_at = plat_time();
if (q.size() < 2) {
fetch_clear(radio_found);
radio_query.clear();
}
}
}
bool net_search_busy()
{
return tv_loading || radio_found.loading ||
(search_query.size() >= 2 && search_query != radio_query);
}
int net_search_serial()
{
return search_serial;
}
const std::vector<NetEntry> &net_search_tv()
{
return tv_list;
}
const std::vector<NetEntry> &net_search_radio()
{
return radio_found.got;
}
libvlc_media_t *net_search_media(const std::string &url)
{
for (NetEntry &e : radio_found.got)
if (e.url == url && e.media) {
libvlc_media_retain(e.media);
return e.media;
}
return nullptr;
}
bool net_is_saved_server(const std::string &url)
{
for (const Saved &s : saved)
+10
View File
@@ -23,6 +23,7 @@ struct NetEntry {
std::string url; /* no login in it: net_login_options() gives it */
bool dir;
libvlc_media_t *media = nullptr; /* VLC's own item (keeps a list's #EXTVLCOPT) */
std::vector<std::string> options; /* or a list's #EXTVLCOPT as media options (":http-referrer=...") */
};
enum NetListState { NET_IDLE, NET_LOADING, NET_READY, NET_FAILED };
@@ -60,6 +61,15 @@ libvlc_media_t *net_take_media(const std::string &url);
/* The media options that log in to an address's saved share (":smb-user=..."). */
std::vector<std::string> net_login_options(const std::string &url);
/* Search, as it's typed: every free TV channel (the whole list, fetched once a
* session) and radio stations by name (asked once the typing pauses). */
void net_search_typed(const std::string &query);
bool net_search_busy();
int net_search_serial(); /* changes when results arrive */
const std::vector<NetEntry> &net_search_tv();
const std::vector<NetEntry> &net_search_radio();
libvlc_media_t *net_search_media(const std::string &url); /* with a reference, or null */
/* What VLC is asking (login / question), answered from the interface. */
NetAsk net_ask();
void net_answer_login(const std::string &user, const std::string &password, bool remember);
+16
View File
@@ -584,6 +584,22 @@ bool start()
} // namespace
/* A plain HTTPS download (redirects followed), for other parts of the app. */
bool web_https_get(const std::string &url, std::string &body, std::string &error)
{
Reply r = https_get(url);
if (!r.error.empty()) {
error = r.error;
return false;
}
if (r.status != 200) {
error = "HTTP " + std::to_string(r.status);
return false;
}
body.swap(r.body);
return true;
}
bool osub_has_key()
{
return !pref_str("osub_key", "").empty();
+3
View File
@@ -34,3 +34,6 @@ OsubState osub_state();
std::vector<SubResult> osub_results();
std::string osub_message(); /* what went wrong, or "12 downloads left today" */
std::string osub_saved_path(); /* the file a download wrote, once */
/* A plain HTTPS GET (redirects followed; blocking: a background thread's). */
bool web_https_get(const std::string &url, std::string &body, std::string &error);
+1 -1
View File
@@ -23,7 +23,7 @@ extern "C" {
#endif
#define VLC_PS5_TITLE_ID "PPSA85300"
#define VLC_PS5_VERSION "0.4"
#define VLC_PS5_VERSION "0.4.5"
/* DualSense buttons (scePadRead's layout, as QUICK3 and PS5_vkQuake use it),
* plus the triggers past half as buttons. */
+20 -1
View File
@@ -10,6 +10,7 @@
* options touchpad)
* <frame> hold <button> <frames>
* <frame> shot <file.png> saves that frame
* <frame> type <text> the on-screen keyboard gets it (_ for a space)
* <frame> quit
*
* SPDX-License-Identifier: GPL-2.0-or-later
@@ -27,7 +28,7 @@
typedef struct {
uint64_t frame;
enum { STEP_HOLD, STEP_SHOT, STEP_QUIT, STEP_STICK } kind;
enum { STEP_HOLD, STEP_SHOT, STEP_QUIT, STEP_STICK, STEP_TYPE } kind;
uint32_t button;
float rx, ry; /* STEP_STICK: the right stick, held for frames */
uint64_t frames;
@@ -87,6 +88,11 @@ static void load_script(void)
s->frames = frames;
} else if (!strcmp(word, "shot") && sscanf(line, "%*u %*s %255s", s->path) == 1) {
s->kind = STEP_SHOT;
} else if (!strcmp(word, "type") && sscanf(line, "%*u %*s %255s", s->path) == 1) {
s->kind = STEP_TYPE;
for (char *c = s->path; *c; c++)
if (*c == '_')
*c = ' ';
} else if (!strcmp(word, "quit")) {
s->kind = STEP_QUIT;
quit_frame = frame;
@@ -255,3 +261,16 @@ bool plat_script_done(uint64_t frame)
{
return frame >= quit_frame;
}
/* Script "type" steps due by now, once (the interface's keyboard reads it). */
const char *plat_take_typed(void)
{
static int next;
for (; next < nsteps; next++) {
if (steps[next].frame > frame_now)
break;
if (steps[next].kind == STEP_TYPE)
return steps[next++].path;
}
return NULL;
}
+5
View File
@@ -166,6 +166,7 @@ static int boot_setting(const char *key, int fallback)
* hw_decode=0 not at all. */
int sceSysmoduleLoadModule(uint16_t id);
void vlc_ps5_vdec_prepare(void) __attribute__((weak)); /* ps5/modules/ps5vdec.c */
void vlc_ps5_vdec_probe(const char *when) __attribute__((weak));
#define SYSMODULE_VIDEODEC2 207
static bool vdec_ready;
static double jb_wait = 2.5; /* seconds the Helper has to answer */
@@ -193,6 +194,8 @@ static void load_video_decoder(void)
/* its compute queue now too, while still in the sandbox */
if (vdec_ready && vlc_ps5_vdec_prepare)
vlc_ps5_vdec_prepare();
if (vdec_ready && vlc_ps5_vdec_probe)
vlc_ps5_vdec_probe(plat_full_access() ? "after full access" : "before full access");
}
int vlc_ps5_vdec_available(void)
@@ -527,6 +530,8 @@ void plat_init(void)
request_full_access();
if (hw_decode && vdec_load != 1)
load_video_decoder();
else if (vdec_ready && vlc_ps5_vdec_probe && plat_full_access())
vlc_ps5_vdec_probe("after full access");
use_app_dir();
}
+448 -63
View File
@@ -54,6 +54,10 @@
namespace {
extern bool log_debug;
/* The picture diagnostics (2026-10-18): only with debug=1 in settings.txt */
#define DIAG(...) do { if (log_debug) fprintf(stderr, __VA_ARGS__); } while (0)
libvlc_instance_t *vlc;
libvlc_media_player_t *mp;
std::string current_path;
@@ -76,6 +80,7 @@ std::atomic<bool> vdec_failed{ false };
std::atomic<int> vdec_active{ 0 }; /* decoders open on the console's decoder */
/* how long a video takes to show its first picture (the log) */
double opened_at;
int64_t opened_from_ms; /* where the open started */
bool first_picture_wait;
std::string software_path;
struct Health {
@@ -149,6 +154,9 @@ struct Colour {
bool wide = false; /* BT.2020 primaries */
float peak = 1000; /* the picture's brightest, in nits (HDR) */
bool full_range = false; /* 0-255 (VLC says so; NV12 has no "J" name) */
/* the decoder's buffer and the part of it that is picture (1280x720 in
* 1280x736): patches/0018 has vmem keep it, the shader crops */
uint32_t buf_w = 0, buf_h = 0, vis_w = 0, vis_h = 0;
};
/* HDR's reference white (BT.2408): what shows as the TV's white in SDR */
const float SDR_WHITE = 203;
@@ -163,6 +171,7 @@ struct Format {
* interleaved in one plane; P010 keeps its 10 bits at the top */
bool two_planes;
Colour colour;
uint32_t vis_w, vis_h; /* the picture in the buffer (top left) */
};
std::mutex pic_lock;
@@ -170,6 +179,54 @@ std::condition_variable pic_freed;
Slot slots[SLOTS];
Format format;
int generation; /* bumped by every vmem setup */
std::atomic<bool> stopping; /* the video before is stopping (in the background) */
/* The same video reopened (hardware to software, fast decoding): its last
* picture stays up until the reopened one's first comes, instead of black */
std::atomic<bool> hold_picture;
/* Each open's number, given to ps5http (":ps5http-serial"): a stop marks its
* number cancelled and its connections end at once. Leaving a slow channel
* waited 10 s for its server to finish sending a segment, and the next video
* waited with it (2026-10-18). */
std::atomic<long> cancelled_serial;
long next_serial, queued_serial, started_serial;
/* A live stream that ended by itself (a segment the server lost, 404):
* opened again, a few times, instead of "Watch again" on a TV channel. */
int live_reconnects;
bool live_reconnecting;
int64_t live_last_ms, live_last_len;
/* Diagnostics, always in the log, a few lines per video (2026-10-18: a
* flash of another video with hardware decoding, not seen on the PC): each
* of the first pictures' brightness when VLC hands it over and when it goes
* up to the screen (a buffer written over in between shows as two numbers),
* and every unusual path a picture takes. */
const int DIAG_PICTURES = 15;
int diag_shown, diag_up, diag_draws, diag_odd;
std::atomic<int> diag_vlc_timing; /* VLC's lateness messages let into the log, this video */
static const char *const slot_names[] = { "free", "writing", "ready", "shown", "gpu" };
/* Average brightness (0-255) of a slot's picture: 16x16 points of its luma. */
unsigned slot_luma(const Slot &sl, const Format &f)
{
if (!sl.mapped || !f.width || !f.height)
return 0;
uint32_t w = f.vis_w ? f.vis_w : f.width, h = f.vis_h ? f.vis_h : f.height;
const uint8_t *y0 = sl.mapped + f.offsets[0];
uint64_t sum = 0;
for (unsigned y = 0; y < 16; y++) {
const uint8_t *row = y0 + (size_t)(h * y / 16 + h / 32) * f.pitches[0];
for (unsigned x = 0; x < 16; x++) {
unsigned px = w * x / 16 + w / 32;
if (!f.ten_bit)
sum += row[px];
else /* P010: 10 bits at the top; I0AL: at the bottom */
sum += f.two_planes ? ((const uint16_t *)row)[px] >> 8 : ((const uint16_t *)row)[px] >> 2;
}
}
return (unsigned)(sum / 256);
}
Colour next_colour; /* vmem_colour's, for the setup right after it */
int shown = -1; /* the slot VLC displayed last, not uploaded yet */
std::atomic<uint64_t> completed_frame{ 0 };
@@ -189,9 +246,14 @@ struct Planes {
uint32_t w[PLANES], h[PLANES];
int generation = -1;
bool has_picture;
/* the frame textures of a new shape came in: the interface placed the
* video earlier in that frame, with the shape before */
uint64_t new_shape_frame;
float prev_aspect; /* the last setup's picture shape */
bool ten_bit, full_range, two_planes;
Colour colour;
uint32_t buf_w, buf_h;
uint32_t vis_w, vis_h;
} planes;
VkSampler sampler;
@@ -259,6 +321,12 @@ void vmem_colour(void *opaque, const video_format_t *fmt)
default: c.matrix = c.transfer != TRC_SDR ? MATRIX_2020 : MATRIX_AUTO; break;
}
c.full_range = fmt->b_color_range_full;
if (!fmt->i_x_offset && !fmt->i_y_offset) {
c.buf_w = fmt->i_width;
c.buf_h = fmt->i_height;
c.vis_w = fmt->i_visible_width;
c.vis_h = fmt->i_visible_height;
}
c.wide = fmt->primaries == COLOR_PRIMARIES_BT2020 ||
(fmt->primaries == COLOR_PRIMARIES_UNDEF && c.transfer != TRC_SDR);
/* How bright it gets: the content's own light level, else the mastering
@@ -386,6 +454,12 @@ unsigned vmem_setup(void **opaque, char *chroma, unsigned *width, unsigned *heig
f.two_planes = two;
f.colour = next_colour;
f.full_range = full || f.colour.full_range;
/* vmem keeps the decoder's visible part when the buffer stays the
* decoder's size (patches/0018); otherwise all of it is picture */
bool keep = f.colour.buf_w == *width && f.colour.buf_h == *height && f.colour.vis_w &&
f.colour.vis_h && f.colour.vis_w <= *width && f.colour.vis_h <= *height;
f.vis_w = keep ? f.colour.vis_w : *width;
f.vis_h = keep ? f.colour.vis_h : *height;
next_colour = Colour{};
uint32_t bpp = ten ? 2 : 1;
uint32_t cw = (*width + 1) / 2, ch = (*height + 1) / 2;
@@ -419,6 +493,8 @@ unsigned vmem_setup(void **opaque, char *chroma, unsigned *width, unsigned *heig
format = f;
info_stale = true;
generation++;
diag_shown = diag_up = diag_draws = 0;
diag_vlc_timing = 0;
fprintf(stderr, "player: pictures %ux%u %s%s%s, %d x %llu KiB\n", f.width, f.height,
ten ? "10-bit" : "8-bit", two ? (ten ? " P010" : " NV12") : "",
f.full_range ? " full range" : "", SLOTS,
@@ -464,9 +540,13 @@ void *vmem_lock(void *opaque, void **out)
found = i;
break;
}
if (diag_odd++ < 20)
DIAG("diag: no free picture buffer: VLC writes over ready one %d\n", found);
}
if (found < 0 || !slots[found].buffer) {
/* Shouldn't happen; give VLC somewhere harmless to write. */
if (diag_odd++ < 20)
DIAG("diag: no picture buffer at all: VLC writes to a scratch one\n");
static uint8_t *scratch;
static VkDeviceSize scratch_size;
if (scratch_size < format.size) {
@@ -499,15 +579,37 @@ void vmem_display(void *opaque, void *picture)
(void)opaque;
int i = (int)(intptr_t)picture;
std::lock_guard<std::mutex> g(pic_lock);
if (i < 0 || i >= SLOTS || slots[i].state != READY)
if (i < 0 || i >= SLOTS || slots[i].state != READY) {
if (diag_odd++ < 20)
DIAG("diag: picture %d to show isn't ready (%s): not shown\n", i,
i >= 0 && i < SLOTS ? slot_names[slots[i].state] : "none");
return;
if (shown >= 0 && shown != i && slots[shown].state == SHOWN)
}
if (stopping) {
if (diag_odd++ < 20)
DIAG("diag: picture %d of the video before (stopping): dropped\n", i);
/* the video before, on its way out: its last pictures aren't for
* the screen (one came up in the next video's frame for a moment,
* "a frame from another video", 2026-10-13) */
slots[i].state = FREE;
return;
}
if (shown >= 0 && shown != i && slots[shown].state == SHOWN) {
slots[shown].state = FREE; /* never drawn: the screen was slower */
if (diag_shown < DIAG_PICTURES)
DIAG("diag: picture %d replaced before it was drawn\n", shown);
}
slots[i].state = SHOWN;
shown = i;
if (diag_shown < DIAG_PICTURES) {
diag_shown++;
DIAG("diag shown %d: buffer %d, setup %d, %.3f s after the open, frame %llu, luma %u\n",
diag_shown, i, generation, plat_time() - opened_at,
(unsigned long long)gfx.frame_number, slot_luma(slots[i], format));
}
if (first_picture_wait) {
first_picture_wait = false;
fprintf(stderr, "player: first picture %.2f s after the open\n", plat_time() - opened_at);
fprintf(stderr, "player: [%.2f] first picture %.2f s after the open\n", plat_time(), plat_time() - opened_at);
}
}
@@ -559,7 +661,6 @@ int amem_setup(void **opaque, char *format, unsigned *rate, unsigned *channels)
}
/* True while a stop runs in the background: what VLC still sends is dropped. */
std::atomic<bool> stopping;
/* A video opened while the one before was still stopping: started after. */
libvlc_media_t *queued_media;
@@ -766,10 +867,19 @@ void audio_main()
/* ---- libvlc ------------------------------------------------------------------- */
/* VLC's player, for a question from the interface: none while the video
* before stops in the background. VLC's getters (time, length, tracks...)
* wait for its stop then: 8.6 s on a live stream that was ending, and the
* whole app froze (2026-10-13). */
static libvlc_media_player_t *asking()
{
return stopping ? nullptr : mp;
}
void read_info()
{
unsigned w = 0, h = 0;
if (libvlc_video_get_size(mp, 0, &w, &h) != 0 || !w || !h)
if (!asking() || libvlc_video_get_size(mp, 0, &w, &h) != 0 || !w || !h)
return;
VideoInfo v = {};
v.width = w;
@@ -981,6 +1091,8 @@ bool make_planes(const Format &f)
planes.two_planes = f.two_planes;
planes.buf_w = f.width;
planes.buf_h = f.height;
planes.vis_w = f.vis_w ? f.vis_w : f.width;
planes.vis_h = f.vis_h ? f.vis_h : f.height;
planes.has_picture = false;
return true;
}
@@ -1008,7 +1120,10 @@ void vlc_log(void *data, int level, const libvlc_log_t *ctx, const char *fmt, va
{
(void)data;
/* Normally errors only (a warning flood, "picture is too late", costs
* nothing on screen now but fills the log); debug=1 in settings.txt: all. */
* nothing on screen now but fills the log); debug=1 in settings.txt: all.
* Pictures VLC drops as late and the clock's corrections are let through,
* 40 per video at most: why a video stutters (planet_earth, 2026-10-18). */
bool timing = false;
if (level < LIBVLC_ERROR && !log_debug)
return;
static const char *const names[] = { "debug", "", "info", "warning", "error" };
@@ -1017,6 +1132,10 @@ void vlc_log(void *data, int level, const libvlc_log_t *ctx, const char *fmt, va
libvlc_log_get_context(ctx, &module, &file, &line);
char msg[1024];
vsnprintf(msg, sizeof(msg), fmt, args);
if (timing) {
fprintf(stderr, "vlc timing [%.2f]: %s\n", plat_time(), msg);
return;
}
/* The console logger's own option, asked for by others; it isn't linked. */
if (!strcmp(msg, "option quiet does not exist"))
return;
@@ -1069,14 +1188,48 @@ void load_decode_mode()
}
}
/* VLC's playback numbers, read on a thread of their own: read on the
* interface's thread (the health check, every second), they once waited
* 8.9 s for a live stream that was ending, and the whole app froze
* (2026-10-13). The interface takes the latest copy. */
std::thread stats_thread;
std::atomic<bool> stats_run;
std::mutex stats_lock;
libvlc_media_stats_t stats_copy;
bool stats_have;
std::atomic<unsigned> stats_serial; /* a new video: numbers read before it are dropped */
void stats_loop()
{
while (stats_run) {
std::this_thread::sleep_for(std::chrono::milliseconds(250));
unsigned serial = stats_serial;
libvlc_media_t *m = mp ? libvlc_media_player_get_media(mp) : nullptr;
libvlc_media_stats_t s = {};
bool ok = m && libvlc_media_get_stats(m, &s) != 0;
if (m)
libvlc_media_release(m);
std::lock_guard<std::mutex> g(stats_lock);
if (serial != stats_serial)
continue; /* the video before's (negative counts, 2026-10-13) */
stats_copy = s;
stats_have = ok;
}
}
void stats_forget()
{
std::lock_guard<std::mutex> g(stats_lock);
stats_serial++;
stats_copy = {};
stats_have = false;
}
bool get_stats(libvlc_media_stats_t *s)
{
libvlc_media_t *m = mp ? libvlc_media_player_get_media(mp) : nullptr;
if (!m)
return false;
bool ok = libvlc_media_get_stats(m, s) != 0;
libvlc_media_release(m);
return ok;
std::lock_guard<std::mutex> g(stats_lock);
*s = stats_copy;
return stats_have;
}
vlc_object_t *mp_object()
@@ -1310,6 +1463,12 @@ extern "C" void vlc_ps5_copy_plane(uint8_t *dst, size_t dst_pitch, const uint8_t
}
/* ps5/modules/ps5vdec.c, from VLC's decoder thread */
/* ps5vdec and ps5http: their diagnostic lines with the detailed log only */
extern "C" int vlc_ps5_log_details(void)
{
return log_debug;
}
extern "C" void vlc_ps5_vdec_failed(void)
{
vdec_failed = true;
@@ -1329,7 +1488,9 @@ bool player_init()
}
cpu_probe();
load_decode_mode();
log_debug = setting_int("debug", 0) != 0 || setting_int("vlc_debug", 0) != 0 || pref_int("debug", 0) != 0;
/* settings.txt only: the Settings row is gone, and an old prefs.txt debug=1
* would have left it on with no way to turn it off */
log_debug = setting_int("debug", 0) != 0 || setting_int("vlc_debug", 0) != 0;
/* The subtitle fonts from the app's folder, wherever it is now (it isn't
* /app0 after the sandbox was lifted). */
std::string font = "--freetype-font=" + std::string(plat_data_dir()) + "/fonts/subtitle.ttf";
@@ -1399,6 +1560,8 @@ bool player_init()
audio_thread = std::thread(audio_main);
}
fprintf(stderr, "player: libvlc %s ready\n", libvlc_get_version());
stats_run = true;
stats_thread = std::thread(stats_loop);
return true;
}
@@ -1423,8 +1586,15 @@ static void wait_for_stop()
stopper.join();
}
extern "C" int vlc_ps5_stream_cancelled(long serial)
{
return serial > 0 && serial <= cancelled_serial;
}
static void stop_in_background()
{
if (started_serial > cancelled_serial)
cancelled_serial = started_serial;
/* already stopping: nothing new can have started since (an open waits
* for the stop), and waiting for it here froze the app */
if (stopping)
@@ -1446,6 +1616,9 @@ static void stop_in_background()
void player_shutdown()
{
stats_run = false;
if (stats_thread.joinable())
stats_thread.join();
wait_for_stop();
if (mp) {
libvlc_media_player_stop(mp);
@@ -1482,9 +1655,13 @@ bool player_open(const std::string &path, int64_t start_ms, const std::vector<st
: libvlc_media_new_path(vlc, path.c_str());
if (!m)
return false;
for (const std::string &o : options)
/* the same one again (hardware to software, a reconnect): its own
* options too, or a channel that needs its user agent or referrer
* failed on the reopen */
const std::vector<std::string> &use = options.empty() && path == current_path ? source_options : options;
for (const std::string &o : use)
libvlc_media_add_option(m, o.c_str());
return player_open_media(m, path, start_ms, options);
return player_open_media(m, path, start_ms, std::vector<std::string>(use));
}
std::string file_uri(const std::string &path);
@@ -1673,6 +1850,10 @@ void side_subs_find(const std::string &url, int serial)
bool link = url_scheme_is(url, "http") || url_scheme_is(url, "https");
if (!listable && !link)
return;
static const char *const video_exts[] = { "mkv", "mp4", "m4v", "avi", "mov", "webm", "wmv", "flv", "ts",
"m2ts", "mts", "mpg", "mpeg", "vob", "ogv", "3gp", "divx", "rmvb" };
if (link && std::none_of(std::begin(video_exts), std::end(video_exts), [&](const char *e) { return ext == e; }))
return; /* a channel's address (".../udp/239.94.1.24:5140"): no file beside it to guess */
std::vector<std::string> opts = source_options;
/* its own reference: a quit during the listing (up to 8 s) mustn't free
* VLC under it */
@@ -1778,6 +1959,7 @@ static bool start_queued();
bool player_open_media(libvlc_media_t *m, const std::string &path, int64_t start_ms,
const std::vector<std::string> &options)
{
opened_from_ms = start_ms;
/* A reopen of the same file (Auto's, Watch again) keeps its login. */
if (!options.empty() || path != current_path)
source_options = options;
@@ -1796,7 +1978,20 @@ bool player_open_media(libvlc_media_t *m, const std::string &path, int64_t start
}
if (url) {
side_subs_find(path, side.serial);
/* Live TV over the network: 2 s of buffer, not VLC's 1. Channels
* whose segments come in late made VLC grow its buffer as it played
* ("PCR is called too late"), and the picture stuttered each time
* (a BBC channel, 2026-10-13). Shares and files keep VLC's own. */
if (url_scheme_is(path, "http") || url_scheme_is(path, "https") || url_scheme_is(path, "udp") ||
url_scheme_is(path, "rtp"))
libvlc_media_add_option(m, ":network-caching=2000");
} else if (!local.empty()) {
/* 1 s between decoding and showing, not VLC's 0.3 s for files: the
* console's VP9 decoder gives a picture back a few pictures later,
* and with YouTube's hidden frames at 24 fps that passed 0.3 s, so
* VLC dropped 2 pictures a second as late (planet_earth, 2026-10-18).
* A file reads in at once: it starts no later. */
libvlc_media_add_option(m, ":file-caching=1000");
std::vector<std::string> subs = local_side_subs(local);
bool on = player_sub_language() != "off";
std::string hint = sub_lang_hint();
@@ -1852,7 +2047,7 @@ bool player_open_media(libvlc_media_t *m, const std::string &path, int64_t start
libvlc_media_add_option(m, ":avcodec-skiploopfilter=4");
libvlc_media_add_option(m, ":avcodec-fast");
}
bool software = path == software_path || setting_int("hw_decode", 1) == 0;
bool software = path == software_path || !player_hw_decoding();
if (software)
libvlc_media_add_option(m, ":no-ps5vdec");
live_paused = false;
@@ -1861,21 +2056,31 @@ bool player_open_media(libvlc_media_t *m, const std::string &path, int64_t start
info_tries = 0;
current_path = path;
health = Health{};
stats_forget();
/* Opening and the first second drop pictures anyway; a resume also seeks
* and buffers first (run 19: one false "can't keep up" right after it). */
health.next = plat_time() + (start_ms > 0 ? 5 : 3);
debug_last = libvlc_media_stats_t{};
fprintf(stderr, "player: open %s at %lld ms%s%s\n", path.c_str(), (long long)start_ms,
fprintf(stderr, "player: [%.2f] open %s at %lld ms%s%s\n", plat_time(), path.c_str(), (long long)start_ms,
fast_now ? " (fast decoding)" : "", software ? " (software decoding)" : "");
/* The video before may still be stopping: a stream stuck on its network
* kept VLC's stop waiting 9 s, and waiting for it here froze the whole
* app (console run 12). This one then starts from player_tick once the
* stop is done. */
queued_serial = ++next_serial;
{
char opt[48];
snprintf(opt, sizeof(opt), ":ps5http-serial=%ld", queued_serial);
libvlc_media_add_option(m, opt);
}
if (!live_reconnecting)
live_reconnects = 0;
live_last_ms = live_last_len = 0;
if (queued_media)
libvlc_media_release(queued_media);
queued_media = m;
if (stopping) {
fprintf(stderr, "player: the video before is still stopping: this one starts after\n");
fprintf(stderr, "player: [%.2f] the video before is still stopping: this one starts after\n", plat_time());
return true;
}
return start_queued();
@@ -1889,7 +2094,26 @@ static bool start_queued()
return false;
queued_media = nullptr;
wait_for_stop();
started_serial = queued_serial;
libvlc_media_player_set_media(mp, m);
stats_forget(); /* until now the media was the video before */
debug_last = libvlc_media_stats_t{};
{
/* the video before is gone now: a picture of it still waiting to go
* up would show in this one's frame for a moment */
std::lock_guard<std::mutex> g(pic_lock);
if (shown >= 0 && slots[shown].state == SHOWN) {
slots[shown].state = FREE;
DIAG("diag: a picture of the video before was waiting: dropped\n");
}
shown = -1;
char states[64] = "";
for (const Slot &sl : slots)
snprintf(states + strlen(states), sizeof(states) - strlen(states), "%s ",
sl.buffer ? slot_names[sl.state] : "-");
DIAG("diag: starting, setup %d, buffers [%s]\n", generation, states);
diag_odd = 0;
}
if (last_media)
libvlc_media_release(last_media);
last_media = m; /* our reference: kept for a live pause */
@@ -1915,6 +2139,21 @@ void player_set_decode_mode(DecodeMode mode)
save_decode_mode();
}
/* prefs.txt hw_decode (Settings); settings.txt hw_decode=0, the older way, still counts */
bool player_hw_decoding()
{
return pref_int("hw_decode", setting_int("hw_decode", 1)) != 0;
}
void player_set_hw_decoding(bool on)
{
pref_set("hw_decode", on ? 1 : 0);
if (on)
software_path.clear();
/* from the next video: switching under a playing one held its picture a
* second or more (the user's test) */
}
bool player_fast_decoding()
{
return fast_now;
@@ -1931,9 +2170,11 @@ bool playing_disc_now()
return true;
if (url_scheme_is(bare, "dvd") || url_scheme_is(bare, "bluray"))
return true;
return mp && libvlc_media_player_get_title_count(mp) > 1;
return asking() && libvlc_media_player_get_title_count(mp) > 1;
}
static bool live_dropped();
PlayerEvent player_tick()
{
double t = plat_time();
@@ -1947,12 +2188,64 @@ PlayerEvent player_tick()
add_ready_subtitle(p.path, p.ready);
/* a video waiting for the one before to finish stopping */
if (queued_media && !stopping) {
fprintf(stderr, "player: the video before stopped: starting %s\n", current_path.c_str());
fprintf(stderr, "player: [%.2f] the video before stopped: starting %s\n", plat_time(), current_path.c_str());
start_queued();
}
if (live_dropped()) {
live_reconnects++;
fprintf(stderr, "player: live stream dropped at %lld of %lld ms: connecting again (%d of 5)\n",
(long long)live_last_ms, (long long)live_last_len, live_reconnects);
hold_picture = true;
std::string path = current_path;
stop_in_background();
{
std::lock_guard<std::mutex> g(audio_lock);
audio_clear_locked();
}
live_reconnecting = true;
player_open(path, 0);
live_reconnecting = false;
return PLAYER_EVENT_NONE;
}
if (stopping || queued_media)
vdec_failed = false; /* the video before's decoder, on its way out */
else if (mp && vdec_failed.exchange(false) && !current_path.empty()) {
/* the console's decoder gave up on this video: FFmpeg, same place.
* At once, not at the next health check (up to 3 s after the start:
* the picture stood still that long, VP9 2026-10-13). */
fprintf(stderr, "player: reopening %s without hardware decoding\n", current_path.c_str());
hold_picture = true;
restore_audio = libvlc_audio_get_track(mp);
restore_spu = libvlc_video_get_spu(mp);
software_path = current_path;
int64_t at = libvlc_media_player_get_time(mp);
/* given up in its first seconds: from where it started (VLC's clock
* had run 2.7 s ahead of the picture, so it reopened further on and
* looked frozen, VP9 2026-10-13) */
if (plat_time() - opened_at < 6)
at = opened_from_ms;
std::string path = current_path;
stop_in_background(); /* not here: VLC's stop froze the screen 1.5-3 s */
{
std::lock_guard<std::mutex> g(audio_lock);
audio_clear_locked();
}
player_open(path, at > 0 ? at : 0);
return PLAYER_EVENT_NONE;
}
if (!mp || t < health.next)
return PLAYER_EVENT_NONE;
health.next = t + 1;
if (!stopping && !queued_media && state() == libvlc_Playing) {
int64_t at = player_time();
if (at > 0) {
live_last_ms = at;
live_last_len = player_length();
/* a minute playing since the last reconnect: a fresh count */
if (live_reconnects && at > 60000)
live_reconnects = 0;
}
}
if (state() == libvlc_Playing)
side_subs_apply();
if (sub_check_at > 0 && t >= sub_check_at && state() == libvlc_Playing) {
@@ -1963,7 +2256,7 @@ PlayerEvent player_tick()
int64_t at = libvlc_media_player_get_time(mp);
std::string path = current_path;
restore_spu = 1000; /* the last subtitle track: picked below once known */
libvlc_media_player_stop(mp);
stop_in_background(); /* not here: VLC's stop froze the screen 1.5-3 s */
{
std::lock_guard<std::mutex> g(audio_lock);
audio_clear_locked();
@@ -1994,22 +2287,6 @@ PlayerEvent player_tick()
libvlc_video_set_spu_delay(mp, kept.spu_delay * 1000);
restore_delays = false;
}
if (vdec_failed.exchange(false) && !current_path.empty()) {
/* the console's decoder gave up on this video: FFmpeg, same place */
fprintf(stderr, "player: reopening %s without hardware decoding\n", current_path.c_str());
restore_audio = libvlc_audio_get_track(mp);
restore_spu = libvlc_video_get_spu(mp);
software_path = current_path;
int64_t at = libvlc_media_player_get_time(mp);
std::string path = current_path;
libvlc_media_player_stop(mp);
{
std::lock_guard<std::mutex> g(audio_lock);
audio_clear_locked();
}
player_open(path, at > 0 ? at : 0);
return PLAYER_EVENT_NONE;
}
libvlc_media_stats_t s;
if (!get_stats(&s))
return PLAYER_EVENT_NONE;
@@ -2032,6 +2309,14 @@ PlayerEvent player_tick()
* reopen would throw the menus away. */
if (playing_disc_now())
playing = false;
/* Nor on a network stream: there too few pictures means the server
* isn't sending fast enough (BBC America's sent a 4 s piece in 3-12 s,
* 2026-10-13), not slow decoding, and the reopen in fast decoding threw
* away what had come in: the channel froze from the start. */
if (url_scheme_is(current_path, "http") || url_scheme_is(current_path, "https") ||
url_scheme_is(current_path, "udp") || url_scheme_is(current_path, "rtp") ||
url_scheme_is(current_path, "rtsp") || url_scheme_is(current_path, "mms"))
playing = false;
bool bad = playing && decoded > 0 &&
(lost > shown || shown < fps * 0.5f);
health.bad_seconds = bad ? health.bad_seconds + 1 : 0;
@@ -2044,12 +2329,13 @@ PlayerEvent player_tick()
/* fast decoding is FFmpeg's: nothing to gain on the console's decoder */
if (!fast_now && decode_mode == DECODE_AUTO && vdec_active == 0) {
fprintf(stderr, "player: switching %s to fast decoding\n", current_path.c_str());
hold_picture = true;
restore_audio = libvlc_audio_get_track(mp);
restore_spu = libvlc_video_get_spu(mp);
fast_path = current_path;
int64_t at = libvlc_media_player_get_time(mp);
std::string path = current_path;
libvlc_media_player_stop(mp);
stop_in_background(); /* not here: VLC's stop froze the screen 1.5-3 s */
{
std::lock_guard<std::mutex> g(audio_lock);
audio_clear_locked();
@@ -2083,8 +2369,30 @@ static libvlc_state_t state()
return mp ? libvlc_media_player_get_state(mp) : libvlc_NothingSpecial;
}
static bool is_stream(const std::string &p)
{
return url_scheme_is(p, "http") || url_scheme_is(p, "https") || url_scheme_is(p, "udp") ||
url_scheme_is(p, "rtp") || url_scheme_is(p, "rtsp") || url_scheme_is(p, "mms");
}
/* A stream that played and then ended well before its end (a live channel
* has none, or a window it never reaches): dropped, not finished. */
static bool live_dropped()
{
if (queued_media || stopping || current_path.empty() || live_reconnects >= 5 || !is_stream(current_path))
return false;
libvlc_state_t s = state();
if (s != libvlc_Ended && s != libvlc_Error)
return false;
if (live_last_ms <= 0)
return false; /* never played: offline, not dropped */
return live_last_len <= 0 || live_last_ms < live_last_len - 5000;
}
bool player_active()
{
if (live_dropped())
return true; /* about to reconnect */
libvlc_state_t s = state();
return live_paused || queued_media || s == libvlc_Opening || s == libvlc_Buffering ||
s == libvlc_Playing || s == libvlc_Paused;
@@ -2092,8 +2400,8 @@ bool player_active()
bool player_ended()
{
if (queued_media)
return false; /* the next one is about to start */
if (queued_media || live_dropped())
return false; /* the next one is about to start, or a reconnect */
libvlc_state_t s = state();
return s == libvlc_Ended || s == libvlc_Error;
}
@@ -2131,7 +2439,7 @@ bool player_in_menu()
return menu;
next = now + 0.25;
menu = false;
if (!mp)
if (!asking())
return menu;
/* A DVD says when menu buttons are on screen ("highlight", set by VLC's
* dvdnav). A DVD's intro or warning screen is in its menu part too but has
@@ -2174,7 +2482,7 @@ bool player_in_menu()
* Blu-ray's pop-up menu, over the film. */
void player_disc_menu(bool popup)
{
if (!mp)
if (!asking())
return;
if (input_thread_t *input = libvlc_get_input_thread(mp)) {
vlc_object_t *in = (vlc_object_t *)input;
@@ -2189,7 +2497,7 @@ void player_disc_menu(bool popup)
bool player_disc_has_popup()
{
bool popup = false;
if (mp)
if (asking())
if (input_thread_t *input = libvlc_get_input_thread(mp)) {
vlc_object_t *in = (vlc_object_t *)input;
popup = var_Type(in, "bluray-popup") != 0 && var_GetBool(in, "bluray-popup");
@@ -2258,12 +2566,12 @@ void player_toggle_pause()
int64_t player_time()
{
return mp ? libvlc_media_player_get_time(mp) : 0;
return asking() ? libvlc_media_player_get_time(mp) : 0;
}
int64_t player_length()
{
int64_t l = mp ? libvlc_media_player_get_length(mp) : 0;
int64_t l = asking() ? libvlc_media_player_get_length(mp) : 0;
return l > 0 ? l : 0;
}
@@ -2297,7 +2605,7 @@ void player_set_volume(int volume)
float player_rate()
{
return mp ? libvlc_media_player_get_rate(mp) : 1.0f;
return asking() ? libvlc_media_player_get_rate(mp) : 1.0f;
}
void player_set_rate(float rate)
@@ -2308,12 +2616,12 @@ void player_set_rate(float rate)
std::vector<Track> player_audio_tracks()
{
return tracks_from(mp ? libvlc_audio_get_track_description(mp) : nullptr);
return tracks_from(asking() ? libvlc_audio_get_track_description(mp) : nullptr);
}
int player_audio_track()
{
return mp ? libvlc_audio_get_track(mp) : -1;
return asking() ? libvlc_audio_get_track(mp) : -1;
}
void player_set_audio_track(int id)
@@ -2325,12 +2633,12 @@ void player_set_audio_track(int id)
std::vector<Track> player_subtitle_tracks()
{
return tracks_from(mp ? libvlc_video_get_spu_description(mp) : nullptr);
return tracks_from(asking() ? libvlc_video_get_spu_description(mp) : nullptr);
}
int player_subtitle_track()
{
return mp ? libvlc_video_get_spu(mp) : -1;
return asking() ? libvlc_video_get_spu(mp) : -1;
}
void player_set_subtitle_track(int id)
@@ -2472,15 +2780,35 @@ void video_upload(VkCommandBuffer cmd)
slot = shown;
}
pic_freed.notify_all();
/* reopened: the last picture stays until there's a new one to show */
if (gen != planes.generation && gen > 0 && slot < 0 && hold_picture && planes.has_picture)
return;
if (gen != planes.generation && gen > 0) {
DIAG("diag: setup %d on screen (was %d): %ux%u%s, picture up %d, hold %d, waiting %d\n", gen,
planes.generation, f.width, f.height, f.two_planes ? " two planes" : "",
(int)planes.has_picture, (int)hold_picture.load(), slot);
hold_picture = false;
/* The same shape again (the file restarted, Watch again, a loop):
* the textures stay, and so does the last picture until the next
* one comes; remade, the video blinked black. */
bool same = planes.image[0] && f.width == planes.buf_w && f.height == planes.buf_h &&
f.ten_bit == planes.ten_bit && f.full_range == planes.full_range &&
f.two_planes == planes.two_planes;
if (!same && !make_planes(f))
DIAG("diag: textures %s\n", same ? "kept (same shape)" : "made new");
static double planes_failed_at;
if (!same && plat_time() - planes_failed_at < 0.5)
return;
if (!same && !make_planes(f)) {
planes_failed_at = plat_time();
return;
}
/* only when the shape changes: 3840x2160 to 3840x2176 with the
* same picture in it (hardware to software) skipped a frame for
* nothing, a black flash (2026-10-18) */
float was = planes.prev_aspect, now_aspect = f.vis_h ? (float)f.vis_w / f.vis_h : 0;
if (!same && (was <= 0 || now_aspect <= 0 || fabsf(was / now_aspect - 1) > 0.01f))
planes.new_shape_frame = gfx.frame_number;
planes.prev_aspect = now_aspect;
planes.generation = gen;
planes.colour = f.colour;
}
@@ -2492,6 +2820,12 @@ void video_upload(VkCommandBuffer cmd)
if (slots[slot].state != SHOWN)
return;
buffer = slots[slot].buffer;
if (diag_up < DIAG_PICTURES) {
diag_up++;
DIAG("diag upload %d: buffer %d, setup %d, frame %llu, luma %u, picture up before %d\n",
diag_up, slot, gen, (unsigned long long)gfx.frame_number + 1, slot_luma(slots[slot], f),
(int)planes.has_picture);
}
slots[slot].state = GPU;
slots[slot].gpu_frame = gfx.frame_number + 1;
if (shown == slot)
@@ -2521,30 +2855,81 @@ void video_upload(VkCommandBuffer cmd)
planes.has_picture = true;
}
double player_open_age()
{
return plat_time() - opened_at;
}
bool video_has_picture()
{
return planes.has_picture;
}
const std::string &player_current_path()
{
return current_path;
}
bool player_match_rate()
{
return pref_int("match_rate", 0) != 0;
}
void player_set_match_rate(bool on)
{
pref_set("match_rate", on ? 1 : 0);
}
bool video_wants_hdr_output()
{
/* settings.txt hdr_output: 0 never, 1 when the console's output is HDR
* (the default), 2 always (a TV the console doesn't report as HDR) */
* (the default), 2 always (a TV the console doesn't report as HDR). It
* follows the console: trying HDR on an SDR output was taken out
* (the user: the PS5 switches the TV itself when it's set to). */
static const int mode = setting_int("hdr_output", 1);
if (mode == 0 || !planes.has_picture || planes.colour.transfer == TRC_SDR || !player_active())
return false;
return mode == 2 || gfx.display_range == 2;
}
float video_picture_aspect()
{
return format.vis_w && format.vis_h ? (float)format.vis_w / format.vis_h : 0;
}
void video_forget_picture()
{
DIAG("diag: [%.2f] picture taken off the screen (was up %d)\n", plat_time(), (int)planes.has_picture);
planes.has_picture = false;
hold_picture = false;
/* and a picture of it still waiting to go up */
std::lock_guard<std::mutex> g(pic_lock);
if (shown >= 0 && slots[shown].state == SHOWN)
slots[shown].state = FREE;
shown = -1;
}
void video_draw(VkCommandBuffer cmd, float x, float y, float w, float h)
{
if (!planes.has_picture)
return;
/* A new shape's first frame: the rectangle was worked out before the
* picture came, from the video before's shape. With the console's
* decoder the size and the first picture come in the same frame, and a
* portrait video flashed stretched over the whole screen after a 16:9
* one ("a frame from another video", caught by the log 2026-10-18).
* Nothing this one frame; the next is placed right. */
if (planes.new_shape_frame == gfx.frame_number) {
DIAG("diag draw: frame %llu skipped, the rectangle is the shape before's\n",
(unsigned long long)gfx.frame_number);
return;
}
if (diag_draws < 6) {
diag_draws++;
DIAG("diag draw: frame %llu, textures of setup %d (now %d), %ux%u at %.0f,%.0f %.0fx%.0f\n",
(unsigned long long)gfx.frame_number, planes.generation, generation, planes.buf_w,
planes.buf_h, x, y, w, h);
}
const VideoInfo &v = player_video_info();
Push p = {};
/* The matrix VLC names; if it doesn't, BT.709 for HD and BT.601 for SD. */
@@ -2576,11 +2961,11 @@ void video_draw(VkCommandBuffer cmd, float x, float y, float w, float h)
p.view[2] = tanf(view_fov * 3.14159265f / 360);
p.view[3] = h > 0 ? w / h : 16.0f / 9;
}
/* The whole buffer is picture: vmem's pictures fill the size it set up
* (1024x560 arrives stretched to 1024x578), so nothing is cut; the
* rectangle has the picture's real shape. */
p.uv_scale[0] = 1;
p.uv_scale[1] = 1;
/* Only the picture, not the decoder's padding rows (1280x720 of a
* 1280x736 buffer): vmem passes the buffer as it is (patches/0018)
* instead of VLC scaling every picture to fill it on the CPU. */
p.uv_scale[0] = planes.buf_w ? (float)planes.vis_w / planes.buf_w : 1.0f;
p.uv_scale[1] = planes.buf_h ? (float)planes.vis_h / planes.buf_h : 1.0f;
VkViewport vp = { x, y, w, h, 0, 1 };
/* The scissor stays on screen ("Fill" makes the rectangle larger than it). */
float sx0 = x < 0 ? 0 : x, sy0 = y < 0 ? 0 : y;
@@ -2606,7 +2991,7 @@ void player_view(float yaw, float pitch, float fov)
int64_t player_audio_delay()
{
return mp ? libvlc_audio_get_delay(mp) / 1000 : 0;
return asking() ? libvlc_audio_get_delay(mp) / 1000 : 0;
}
void player_set_audio_delay(int64_t ms)
@@ -2618,7 +3003,7 @@ void player_set_audio_delay(int64_t ms)
int64_t player_subtitle_delay()
{
return mp ? libvlc_video_get_spu_delay(mp) / 1000 : 0;
return asking() ? libvlc_video_get_spu_delay(mp) / 1000 : 0;
}
void player_set_subtitle_delay(int64_t ms)
@@ -2642,7 +3027,7 @@ static bool title_is_menu(int title)
std::vector<Chapter> player_chapters()
{
if (!mp)
if (!asking())
return chapters_cache;
/* A disc moves between titles (menu, intro, the film): each has its own
* chapters, so the list is read again when the title changes. */
@@ -2676,7 +3061,7 @@ std::vector<Chapter> player_chapters()
int player_chapter()
{
return mp ? libvlc_media_player_get_chapter(mp) : -1;
return asking() ? libvlc_media_player_get_chapter(mp) : -1;
}
void player_set_chapter(int index)
@@ -2702,8 +3087,8 @@ bool player_screenshot(const std::string &path)
shot.data.assign(slots[last_slot].mapped, slots[last_slot].mapped + format.size);
}
/* the whole buffer: the picture fills it (see video_draw) */
shot.w = shot.f.width;
shot.h = shot.f.height;
shot.w = shot.f.vis_w ? shot.f.vis_w : shot.f.width;
shot.h = shot.f.vis_h ? shot.f.vis_h : shot.f.height;
shot.path = path;
std::thread(shot_main, std::move(shot)).detach();
return true;
+12
View File
@@ -90,10 +90,18 @@ void player_spectrum(float *bands, int n);
/* Render thread, every frame: */
void video_upload(VkCommandBuffer cmd); /* before the render pass */
bool video_has_picture();
/* seconds since the video was opened */
double player_open_age();
/* An HDR video is showing and HDR10 output is wanted for it (settings.txt
* hdr_output, the console's output). */
bool video_wants_hdr_output();
/* Match frame rate: films at 23.976 / 24 / 25 / 50 fps switch the TV to that
* refresh while they play (saved; Off by default: some TVs blank a moment). */
bool player_match_rate();
const std::string &player_current_path(); /* what plays (or was last opened) */
void player_set_match_rate(bool on);
void video_draw(VkCommandBuffer cmd, float x, float y, float w, float h); /* in the pass */
float video_picture_aspect(); /* the pictures VLC sends now: width / height (0: none yet) */
void video_forget_picture(); /* back to the library */
/* One line about the player in the log (state, time, picture slots, sound queue,
* pictures decoded/shown/lost since the last line). */
@@ -178,6 +186,10 @@ void player_set_debug_log(bool on);
DecodeMode player_decode_mode();
void player_set_decode_mode(DecodeMode mode); /* saved; applies from the next file */
bool player_fast_decoding(); /* the current file decodes fast */
/* Hardware decoding (the console's video decoder) or FFmpeg on the CPU:
* saved; applies from the next video. */
bool player_hw_decoding();
void player_set_hw_decoding(bool on);
/* Main thread, every frame while playing: watches for lost pictures. */
enum PlayerEvent { PLAYER_EVENT_NONE, PLAYER_EVENT_WENT_FAST, PLAYER_EVENT_TOO_SLOW };
PlayerEvent player_tick();
+214 -39
View File
@@ -171,9 +171,11 @@ std::string vlc_message(const std::string &m)
}
if (m.compare(0, 14, "Please provide") == 0)
return "A Windows PC takes its own account; a NAS, the user you made on it.";
/* libbluray without a JVM (none runs on the console): not "install Java" */
/* libbluray without a JVM (none runs on the console): not "install Java".
* Said plainly: "it plays without them" promised a film that often
* didn't come (the user, 2026-10-18). */
if (m.compare(0, 30, "This Blu-ray disc requires Java") == 0)
return "This Blu-ray's menus are made in Java, which the PS5 can't run: it plays without them";
return "Java Blu-ray discs (BD-J) aren't supported on the PS5";
if (m.compare(0, 6, "Track ") == 0 && m.size() > 6 && isdigit((unsigned char)m[6]))
return trf("Track %d", atoi(m.c_str() + 6));
return m;
@@ -544,6 +546,15 @@ void ci_add(float cx, float cy, float s)
rect(cx + s * 0.278f, cy + s * 0.17f, s * 0.045f, s * 0.18f, IM_COL32(255, 255, 255, 255));
}
void icon_link(float cx, float cy, float s, ImU32 col);
/* Open a network link: a blue disc with a chain link */
void ci_link(float cx, float cy, float s)
{
circle(cx, cy, s * 0.38f, IM_COL32(52, 120, 214, 255));
icon_link(cx, cy, s * 0.46f, IM_COL32(255, 255, 255, 255));
}
/* ---- icons, drawn so they stay sharp at any size ----------------------------------- */
void icon_play(float cx, float cy, float s, ImU32 col)
@@ -1020,7 +1031,7 @@ void rebuild_lists()
/* Network places in Browse: a server (saved SMB share or DLNA server), a
* folder or file on it, and "Add a network share". */
enum { NET_NONE, NET_SERVER, NET_DIR, NET_FILE, NET_ADD, NET_ONLINE };
enum { NET_NONE, NET_SERVER, NET_DIR, NET_FILE, NET_ADD, NET_ONLINE, NET_LINK };
struct BrowseEntry {
std::string name, path;
@@ -1122,6 +1133,7 @@ void rebuild_browse()
for (const NetServer &sv : net_servers())
browse_entries.push_back({ sv.name, sv.url, true, -1, false, NET_SERVER });
browse_entries.push_back({ "Add a network share", "", false, -1, false, NET_ADD });
browse_entries.push_back({ "Open a network link", "", false, -1, false, NET_LINK });
browse_entries.push_back({ "Free TV channels", "tv:", true, -1, false, NET_ONLINE });
browse_entries.push_back({ "Internet radio", "radio:", true, -1, false, NET_ONLINE });
return;
@@ -1253,8 +1265,11 @@ void save_position(bool finished)
{
if (playing_path.empty() || is_link(playing_path))
return;
int64_t t = finished ? 0 : player_time();
library_set_resume(playing_path, t, player_length());
int64_t t = finished ? 0 : player_time(), len = player_length();
/* nothing known (the player busy stopping): the saved spot stays */
if (!finished && len <= 0)
return;
library_set_resume(playing_path, t, len);
}
void leave_player(bool finished)
@@ -2446,6 +2461,7 @@ std::vector<std::pair<uint32_t, const char *>> playlist_hints()
void start_stream(const std::string &url, const std::vector<std::string> &options = {}, bool remember = true,
const std::string &name = "", libvlc_media_t *media = nullptr);
void open_add_share();
void open_link();
void spinner(float cx, float cy, float r);
@@ -2603,7 +2619,7 @@ void classic_label(const BrowseEntry &e, std::string *name, std::string *type, i
if (e.net == NET_SERVER) {
*type = net_is_saved_server(e.path) ? "Network share (SMB)" : "Media server (DLNA)";
*group = 1;
} else if (e.net == NET_ADD) {
} else if (e.net == NET_ADD || e.net == NET_LINK) {
*type = "";
*group = 1;
} else if (e.net == NET_ONLINE) {
@@ -2669,6 +2685,8 @@ void classic_icon(const BrowseEntry &e, float cx, float cy, float s)
ci_server(cx, cy, s);
else if (e.net == NET_ADD)
ci_add(cx, cy, s);
else if (e.net == NET_LINK)
ci_link(cx, cy, s);
else if (e.net == NET_ONLINE)
e.path == "tv:" ? ci_tv(cx, cy, s) : ci_radio(cx, cy, s);
else if (e.net == NET_FILE)
@@ -2908,6 +2926,8 @@ void mini_player()
text(f_reg, 17, kx + 46, y + 17, C_DIM, "Open");
}
void open_search(const std::string &text = "");
void tab_browse()
{
/* The network listing or the servers found changed: rebuild. */
@@ -2976,6 +2996,8 @@ void tab_browse()
BrowseEntry e = browse_entries[browse_focus];
if (e.net == NET_ADD) {
open_add_share();
} else if (e.net == NET_LINK) {
open_link();
} else if (e.net == NET_SERVER || e.net == NET_DIR || e.net == NET_ONLINE) {
net_enter(e.path, e.name);
} else {
@@ -3084,6 +3106,8 @@ void tab_browse()
icon_radio(146, y + 33, 34, mix(C_DIM, C_ORANGE, f));
else if (e.net == NET_ADD)
icon_plus_circle(146, y + 33, 32, mix(C_DIM, C_ORANGE, f));
else if (e.net == NET_LINK)
icon_link(146, y + 33, 30, mix(C_DIM, C_ORANGE, f));
else if (e.net == NET_FILE) {
int k = name_kind(e.name);
if (k == 1)
@@ -3322,8 +3346,16 @@ void kb_backspace()
/* Draws the keyboard at (x, y), w wide; handles the buttons when it has the
* focus. Returns true on Done. *leave_up: ↑ was pressed on the top row. */
#ifdef VLCPS5_HOST
extern "C" const char *plat_take_typed(void); /* the test script's "type" steps */
#endif
bool kb_frame(float x, float y, float w, bool focused, float a, bool *leave_up)
{
#ifdef VLCPS5_HOST
if (const char *typed = plat_take_typed())
kb.text += typed;
#endif
const float gap = 10, kh = 62;
float kw = (w - 9 * gap) / 10;
std::vector<int> bottom = kb_bottom();
@@ -3474,15 +3506,24 @@ void text_box(float x, float y, float w, const char *placeholder, float a, bool
/* ---- Search and Open a network link -------------------------------------------- */
enum FoundKind { F_ITEM, F_PLAYLIST, F_BROWSE, F_SERVER, F_LINK, F_TV, F_STATION, F_BUSY };
struct Found {
int item; /* library index, or -1 */
int playlist; /* playlist index, or -1 */
std::string link;
int kind;
int index = -1; /* library item, playlist or Browse entry */
std::string link; /* a server's or a recent link's address */
};
std::vector<Found> found;
int found_focus;
bool in_list; /* focus on the list (above the keyboard) */
std::string found_for = "\x01";
int found_serial = -1; /* the Browse network listing searched */
int found_online = -1; /* the online results searched (net_search_serial) */
/* Searching from inside a network list (a channel list, a share): its entries too. */
bool search_in_browse()
{
return screen == LIBRARY && tab == BROWSE && !net_path.empty() && !sub_pick;
}
std::string lower_ascii(std::string s)
{
@@ -3491,26 +3532,78 @@ std::string lower_ascii(std::string s)
return s;
}
std::vector<std::string> recent_links();
std::string link_name(const std::string &url);
/* Everything the app knows by name: the list open in Browse (channels, a
* share's folder), playlists, videos and songs, saved shares and servers,
* recent links, and the online lists (every free TV channel, radio stations
* by name). */
void update_search()
{
if (found_for == kb.text)
int serial = search_in_browse() ? net_list_serial() : -1;
net_search_typed(kb.text);
int online = net_search_serial() * 2 + (net_search_busy() ? 1 : 0);
if (found_for == kb.text && found_serial == serial && found_online == online)
return;
bool typed = found_for != kb.text;
found_for = kb.text;
found_serial = serial;
found_online = online;
found.clear();
if (typed)
found_focus = 0;
std::string q = lower_ascii(kb.text);
while (!q.empty() && q.back() == ' ')
q.pop_back();
while (!q.empty() && q[0] == ' ')
q.erase(0, 1);
if (q.empty())
return;
auto has = [&](const std::string &name) { return lower_ascii(name).find(q) != std::string::npos; };
if (serial >= 0) {
/* The list open in Browse first: that's what is being looked through. */
int got = 0;
for (int i = 0; i < (int)browse_entries.size() && got < 200; i++)
if (browse_entries[i].net != NET_NONE && browse_entries[i].net != NET_ADD && browse_entries[i].net != NET_LINK && has(browse_entries[i].name)) {
found.push_back({ F_BROWSE, i });
got++;
}
}
auto &pls = library_playlists();
for (int i = 0; i < (int)pls.size(); i++)
if (lower_ascii(pls[i].name).find(q) != std::string::npos)
found.push_back({ -1, i, "" });
for (int i = 0; i < (int)L().size() && found.size() < 60; i++)
if (lower_ascii(L()[i].name).find(q) != std::string::npos ||
lower_ascii(L()[i].folder.substr(L()[i].folder.rfind('/') + 1)).find(q) != std::string::npos)
found.push_back({ i, -1, "" });
found_focus = 0;
if (has(pls[i].name))
found.push_back({ F_PLAYLIST, i });
int items = 0;
for (int i = 0; i < (int)L().size() && items < 100; i++)
if (has(L()[i].name) || has(L()[i].folder.substr(L()[i].folder.rfind('/') + 1))) {
found.push_back({ F_ITEM, i });
items++;
}
for (const NetServer &sv : net_servers())
if (has(sv.name) || has(sv.url))
found.push_back({ F_SERVER, -1, sv.url });
for (const std::string &l : recent_links())
if (has(link_name(l)) || has(l))
found.push_back({ F_LINK, -1, l });
/* Online, as it comes: free TV channels and radio stations, played
* straight from here */
if (q.size() >= 2) {
const std::vector<NetEntry> &tv = net_search_tv();
int got = 0;
for (int i = 0; i < (int)tv.size() && got < 60; i++)
if (has(tv[i].name)) {
found.push_back({ F_TV, i });
got++;
}
const std::vector<NetEntry> &radio = net_search_radio();
for (int i = 0; i < (int)radio.size() && i < 40; i++)
found.push_back({ F_STATION, i });
if (net_search_busy())
found.push_back({ F_BUSY });
}
if (found_focus >= (int)found.size())
found_focus = found.empty() ? 0 : (int)found.size() - 1;
}
/* Recent links, newest first: cache/links.txt. */
@@ -3626,9 +3719,9 @@ void start_stream(const std::string &url, const std::vector<std::string> &option
was_active = false;
}
void open_search()
void open_search(const std::string &text)
{
kb_open("", false);
kb_open(text, false);
found_for = "\x01";
in_list = false;
modal = SEARCH;
@@ -4424,12 +4517,46 @@ void modal_text_screen()
std::string title, sub;
if (search) {
const Found &r = found[i];
if (r.playlist >= 0) {
if (r.kind == F_TV) {
icon_tv(x + 22, ry + 26, 28, alpha(C_DIM, a));
title = net_search_tv()[r.index].name;
sub = tr("Free TV channels");
} else if (r.kind == F_STATION) {
icon_radio(x + 22, ry + 26, 28, alpha(C_DIM, a));
title = net_search_radio()[r.index].name;
sub = tr("Internet radio");
} else if (r.kind == F_BUSY) {
spinner(x + 22, ry + 26, 12);
title = tr("Looking for channels and stations\xE2\x80\xA6");
} else if (r.kind == F_SERVER) {
icon_server(x + 22, ry + 26, 28, alpha(C_DIM, a));
title = r.link;
for (const NetServer &sv : net_servers())
if (sv.url == r.link)
title = sv.name;
sub = tr("Network");
} else if (r.kind == F_LINK) {
icon_link(x + 22, ry + 26, 28, alpha(C_DIM, a));
title = link_name(r.link);
sub = r.link;
} else if (r.kind == F_BROWSE) {
const BrowseEntry &b = browse_entries[r.index];
if (b.dir)
icon_folder(x + 22, ry + 26, 28, alpha(C_DIM, a));
else if (net_path[0].first.compare(0, 3, "tv:") == 0)
icon_tv(x + 22, ry + 26, 28, alpha(C_DIM, a));
else if (net_path[0].first.compare(0, 6, "radio:") == 0)
icon_radio(x + 22, ry + 26, 28, alpha(C_DIM, a));
else
icon_film(x + 22, ry + 26, 30, alpha(C_DIM, a));
title = b.name;
sub = net_path.back().second;
} else if (r.kind == F_PLAYLIST) {
icon_list(x + 22, ry + 26, 28, alpha(C_DIM, a));
title = library_playlists()[r.playlist].name;
title = library_playlists()[r.index].name;
sub = "Playlist";
} else {
const MediaItem &m = L()[r.item];
const MediaItem &m = L()[r.index];
if (m.audio)
icon_note(x + 22, ry + 26, 26, alpha(C_DIM, a));
else
@@ -4456,14 +4583,41 @@ void modal_text_screen()
if (search) {
Found r = found[found_focus];
modal = NONE;
if (r.playlist >= 0) {
play_playlist(library_playlists()[r.playlist].path);
if (r.kind == F_BUSY) {
modal = SEARCH; /* nothing to open yet: stay */
} else if (r.kind == F_TV || r.kind == F_STATION) {
const NetEntry e = (r.kind == F_TV ? net_search_tv() : net_search_radio())[r.index];
start_stream(e.url, e.options, false, e.name, net_search_media(e.url));
} else if (r.kind == F_SERVER) {
/* a place of Browse, opened from its top (○ walks back up) */
screen = LIBRARY;
screen_fade = 0;
tab = BROWSE;
net_path.clear();
browse_dir.clear();
std::string name = r.link;
for (const NetServer &sv : net_servers())
if (sv.url == r.link)
name = sv.name;
net_enter(r.link, name);
} else if (r.kind == F_LINK) {
start_stream(r.link);
} else if (r.kind == F_BROWSE) {
/* as ✕ on that row of Browse */
BrowseEntry e = browse_entries[r.index];
browse_focus = r.index;
if (e.dir)
net_enter(e.path, e.name);
else
start_stream(e.path, net_login_options(e.path), false, e.name, net_take_media(e.path));
} else if (r.kind == F_PLAYLIST) {
play_playlist(library_playlists()[r.index].path);
} else {
std::vector<int> list;
for (const Found &x2 : found)
if (x2.item >= 0 && (!L()[x2.item].other || x2.item == r.item))
list.push_back(x2.item);
open_item(r.item, list);
if (x2.kind == F_ITEM && (!L()[x2.index].other || x2.index == r.index))
list.push_back(x2.index);
open_item(r.index, list);
}
} else {
std::string url = links[found_focus];
@@ -4621,10 +4775,10 @@ void modal_options()
};
std::vector<Item> items;
if (in_player)
items = { { "Resume", OI_RESUME }, { "Audio, subtitles and more", OI_TRACKS },
items = { { "Resume", OI_RESUME }, { "Audio, subtitles and more", OI_TRACKS }, { "Search", OI_SEARCH },
{ "Send files from a phone or PC", OI_PHONE }, { "Settings", OI_SETTINGS }, { "Stop playback", OI_STOP } };
else
items = { { "Search", OI_SEARCH }, { "Open a network link", OI_LINK },
items = { { "Search", OI_SEARCH },
{ "Send files from a phone or PC", OI_PHONE }, { "Settings", OI_SETTINGS },
{ "Rescan media", OI_RESCAN } };
/* the file in front of you: into a playlist */
@@ -4759,13 +4913,14 @@ void setup_begin(bool with_logo);
enum {
ST_DECODING, ST_ALANG, ST_SLANG, ST_SOUND, ST_NIGHT, ST_SPEAKERS, ST_THEME, ST_HAPTICS,
ST_SWIPES, ST_REPEAT, ST_SHUFFLE, ST_DEBUG, ST_STORAGE, ST_VERSION, ST_RELOAD, ST_ABOUT,
ST_WEB, ST_LOOK, ST_OSUB, ST_UILANG, ST_SETUP, ST_BDREGION, ST_COUNT
ST_WEB, ST_LOOK, ST_OSUB, ST_UILANG, ST_SETUP, ST_BDREGION, ST_HWDEC, ST_MATCH, ST_COUNT
};
const char *const st_labels[ST_COUNT] = {
"Decoding", "Audio language", "Subtitle language", "Sound output", "Night mode",
"Speaker test", "Theme", "Controller vibration", "Touchpad swipes", "Repeat the list", "Shuffle",
"Detailed log", "Storage", "Version", "Reload drives", "About VLC", "Phone access", "Look",
"Subtitle downloads", "Language", "Run setup again", "Blu-ray region",
"Subtitle downloads", "Language", "Run setup again", "Blu-ray region", "Hardware decoding",
"Match frame rate",
};
/* One line under the focused setting: what it does. */
const char *const st_help[ST_COUNT] = {
@@ -4791,6 +4946,8 @@ const char *const st_help[ST_COUNT] = {
"The language of VLC's menus",
"Language, look and playback, chosen again",
"A: the Americas and East Asia. B: Europe, Africa and Oceania. C: China, Russia and India",
"The console's video chip. Off: the processor decodes, slower for 4K",
"Films play at their own 24, 25 or 50 Hz: smoother pans. Some TVs go black a moment",
};
/* Categories: an icon, a name, and their settings. */
@@ -4799,15 +4956,17 @@ struct SettingsCat {
int icon;
std::vector<int> rows;
};
enum { SI_PLAY, SI_SOUND, SI_THEME, SI_LIST, SI_GEAR };
enum { SI_PLAY, SI_SOUND, SI_THEME, SI_LIST, SI_GEAR, SI_VIDEO };
const SettingsCat settings_cats[] = {
{ "Playback", SI_PLAY, { ST_DECODING, ST_ALANG, ST_SLANG, ST_OSUB, ST_BDREGION } },
{ "Playback", SI_PLAY, { ST_ALANG, ST_SLANG, ST_OSUB, ST_BDREGION } },
{ "Video", SI_VIDEO, { ST_HWDEC, ST_MATCH, ST_DECODING } },
{ "Sound", SI_SOUND, { ST_SOUND, ST_NIGHT, ST_SPEAKERS } },
{ "Interface", SI_THEME, { ST_UILANG, ST_LOOK, ST_THEME, ST_SWIPES } },
{ "Playlists", SI_LIST, { ST_REPEAT, ST_SHUFFLE } },
{ "System", SI_GEAR, { ST_WEB, ST_STORAGE, ST_RELOAD, ST_SETUP, ST_DEBUG, ST_ABOUT } },
/* (no "Log details" row: the detailed log is for test builds, settings.txt debug=1) */
{ "System", SI_GEAR, { ST_WEB, ST_STORAGE, ST_RELOAD, ST_SETUP, ST_ABOUT } },
};
const int SETTINGS_CATS = 5;
const int SETTINGS_CATS = 6;
/* ---- settings icons: thin outlines, like the tab bar's ---------------------- */
@@ -4916,6 +5075,7 @@ void category_icon(int icon, float cx, float cy, float s, ImU32 col)
case SI_SOUND: icon_speaker(cx, cy, s * 0.9f, col); break;
case SI_THEME: si_palette(cx, cy, s, col); break;
case SI_LIST: icon_list(cx, cy, s * 0.9f, col); break;
case SI_VIDEO: icon_film(cx, cy, s * 0.9f, col); break;
default: icon_gear(cx, cy, s * 0.8f, col); break;
}
}
@@ -4923,7 +5083,8 @@ void category_icon(int icon, float cx, float cy, float s, ImU32 col)
void setting_icon(int id, float cx, float cy, float s, ImU32 col)
{
switch (id) {
case ST_DECODING: si_chip(cx, cy, s, col); break;
case ST_DECODING: case ST_HWDEC: si_chip(cx, cy, s, col); break;
case ST_MATCH: icon_speed(cx, cy, s * 0.8f, col); break;
case ST_ALANG: case ST_UILANG: si_globe(cx, cy, s, col); break;
case ST_SLANG: case ST_OSUB: icon_cc(cx, cy, s * 0.9f, col); break;
case ST_BDREGION: si_globe(cx, cy, s, col); break;
@@ -4970,6 +5131,8 @@ std::string st_value(int id)
static const char *const dec[] = { "Auto (fast when needed)", "Always fast", "Full quality" };
switch (id) {
case ST_DECODING: return dec[player_decode_mode()];
case ST_HWDEC: return player_hw_decoding() ? "On" : "Off";
case ST_MATCH: return player_match_rate() ? "On" : "Off";
case ST_ALANG: return lang_name(player_audio_language(), false);
case ST_SLANG: return lang_name(player_sub_language(), true);
case ST_SOUND: return player_audio_output() ? "Surround 5.1 / 7.1" : "Stereo";
@@ -5009,6 +5172,12 @@ void st_change(int id, int step)
case ST_DECODING:
player_set_decode_mode((DecodeMode)((player_decode_mode() + 3 + dir) % 3));
break;
case ST_MATCH: player_set_match_rate(!player_match_rate()); break;
case ST_HWDEC:
player_set_hw_decoding(!player_hw_decoding());
if (player_active())
toast("Hardware decoding: from the next video", 2.5);
break;
case ST_ALANG: cycle_language(false, dir); break;
case ST_SLANG: cycle_language(true, dir); break;
case ST_BDREGION: {
@@ -5282,7 +5451,7 @@ void modal_speakers()
if (hit(PAD_CIRCLE)) {
player_speaker_test(-1);
modal = SETTINGS;
settings_cat = 1;
settings_cat = 2; /* Sound */
settings_inside = true;
modal_focus = 2;
return;
@@ -6115,7 +6284,12 @@ void compute_video_rect()
vid_rect[3] = sh;
return;
}
float aspect = v.aspect > 0 ? v.aspect : 16.0f / 9;
/* until the video's info is read (a frame or two after its first
* picture), the picture's own shape: 16:9 there stretched a portrait
* video over the whole screen for its first frames (it looked like
* another video flashing by) */
float pic_aspect = video_picture_aspect();
float aspect = v.aspect > 0 ? v.aspect : pic_aspect > 0 ? pic_aspect : 16.0f / 9;
if (aspect_mode == PIC_16_9)
aspect = 16.0f / 9;
else if (aspect_mode == PIC_4_3)
@@ -7876,7 +8050,8 @@ void ui_frame(const PadState &pad, float frame_dt)
mini_player();
if (modal == NONE) {
bool open_kind = tab == BROWSE && browse_focus < (int)browse_entries.size() &&
(browse_entries[browse_focus].dir || browse_entries[browse_focus].net == NET_ADD);
(browse_entries[browse_focus].dir || browse_entries[browse_focus].net == NET_ADD ||
browse_entries[browse_focus].net == NET_LINK);
std::vector<std::pair<uint32_t, const char *>> h = { { PAD_CROSS, open_kind ? "Open" : "Play" } };
int fi = focused_item();
if (fi >= 0 && L()[fi].other) {