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> </div>
<select id="streaming-quality-setting"> <select id="streaming-quality-setting">
<option value="auto">Auto (Adaptive)</option> <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="LOSSLESS">Lossless (16-bit)</option>
<option value="HIGH">AAC 320kbps</option> <option value="HIGH">AAC 320kbps</option>
<option value="LOW">AAC 96kbps</option> <option value="LOW">AAC 96kbps</option>

View file

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

View file

@ -467,31 +467,7 @@ document.addEventListener('DOMContentLoaded', async () => {
const audioPlayer = document.getElementById('audio-player'); const audioPlayer = document.getElementById('audio-player');
// i love ios and macos!!!! webkit fucking SUCKS BULLSHIT sorry ios/macos heads yall getting lossless only playback // 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. const currentQuality = localStorage.getItem('playback-quality') || 'HI_RES_LOSSLESS';
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';
await Player.initialize(audioPlayer, MusicAPI.instance, currentQuality); await Player.initialize(audioPlayer, MusicAPI.instance, currentQuality);
// Initialize tracker // Initialize tracker

View file

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

View file

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

View file

@ -22,6 +22,7 @@ import {
import { audioContextManager } from './audio-context.js'; import { audioContextManager } from './audio-context.js';
import { isIos, isSafari } from './platform-detection.js'; import { isIos, isSafari } from './platform-detection.js';
import { db } from './db.js'; import { db } from './db.js';
import { getProxyUrl } from './proxy-utils.js';
import { SVG_CLOCK, SVG_ATMOS } from './icons.js'; import { SVG_CLOCK, SVG_ATMOS } from './icons.js';
import { UIRenderer } from './ui.js'; import { UIRenderer } from './ui.js';
@ -133,17 +134,25 @@ export class Player {
}, },
abr: { abr: {
enabled: true, 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, defaultBandwidthEstimate: 100000,
switchInterval: 1, // Check more frequently switchInterval: 1,
bandwidthDowngradeTarget: 0.8, // Downgrade more aggressively if bandwidth drops bandwidthDowngradeTarget: 0.8,
restrictToElementSize: false, restrictToElementSize: false,
}, },
mediaSource: { mediaSource: {
codecSwitchingStrategy: 'smooth', 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('adaptation', this.updateAdaptiveQualityBadge.bind(this));
this.shakaPlayer.addEventListener('variantchanged', 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 // which delays the event loop and natively adds gap/latency
await this.safePlay(activeElement); await this.safePlay(activeElement);
} else { } else {
if (this.shakaInitialized) {
try {
this.shakaPlayer.unload();
this.shakaPlayer.detach();
} catch {}
this.shakaInitialized = false;
}
activeElement.src = streamUrl; activeElement.src = streamUrl;
this.applyAudioEffects(); this.applyAudioEffects();
this.updateAdaptiveQualityBadge(); this.updateAdaptiveQualityBadge();

View file

@ -10,6 +10,53 @@ import purgecss from 'vite-plugin-purgecss';
import { execSync } from 'child_process'; import { execSync } from 'child_process';
import { playwright } from '@vitest/browser-playwright'; 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() { function getGitCommitHash() {
try { try {
return execSync('git rev-parse --short HEAD').toString().trim(); return execSync('git rev-parse --short HEAD').toString().trim();
@ -80,6 +127,7 @@ export default defineConfig((_options) => {
}, },
}, },
plugins: [ plugins: [
proxyAudioPlugin(),
purgecss({ purgecss({
variables: false, // DO NOT REMOVE UNUSED VARIABLES (breaks web components like am-lyrics) variables: false, // DO NOT REMOVE UNUSED VARIABLES (breaks web components like am-lyrics)
safelist: { safelist: {