···11import { Icons } from "@/components/Icon";
22import { VideoPlayerButton } from "@/components/player/internals/Button";
33import { usePlayerStore } from "@/stores/player/store";
44+import { isSafari } from "@/utils/detectFeatures";
4556export function Airplay() {
67 const canAirplay = usePlayerStore((s) => s.interface.canAirplay);
78 const display = usePlayerStore((s) => s.display);
8999- if (!canAirplay) return null;
1010+ // Show Airplay button on Safari browsers (which support AirPlay natively)
1111+ // or when the webkit event has confirmed availability
1212+ if (!canAirplay && !isSafari) return null;
10131114 return (
1215 <VideoPlayerButton
+2
src/components/player/atoms/Chromecast.tsx
···3838 const context = castFramework.CastContext.getInstance();
3939 const updateVisibility = () => {
4040 const state = context.getCastState();
4141+ // Hide only if we know for sure there are no devices available
4242+ // Show the button for other states (NOT_CONNECTED, CONNECTING, CONNECTED)
4143 setCastHidden(state === castFramework.CastState.NO_DEVICES_AVAILABLE);
4244 };
4345
+44-13
src/components/player/display/base.ts
···784784 proxiedUrl = source.url; // Already proxied or no headers needed
785785 }
786786 } else if (source?.type === "mp4") {
787787- // TODO: Implement MP4 proxy for protected streams
788788- const hasHeaders =
789789- source.headers && Object.keys(source.headers).length > 0;
790790- if (hasHeaders) {
787787+ const allHeaders = {
788788+ ...source.preferredHeaders,
789789+ ...source.headers,
790790+ };
791791+ const hasHeaders = Object.keys(allHeaders).length > 0;
792792+ if (!isUrlAlreadyProxied(source.url) && hasHeaders) {
791793 // Use MP4 proxy for streams with headers
792792- proxiedUrl = createMP4ProxyUrl(source.url, source.headers || {});
794794+ proxiedUrl = createMP4ProxyUrl(source.url, allHeaders);
793795 } else {
794796 proxiedUrl = source.url;
795797 }
796798 }
797799800800+ // Function to restore original URL
801801+ const restoreOriginalUrl = () => {
802802+ if (source?.type === "hls") {
803803+ if (hls && originalUrl) {
804804+ hls.loadSource(originalUrl);
805805+ }
806806+ } else if (originalUrl) {
807807+ videoPlayer.src = originalUrl;
808808+ }
809809+ };
810810+811811+ // Function to check airplay state and restore if needed
812812+ const checkAirplayState = () => {
813813+ const isWireless = videoPlayer.webkitCurrentPlaybackTargetIsWireless;
814814+ if (!isWireless) {
815815+ // Airplay didn't start or ended, restore original URL
816816+ restoreOriginalUrl();
817817+ }
818818+ };
819819+798820 if (proxiedUrl && proxiedUrl !== originalUrl) {
799799- // Temporarily set the proxied URL for Airplay
821821+ // Set the proxied URL for Airplay
800822 if (source?.type === "hls") {
801823 if (hls) {
802824 hls.loadSource(proxiedUrl);
···809831 setTimeout(() => {
810832 videoPlayer.webkitShowPlaybackTargetPicker();
811833812812- // Restore original URL after a short delay
834834+ // Check airplay state after user interaction
835835+ // Give user time to select device, then check if airplay started
813836 setTimeout(() => {
814814- if (source?.type === "hls") {
815815- if (hls && originalUrl) {
816816- hls.loadSource(originalUrl);
817817- }
818818- } else if (originalUrl) {
819819- videoPlayer.src = originalUrl;
837837+ checkAirplayState();
838838+ }, 2000);
839839+840840+ // Set up periodic check for airplay state changes
841841+ const airplayCheckInterval = setInterval(() => {
842842+ const isWireless =
843843+ videoPlayer.webkitCurrentPlaybackTargetIsWireless;
844844+ if (!isWireless) {
845845+ // Airplay ended, restore original URL
846846+ restoreOriginalUrl();
847847+ clearInterval(airplayCheckInterval);
820848 }
821849 }, 1000);
850850+851851+ // Clear interval after 5 minutes as safety measure
852852+ setTimeout(() => clearInterval(airplayCheckInterval), 300000);
822853 }, 100);
823854 } else {
824855 // No proxying needed, just trigger Airplay