···1212} from "@/components/player/display/displayInterface";
1313import { handleBuffered } from "@/components/player/utils/handleBuffered";
1414import { getMediaErrorDetails } from "@/components/player/utils/mediaErrorDetails";
1515+import {
1616+ createM3U8ProxyUrl,
1717+ createMP4ProxyUrl,
1818+ isUrlAlreadyProxied,
1919+} from "@/components/player/utils/proxy";
1520import { useLanguageStore } from "@/stores/language";
1621import {
1722 LoadableSource,
···579584 },
580585 startAirplay() {
581586 const videoPlayer = videoElement as any;
582582- if (videoPlayer && videoPlayer.webkitShowPlaybackTargetPicker) {
587587+ if (!videoPlayer || !videoPlayer.webkitShowPlaybackTargetPicker) return;
588588+589589+ if (!source) {
590590+ // No source loaded, just trigger Airplay
591591+ videoPlayer.webkitShowPlaybackTargetPicker();
592592+ return;
593593+ }
594594+595595+ // Store the original URL to restore later
596596+ const originalUrl =
597597+ source?.type === "hls" ? hls?.url || source.url : videoPlayer.src;
598598+599599+ let proxiedUrl: string | null = null;
600600+601601+ if (source?.type === "hls") {
602602+ // Only proxy HLS streams if they need it:
603603+ // 1. Not already proxied AND
604604+ // 2. Has headers (either preferredHeaders or headers)
605605+ const allHeaders = {
606606+ ...source.preferredHeaders,
607607+ ...source.headers,
608608+ };
609609+ const hasHeaders = Object.keys(allHeaders).length > 0;
610610+611611+ // Don't create proxy URL if it's already using the proxy
612612+ if (!isUrlAlreadyProxied(source.url) && hasHeaders) {
613613+ proxiedUrl = createM3U8ProxyUrl(source.url, allHeaders);
614614+ } else {
615615+ proxiedUrl = source.url; // Already proxied or no headers needed
616616+ }
617617+ } else if (source?.type === "mp4") {
618618+ // TODO: Implement MP4 proxy for protected streams
619619+ const hasHeaders =
620620+ source.headers && Object.keys(source.headers).length > 0;
621621+ if (hasHeaders) {
622622+ // Use MP4 proxy for streams with headers
623623+ proxiedUrl = createMP4ProxyUrl(source.url, source.headers || {});
624624+ } else {
625625+ proxiedUrl = source.url;
626626+ }
627627+ }
628628+629629+ if (proxiedUrl && proxiedUrl !== originalUrl) {
630630+ // Temporarily set the proxied URL for Airplay
631631+ if (source?.type === "hls") {
632632+ if (hls) {
633633+ hls.loadSource(proxiedUrl);
634634+ }
635635+ } else {
636636+ videoPlayer.src = proxiedUrl;
637637+ }
638638+639639+ // Small delay to ensure the URL is set before triggering Airplay
640640+ setTimeout(() => {
641641+ videoPlayer.webkitShowPlaybackTargetPicker();
642642+643643+ // Restore original URL after a short delay
644644+ setTimeout(() => {
645645+ if (source?.type === "hls") {
646646+ if (hls && originalUrl) {
647647+ hls.loadSource(originalUrl);
648648+ }
649649+ } else if (originalUrl) {
650650+ videoPlayer.src = originalUrl;
651651+ }
652652+ }, 1000);
653653+ }, 100);
654654+ } else {
655655+ // No proxying needed, just trigger Airplay
583656 videoPlayer.webkitShowPlaybackTargetPicker();
584657 }
585658 },
+58
src/components/player/utils/proxy.ts
···11+import { getLoadbalancedM3U8ProxyUrl } from "@/backend/providers/fetchers";
22+import { getM3U8ProxyUrls } from "@/utils/proxyUrls";
33+44+/**
55+ * Creates a proxied M3U8 URL for HLS streams using a random proxy from config
66+ * @param url - The original M3U8 URL to proxy
77+ * @param headers - Headers to include with the request
88+ * @returns The proxied M3U8 URL
99+ */
1010+export function createM3U8ProxyUrl(
1111+ url: string,
1212+ headers: Record<string, string> = {},
1313+): string {
1414+ // Get a random M3U8 proxy URL from the configuration
1515+ const proxyBaseUrl = getLoadbalancedM3U8ProxyUrl();
1616+1717+ if (!proxyBaseUrl) {
1818+ console.warn("No M3U8 proxy URLs available in configuration");
1919+ return url; // Fallback to original URL
2020+ }
2121+2222+ const encodedUrl = encodeURIComponent(url);
2323+ const encodedHeaders = encodeURIComponent(JSON.stringify(headers));
2424+ return `${proxyBaseUrl}/m3u8-proxy?url=${encodedUrl}${headers ? `&headers=${encodedHeaders}` : ""}`;
2525+}
2626+2727+/**
2828+ * TODO: Creates a proxied MP4 URL for MP4 streams
2929+ * @param url - The original MP4 URL to proxy
3030+ * @param headers - Headers to include with the request
3131+ * @returns The proxied MP4 URL
3232+ */
3333+export function createMP4ProxyUrl(
3434+ url: string,
3535+ _headers: Record<string, string> = {},
3636+): string {
3737+ // TODO: Implement MP4 proxy for protected streams
3838+ // This would need a separate MP4 proxy service that can handle headers
3939+ // For now, return the original URL
4040+ console.warn("MP4 proxy not yet implemented - using original URL");
4141+ return url;
4242+}
4343+4444+/**
4545+ * Checks if a URL is already using one of the configured M3U8 proxy services
4646+ * @param url - The URL to check
4747+ * @returns True if the URL is already proxied, false otherwise
4848+ */
4949+export function isUrlAlreadyProxied(url: string): boolean {
5050+ // Check if URL contains the m3u8-proxy pattern
5151+ if (url.includes("/m3u8-proxy?url=")) {
5252+ return true;
5353+ }
5454+5555+ // Also check if URL starts with any of the configured proxy URLs
5656+ const proxyUrls = getM3U8ProxyUrls();
5757+ return proxyUrls.some((proxyUrl) => url.startsWith(proxyUrl));
5858+}