make hi-res work

This commit is contained in:
uimaxbai 2026-04-18 12:57:40 +01:00
parent c2b3f7312e
commit 6d79dafadb
7 changed files with 87 additions and 39 deletions

View file

@ -4063,6 +4063,7 @@
</div>
<select id="streaming-quality-setting">
<option value="auto">Auto (Adaptive)</option>
<option value="HI_RES_LOSSLESS">Hi-Res Lossless (24-bit)</option>
<option value="LOSSLESS">Lossless (16-bit)</option>
<option value="HIGH">AAC 320kbps</option>
<option value="LOW">AAC 96kbps</option>

View file

@ -11,6 +11,7 @@ import { preferDolbyAtmosSettings, trackDateSettings, devModeSettings } from './
import { APICache } from './cache.js';
import { DashDownloader } from './dash-downloader.ts';
import { HlsDownloader } from './hls-downloader.js';
import { getProxyUrl } from './proxy-utils.js';
import { loadFfmpeg, FfmpegError, ffmpeg } from './ffmpeg.js';
import { triggerDownload, applyAudioPostProcessing } from './download-utils.ts';
import { isCustomFormat } from './ffmpegFormats.ts';
@ -117,7 +118,9 @@ export class LosslessAPI {
for (let attempt = 1; attempt <= maxTotalAttempts; attempt++) {
const instance = instances[instanceIndex % instances.length];
const baseUrl = typeof instance === 'string' ? instance : instance.url;
const url = baseUrl.endsWith('/') ? `${baseUrl}${relativePath.substring(1)}` : `${baseUrl}${relativePath}`;
const url = baseUrl.endsWith('/')
? `${baseUrl}${relativePath.substring(1)}`
: `${baseUrl}${relativePath}`;
try {
const response = await fetch(url, { signal: options.signal });
@ -134,7 +137,10 @@ export class LosslessAPI {
}
if (response.status === 401) {
const errorData = await response.clone().json().catch(() => null);
const errorData = await response
.clone()
.json()
.catch(() => null);
if (errorData?.subStatus === 11002) {
console.warn(`Auth failed on ${baseUrl}. Trying next instance...`);
instanceIndex++;
@ -1763,7 +1769,7 @@ export class LosslessAPI {
if (streamUrl.startsWith('blob:')) {
try {
const downloader = new DashDownloader();
blob = await downloader.downloadDashStream(streamUrl, {
blob = await downloader.downloadDashStream(getProxyUrl(streamUrl), {
signal: options.signal,
onProgress,
calculateDashBytes: calculateDashBytes ?? true,
@ -1782,7 +1788,7 @@ export class LosslessAPI {
} else if (streamUrl.includes('.m3u8') || streamUrl.includes('application/vnd.apple.mpegurl')) {
try {
const downloader = new HlsDownloader();
blob = await downloader.downloadHlsStream(streamUrl, {
blob = await downloader.downloadHlsStream(getProxyUrl(streamUrl), {
signal: options.signal,
onProgress,
});
@ -1807,7 +1813,7 @@ export class LosslessAPI {
/* ignore HEAD failure; proceed with GET */
}
const response = await fetch(streamUrl, {
const response = await fetch(getProxyUrl(streamUrl), {
cache: 'no-store',
signal: options.signal,
});

View file

@ -467,31 +467,7 @@ document.addEventListener('DOMContentLoaded', async () => {
const audioPlayer = document.getElementById('audio-player');
// i love ios and macos!!!! webkit fucking SUCKS BULLSHIT sorry ios/macos heads yall getting lossless only playback
// Use isIos from platform-detection (set before UA spoof in index.html) so detection works on real iOS.
if (isIos || isSafari) {
const qualitySelect = document.getElementById('streaming-quality-setting');
const downloadQualitySelect = document.getElementById('download-quality-setting');
const removeHiRes = (select) => {
if (!select) return;
const option = select.querySelector('option[value="HI_RES_LOSSLESS"]');
if (option) option.remove();
};
removeHiRes(qualitySelect);
removeHiRes(downloadQualitySelect);
if (isIos) {
document.querySelector('#hi-res-download-warning').style.display = '';
}
const currentQualitySetting = localStorage.getItem('playback-quality');
if (!currentQualitySetting || currentQualitySetting === 'HI_RES_LOSSLESS') {
localStorage.setItem('playback-quality', 'LOSSLESS');
}
}
const currentQuality = localStorage.getItem('playback-quality') || 'LOSSLESS';
const currentQuality = localStorage.getItem('playback-quality') || 'HI_RES_LOSSLESS';
await Player.initialize(audioPlayer, MusicAPI.instance, currentQuality);
// Initialize tracker

View file

@ -30,7 +30,7 @@ export class DashDownloader {
await Promise.all(
urls.map(async (url) => {
const result = await fetch(url, { method: 'HEAD', signal });
const result = await fetch(getProxyUrl(url), { method: 'HEAD', signal });
if (result.ok) {
const contentLength = result.headers.get('Content-Length');
@ -75,7 +75,7 @@ export class DashDownloader {
onProgress?.(new SegmentedDownloadProgress(downloadedBytes, totalSize ?? undefined, i, totalSegments));
const url = urls[i];
const url = getProxyUrl(urls[i]);
const segmentResponse = await fetch(url, { signal });
if (!segmentResponse.ok) {

View file

@ -1,4 +1,5 @@
import { SegmentedDownloadProgress } from './progressEvents';
import { getProxyUrl } from './proxy-utils';
export class HlsDownloader {
constructor() {}
@ -6,12 +7,12 @@ export class HlsDownloader {
async downloadHlsStream(masterUrl, options = {}) {
const { onProgress, signal } = options;
const response = await fetch(masterUrl, { signal });
const response = await fetch(getProxyUrl(masterUrl), { signal });
const masterText = await response.text();
const variantUrl = this.getBestVariantUrl(masterUrl, masterText);
const mediaResponse = await fetch(variantUrl, { signal });
const mediaResponse = await fetch(getProxyUrl(variantUrl), { signal });
const mediaText = await mediaResponse.text();
const segments = this.parseMediaPlaylist(variantUrl, mediaText);
@ -29,7 +30,7 @@ export class HlsDownloader {
onProgress?.(new SegmentedDownloadProgress(downloadedBytes, undefined, i, totalSegments));
const segmentUrl = segments[i];
const segmentResponse = await fetch(segmentUrl, { signal });
const segmentResponse = await fetch(getProxyUrl(segmentUrl), { signal });
if (!segmentResponse.ok) {
throw new Error(`Failed to fetch segment ${i}: ${segmentResponse.status}`);

View file

@ -22,6 +22,7 @@ import {
import { audioContextManager } from './audio-context.js';
import { isIos, isSafari } from './platform-detection.js';
import { db } from './db.js';
import { getProxyUrl } from './proxy-utils.js';
import { SVG_CLOCK, SVG_ATMOS } from './icons.js';
import { UIRenderer } from './ui.js';
@ -133,17 +134,25 @@ export class Player {
},
abr: {
enabled: true,
// Start with a low bandwidth estimate (200kbps) so it plays instantly
// on slow connections and smoothly scales UP to Hi-Fi if the connection allows.
defaultBandwidthEstimate: 100000,
switchInterval: 1, // Check more frequently
bandwidthDowngradeTarget: 0.8, // Downgrade more aggressively if bandwidth drops
switchInterval: 1,
bandwidthDowngradeTarget: 0.8,
restrictToElementSize: false,
},
mediaSource: {
codecSwitchingStrategy: 'smooth',
},
});
this.shakaPlayer.getNetworkingEngine().registerRequestFilter((type, request) => {
if (type === shaka.net.NetworkingEngine.RequestType.SEGMENT) {
const uris = request.uris;
for (let i = 0; i < uris.length; i++) {
if (uris[i].includes('tidal.com')) {
uris[i] = getProxyUrl(uris[i]);
}
}
}
});
this.shakaPlayer.addEventListener('adaptation', this.updateAdaptiveQualityBadge.bind(this));
this.shakaPlayer.addEventListener('variantchanged', this.updateAdaptiveQualityBadge.bind(this));
@ -1266,6 +1275,13 @@ export class Player {
// which delays the event loop and natively adds gap/latency
await this.safePlay(activeElement);
} else {
if (this.shakaInitialized) {
try {
this.shakaPlayer.unload();
this.shakaPlayer.detach();
} catch {}
this.shakaInitialized = false;
}
activeElement.src = streamUrl;
this.applyAudioEffects();
this.updateAdaptiveQualityBadge();

View file

@ -10,6 +10,53 @@ import purgecss from 'vite-plugin-purgecss';
import { execSync } from 'child_process';
import { playwright } from '@vitest/browser-playwright';
function proxyAudioPlugin() {
return {
name: 'proxy-audio-dev',
configureServer(server) {
server.middlewares.use('/proxy-audio', async (req, res) => {
const url = new URL(req.url, 'http://localhost');
const targetUrl = url.searchParams.get('url');
if (!targetUrl) {
res.writeHead(400);
res.end('Missing url parameter');
return;
}
try {
const headers = new Headers();
headers.set('Origin', 'https://listen.tidal.com');
headers.set(
'User-Agent',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
);
const upstream = await fetch(targetUrl, {
method: req.method,
headers,
redirect: 'follow',
});
const body = Buffer.from(await upstream.arrayBuffer());
res.writeHead(upstream.status, {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, HEAD, OPTIONS',
'Access-Control-Expose-Headers': '*',
'content-type': upstream.headers.get('content-type') || 'application/octet-stream',
'content-length': body.length,
});
res.end(body);
} catch (error) {
res.writeHead(500);
res.end('Proxy Error: ' + error.message);
}
});
},
};
}
function getGitCommitHash() {
try {
return execSync('git rev-parse --short HEAD').toString().trim();
@ -80,6 +127,7 @@ export default defineConfig((_options) => {
},
},
plugins: [
proxyAudioPlugin(),
purgecss({
variables: false, // DO NOT REMOVE UNUSED VARIABLES (breaks web components like am-lyrics)
safelist: {