diff --git a/.DS_Store b/.DS_Store
index a5623a6..a11fa4b 100644
Binary files a/.DS_Store and b/.DS_Store differ
diff --git a/TV Player Avalonia/App.axaml b/TV Player Avalonia/App.axaml
index 368177d..2a20761 100644
--- a/TV Player Avalonia/App.axaml
+++ b/TV Player Avalonia/App.axaml
@@ -3,54 +3,139 @@
xmlns:vm="clr-namespace:TV_Player.AvaloniaApp.ViewModels"
xmlns:views="clr-namespace:TV_Player.AvaloniaApp.Views"
x:Class="TV_Player.AvaloniaApp.App"
- RequestedThemeVariant="Dark">
+ RequestedThemeVariant="Default">
+
+
+
+
+
+
+
+
+
+
+
-
+
-
+
-
+
+
+
+
+
diff --git a/TV Player Avalonia/MainWindow.axaml b/TV Player Avalonia/MainWindow.axaml
index 5a192d8..544eff3 100644
--- a/TV Player Avalonia/MainWindow.axaml
+++ b/TV Player Avalonia/MainWindow.axaml
@@ -10,43 +10,42 @@
MinHeight="450"
Title="TV"
WindowState="{Binding CurrentWindowState}"
- SystemDecorations="None">
+ Background="#EEF1F5">
-
-
-
-
-
+
-
-
+
-
+
+
+
diff --git a/TV Player Avalonia/TV Player Avalonia.csproj b/TV Player Avalonia/TV Player Avalonia.csproj
index a33e095..9542744 100644
--- a/TV Player Avalonia/TV Player Avalonia.csproj
+++ b/TV Player Avalonia/TV Player Avalonia.csproj
@@ -10,6 +10,7 @@
+
@@ -18,7 +19,6 @@
-
@@ -29,4 +29,33 @@
+
+
+
+ PreserveNewest
+ natives/macos/%(RecursiveDir)%(Filename)%(Extension)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/TV Player Avalonia/Views/PlayerView.axaml b/TV Player Avalonia/Views/PlayerView.axaml
index 3f4986e..a3b9748 100644
--- a/TV Player Avalonia/Views/PlayerView.axaml
+++ b/TV Player Avalonia/Views/PlayerView.axaml
@@ -5,51 +5,55 @@
-
+
-
+
+
-
-
-
+
+
+
-
-
-
-
+
+
+
+
-
-
+
+
+
-
+
-
-
+
+
@@ -57,7 +61,7 @@
-
+
@@ -65,43 +69,45 @@
-
-
-
+
+
+
-
-
-
-
+
+
+
+
-
+
-
-
-
+
+
+
-
-
+
diff --git a/TV Player Avalonia/Views/PlayerView.axaml.cs b/TV Player Avalonia/Views/PlayerView.axaml.cs
index 721e19c..57df67c 100644
--- a/TV Player Avalonia/Views/PlayerView.axaml.cs
+++ b/TV Player Avalonia/Views/PlayerView.axaml.cs
@@ -1,7 +1,13 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
+using Avalonia.Threading;
using LibVLCSharp.Shared;
+using System.Diagnostics;
using System.ComponentModel;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Web;
using TV_Player.AvaloniaApp.ViewModels;
namespace TV_Player.AvaloniaApp.Views;
@@ -10,12 +16,17 @@ public partial class PlayerView : UserControl
{
private LibVLC? _libVlc;
private MediaPlayer? _mediaPlayer;
+ private Media? _currentMedia;
private PlayerViewModel? _viewModel;
private bool _initialized;
+ private CancellationTokenSource? _bufferingWatchdogCts;
+ private string? _activeStreamUrl;
+ private bool _fallbackAttempted;
public PlayerView()
{
AvaloniaXamlLoader.Load(this);
+ Log("PlayerView ctor");
DataContextChanged += OnDataContextChanged;
AttachedToVisualTree += OnAttachedToVisualTree;
DetachedFromVisualTree += OnDetachedFromVisualTree;
@@ -26,35 +37,178 @@ public partial class PlayerView : UserControl
if (_initialized)
return;
- Core.Initialize();
- _libVlc = new LibVLC(enableDebugLogs: true);
- _mediaPlayer = new MediaPlayer(_libVlc)
+ Log("Attach to visual tree -> initializing VLC");
+ try
{
- EnableHardwareDecoding = true
+ string? macPluginDirectory = null;
+ if (OperatingSystem.IsMacOS())
+ {
+ if (!TryGetMacLibVlcPaths(out var libVlcDirectory, out macPluginDirectory))
+ {
+ const string message = "VLC native dependencies not found. Build once with internet to auto-bundle natives/macos (lib + plugins).";
+ Log(message);
+ _viewModel?.SetPlaybackStatus(message);
+ return;
+ }
+
+ if (!string.IsNullOrWhiteSpace(macPluginDirectory) && Directory.Exists(macPluginDirectory))
+ {
+ Environment.SetEnvironmentVariable("VLC_PLUGIN_PATH", macPluginDirectory);
+ Log($"VLC_PLUGIN_PATH set to: {macPluginDirectory}");
+ }
+
+ var dyldLibraryPath = PrependPath(Environment.GetEnvironmentVariable("DYLD_LIBRARY_PATH"), libVlcDirectory);
+ var dyldFallbackPath = PrependPath(Environment.GetEnvironmentVariable("DYLD_FALLBACK_LIBRARY_PATH"), libVlcDirectory);
+ Environment.SetEnvironmentVariable("DYLD_LIBRARY_PATH", dyldLibraryPath);
+ Environment.SetEnvironmentVariable("DYLD_FALLBACK_LIBRARY_PATH", dyldFallbackPath);
+ Log($"DYLD_LIBRARY_PATH set to: {dyldLibraryPath}");
+ Log($"DYLD_FALLBACK_LIBRARY_PATH set to: {dyldFallbackPath}");
+
+ Log($"Core.Initialize(path) start: {libVlcDirectory}");
+ Core.Initialize(libVlcDirectory);
+ Log("Core.Initialize(path) completed");
+ }
+ else
+ {
+ Log("Core.Initialize() start");
+ Core.Initialize();
+ Log("Core.Initialize() completed");
+ }
+
+ _libVlc = new LibVLC(enableDebugLogs: true);
+ Log("LibVLC instance created");
+
+ _mediaPlayer = new MediaPlayer(_libVlc)
+ {
+ // Software decode is more reliable for mixed IPTV codecs on macOS.
+ EnableHardwareDecoding = !OperatingSystem.IsMacOS()
+ };
+ Log($"MediaPlayer created. HW decoding enabled: {_mediaPlayer.EnableHardwareDecoding}");
+
+ _mediaPlayer.Buffering += OnMediaPlayerBuffering;
+ _mediaPlayer.Playing += OnMediaPlayerPlaying;
+ _mediaPlayer.EncounteredError += OnMediaPlayerEncounteredError;
+ _mediaPlayer.EndReached += OnMediaPlayerEndReached;
+ _mediaPlayer.Stopped += OnMediaPlayerStopped;
+
+ VideoView.MediaPlayer = _mediaPlayer;
+ _initialized = true;
+ Log("VideoView.MediaPlayer assigned");
+ PlayCurrentStream();
+ }
+ catch (System.Exception ex)
+ {
+ Log($"VLC initialization failed: {ex}");
+ _viewModel?.SetPlaybackStatus($"VLC init failed: {ex.Message}");
+ }
+ }
+
+ private static bool TryGetMacLibVlcPaths(out string libDirectory, out string pluginDirectory)
+ {
+ var outputBase = AppContext.BaseDirectory;
+ var candidates = new[]
+ {
+ outputBase,
+ Path.Combine(outputBase, "natives", "macos", "lib"),
+ Path.Combine(outputBase, "lib"),
+ Path.Combine(outputBase, "libvlc", "osx-x64", "lib"),
+ "/Applications/VLC.app/Contents/MacOS/lib",
+ "/opt/homebrew/lib",
+ "/usr/local/lib"
};
- VideoView.MediaPlayer = _mediaPlayer;
- _initialized = true;
- PlayCurrentStream();
+ foreach (var candidateDir in candidates)
+ {
+ var libvlc = Path.Combine(candidateDir, "libvlc.dylib");
+ var libvlccore = Path.Combine(candidateDir, "libvlccore.dylib");
+ if (File.Exists(libvlc) && File.Exists(libvlccore))
+ {
+ var pluginCandidates = new[]
+ {
+ Path.Combine(candidateDir, "plugins"),
+ Path.Combine(Path.GetDirectoryName(candidateDir) ?? string.Empty, "plugins")
+ };
+
+ pluginDirectory = string.Empty;
+ foreach (var candidatePluginDir in pluginCandidates)
+ {
+ if (Directory.Exists(candidatePluginDir))
+ {
+ pluginDirectory = candidatePluginDir;
+ break;
+ }
+ }
+
+ libDirectory = candidateDir;
+ Log($"Found macOS VLC libs in: {candidateDir}");
+ if (!string.IsNullOrWhiteSpace(pluginDirectory))
+ {
+ Log($"Found macOS VLC plugins in: {pluginDirectory}");
+ }
+ else
+ {
+ Log("macOS VLC plugins directory not found next to libraries");
+ }
+ return true;
+ }
+ }
+
+ libDirectory = string.Empty;
+ pluginDirectory = string.Empty;
+ Log("macOS VLC libs not found. Checked directories: " + string.Join("; ", candidates));
+ return false;
+ }
+
+ private static string PrependPath(string? existingPaths, string pathToPrepend)
+ {
+ if (string.IsNullOrWhiteSpace(pathToPrepend))
+ return existingPaths ?? string.Empty;
+
+ if (string.IsNullOrWhiteSpace(existingPaths))
+ return pathToPrepend;
+
+ var parts = existingPaths.Split(':', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
+ if (parts.Contains(pathToPrepend))
+ return existingPaths;
+
+ return pathToPrepend + ":" + existingPaths;
}
private void OnDetachedFromVisualTree(object? sender, Avalonia.VisualTreeAttachmentEventArgs e)
{
+ Log("Detach from visual tree -> disposing player resources");
if (_viewModel != null)
{
_viewModel.PropertyChanged -= OnViewModelPropertyChanged;
}
+ if (_mediaPlayer != null)
+ {
+ _mediaPlayer.Buffering -= OnMediaPlayerBuffering;
+ _mediaPlayer.Playing -= OnMediaPlayerPlaying;
+ _mediaPlayer.EncounteredError -= OnMediaPlayerEncounteredError;
+ _mediaPlayer.EndReached -= OnMediaPlayerEndReached;
+ _mediaPlayer.Stopped -= OnMediaPlayerStopped;
+ }
+
_mediaPlayer?.Stop();
+ _bufferingWatchdogCts?.Cancel();
+ _bufferingWatchdogCts?.Dispose();
+ _currentMedia?.Dispose();
_mediaPlayer?.Dispose();
_libVlc?.Dispose();
+ _bufferingWatchdogCts = null;
+ _currentMedia = null;
_mediaPlayer = null;
_libVlc = null;
+ _activeStreamUrl = null;
+ _fallbackAttempted = false;
_initialized = false;
}
private void OnDataContextChanged(object? sender, System.EventArgs e)
{
+ Log($"DataContext changed. New type: {DataContext?.GetType().Name ?? "null"}");
if (_viewModel != null)
{
_viewModel.PropertyChanged -= OnViewModelPropertyChanged;
@@ -73,6 +227,7 @@ public partial class PlayerView : UserControl
{
if (e.PropertyName == nameof(PlayerViewModel.StreamUrl))
{
+ Log("ViewModel StreamUrl changed -> replaying stream");
PlayCurrentStream();
}
}
@@ -80,30 +235,273 @@ public partial class PlayerView : UserControl
private void PlayCurrentStream()
{
if (!_initialized || _viewModel == null || _mediaPlayer == null || _libVlc == null)
+ {
+ Log($"PlayCurrentStream skipped: initialized={_initialized}, hasVm={_viewModel != null}, hasPlayer={_mediaPlayer != null}, hasLibVlc={_libVlc != null}");
return;
+ }
if (string.IsNullOrWhiteSpace(_viewModel.StreamUrl))
{
+ Log("PlayCurrentStream aborted: empty StreamUrl");
_viewModel.SetPlaybackStatus("No stream URL available for this channel.");
return;
}
try
{
- if (_mediaPlayer.Media != null)
- {
- _mediaPlayer.Stop();
- _mediaPlayer.Media.Dispose();
- }
+ var streamUrl = _viewModel.StreamUrl.Trim();
+ Log($"PlayCurrentStream start. Url={SanitizeUrlForLog(streamUrl)}");
+ _activeStreamUrl = streamUrl;
+ _fallbackAttempted = false;
- var media = new Media(_libVlc, new Uri(_viewModel.StreamUrl));
- _mediaPlayer.Media = media;
- _mediaPlayer.Play();
- _viewModel.SetPlaybackStatus("Playing with embedded VLC.");
+ _mediaPlayer.Stop();
+ _currentMedia?.Dispose();
+ _currentMedia = null;
+
+ _viewModel.SetPlaybackStatus("Buffering stream...");
+ StartBufferingWatchdog();
+
+ var started = StartPlayback(streamUrl, fallback: false);
+ Log($"Primary playback start result: {started}");
+ if (!started)
+ {
+ _viewModel.SetPlaybackStatus("Stream failed to start. Try another channel or Open Externally.");
+ }
}
catch (System.Exception ex)
{
+ Log($"PlayCurrentStream exception: {ex}");
_viewModel.SetPlaybackStatus($"Embedded playback failed: {ex.Message}. Use Open Externally as a fallback.");
}
}
+
+ private static Media BuildMediaFromStreamUrl(LibVLC libVlc, string rawStreamUrl)
+ {
+ // Common IPTV format: http://.../stream.m3u8|user-agent=...&referer=...
+ var splitIndex = rawStreamUrl.IndexOf('|');
+ if (splitIndex <= 0)
+ {
+ return new Media(libVlc, rawStreamUrl, FromType.FromLocation);
+ }
+
+ var baseUrl = rawStreamUrl[..splitIndex].Trim();
+ var optionsSegment = rawStreamUrl[(splitIndex + 1)..].Trim();
+ var media = new Media(libVlc, baseUrl, FromType.FromLocation);
+
+ if (string.IsNullOrWhiteSpace(optionsSegment))
+ {
+ return media;
+ }
+
+ var pairs = optionsSegment.Split('&', System.StringSplitOptions.RemoveEmptyEntries | System.StringSplitOptions.TrimEntries);
+ foreach (var pair in pairs)
+ {
+ var equalsIndex = pair.IndexOf('=');
+ if (equalsIndex <= 0 || equalsIndex == pair.Length - 1)
+ {
+ continue;
+ }
+
+ var key = pair[..equalsIndex].Trim().ToLowerInvariant();
+ var value = HttpUtility.UrlDecode(pair[(equalsIndex + 1)..].Trim());
+ if (string.IsNullOrWhiteSpace(value))
+ {
+ continue;
+ }
+
+ switch (key)
+ {
+ case "user-agent":
+ case "http-user-agent":
+ media.AddOption($":http-user-agent={value}");
+ break;
+ case "referer":
+ case "referrer":
+ case "http-referrer":
+ case "http-referer":
+ media.AddOption($":http-referrer={value}");
+ break;
+ case "origin":
+ media.AddOption($":http-header=Origin={value}");
+ break;
+ case "cookie":
+ case "cookies":
+ media.AddOption($":http-header=Cookie={value}");
+ break;
+ }
+ }
+
+ return media;
+ }
+
+ private bool StartPlayback(string streamUrl, bool fallback)
+ {
+ if (_mediaPlayer == null || _libVlc == null)
+ return false;
+
+ Log($"StartPlayback called. fallback={fallback}, url={SanitizeUrlForLog(streamUrl)}");
+
+ _currentMedia?.Dispose();
+ _currentMedia = BuildMediaFromStreamUrl(_libVlc, streamUrl);
+ _currentMedia.AddOption(":network-caching=1500");
+ _currentMedia.AddOption(":live-caching=1500");
+ _currentMedia.AddOption(":http-reconnect=true");
+ _currentMedia.AddOption(":codec=avcodec");
+ if (OperatingSystem.IsMacOS())
+ {
+ _currentMedia.AddOption(":avcodec-hw=none");
+ }
+
+ if (fallback)
+ {
+ _currentMedia.AddOption(":demux=any");
+ _currentMedia.AddOption(":hls-segment-threads=4");
+ _currentMedia.AddOption(":http-continuous=true");
+ Log("Fallback options applied: demux=any, hls-segment-threads=4, http-continuous=true");
+ }
+
+ var started = _mediaPlayer.Play(_currentMedia);
+ Log($"MediaPlayer.Play returned: {started}");
+ return started;
+ }
+
+ private void StartBufferingWatchdog()
+ {
+ _bufferingWatchdogCts?.Cancel();
+ _bufferingWatchdogCts?.Dispose();
+ _bufferingWatchdogCts = new CancellationTokenSource();
+ var token = _bufferingWatchdogCts.Token;
+ Log("Buffering watchdog started (12s)");
+
+ _ = Task.Run(async () =>
+ {
+ try
+ {
+ await Task.Delay(TimeSpan.FromSeconds(12), token);
+ }
+ catch (TaskCanceledException)
+ {
+ return;
+ }
+
+ if (token.IsCancellationRequested)
+ return;
+
+ if (_mediaPlayer == null || _viewModel == null || string.IsNullOrWhiteSpace(_activeStreamUrl))
+ return;
+
+ if (_mediaPlayer.IsPlaying || _fallbackAttempted)
+ return;
+
+ _fallbackAttempted = true;
+ Log("Buffering watchdog triggered fallback attempt");
+ Dispatcher.UIThread.Post(() =>
+ {
+ if (_viewModel == null)
+ return;
+
+ _viewModel.SetPlaybackStatus("Still buffering, retrying with fallback settings...");
+ var started = StartPlayback(_activeStreamUrl!, fallback: true);
+ Log($"Fallback playback start result: {started}");
+ if (!started)
+ {
+ _viewModel.SetPlaybackStatus("Fallback playback failed. Try Open Externally for this channel.");
+ }
+ });
+ }, token);
+ }
+
+ private void OnMediaPlayerBuffering(object? sender, MediaPlayerBufferingEventArgs e)
+ {
+ if (_viewModel == null)
+ return;
+
+ if (e.Cache < 1 || ((int)e.Cache % 10) == 0)
+ {
+ Log($"MediaPlayer buffering: {e.Cache:0}%");
+ }
+
+ Dispatcher.UIThread.Post(() =>
+ {
+ _viewModel.SetPlaybackStatus($"Buffering stream... {e.Cache:0}%");
+ });
+ }
+
+ private void OnMediaPlayerPlaying(object? sender, System.EventArgs e)
+ {
+ if (_viewModel == null)
+ return;
+
+ Dispatcher.UIThread.Post(() =>
+ {
+ _bufferingWatchdogCts?.Cancel();
+ Log("MediaPlayer event: Playing");
+ _viewModel.SetPlaybackStatus(string.Empty);
+ });
+ }
+
+ private void OnMediaPlayerEncounteredError(object? sender, System.EventArgs e)
+ {
+ if (_viewModel == null)
+ return;
+
+ Dispatcher.UIThread.Post(() =>
+ {
+ _bufferingWatchdogCts?.Cancel();
+ Log("MediaPlayer event: EncounteredError");
+ _viewModel.SetPlaybackStatus("Playback error. The stream may be unavailable or unsupported.");
+ });
+ }
+
+ private void OnMediaPlayerEndReached(object? sender, System.EventArgs e)
+ {
+ if (_viewModel == null)
+ return;
+
+ Dispatcher.UIThread.Post(() =>
+ {
+ _bufferingWatchdogCts?.Cancel();
+ Log("MediaPlayer event: EndReached");
+ _viewModel.SetPlaybackStatus("Stream ended.");
+ });
+ }
+
+ private void OnMediaPlayerStopped(object? sender, System.EventArgs e)
+ {
+ if (_viewModel == null)
+ return;
+
+ Dispatcher.UIThread.Post(() =>
+ {
+ _bufferingWatchdogCts?.Cancel();
+ Log("MediaPlayer event: Stopped");
+ if (string.IsNullOrWhiteSpace(_viewModel.PlaybackStatus))
+ {
+ _viewModel.SetPlaybackStatus("Playback stopped.");
+ }
+ });
+ }
+
+ private static string SanitizeUrlForLog(string url)
+ {
+ if (string.IsNullOrWhiteSpace(url))
+ return "";
+
+ var trimmed = url.Trim();
+ var tokenIndex = trimmed.IndexOf("token=", System.StringComparison.OrdinalIgnoreCase);
+ if (tokenIndex >= 0)
+ {
+ return "";
+ }
+
+ var splitIndex = trimmed.IndexOf('|');
+ return splitIndex > 0 ? trimmed[..splitIndex] + "|" : trimmed;
+ }
+
+ private static void Log(string message)
+ {
+ var line = $"[PlayerView] {DateTime.Now:HH:mm:ss.fff} {message}";
+ Debug.WriteLine(line);
+ Console.WriteLine(line);
+ }
}
diff --git a/TV Player Avalonia/Views/PlaylistsGroupView.axaml b/TV Player Avalonia/Views/PlaylistsGroupView.axaml
index 16ef0d6..ee5b3c0 100644
--- a/TV Player Avalonia/Views/PlaylistsGroupView.axaml
+++ b/TV Player Avalonia/Views/PlaylistsGroupView.axaml
@@ -19,13 +19,10 @@
Height="70"
Margin="8,6"
Classes="card"
- Background="#B0000000"
- BorderBrush="Yellow"
- BorderThickness="2"
Command="{Binding DataContext.SelectPlaylistCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
CommandParameter="{Binding}">
diff --git a/TV Player Avalonia/Views/ProgramsListView.axaml b/TV Player Avalonia/Views/ProgramsListView.axaml
index 66ae9b7..9cf73e5 100644
--- a/TV Player Avalonia/Views/ProgramsListView.axaml
+++ b/TV Player Avalonia/Views/ProgramsListView.axaml
@@ -1,5 +1,6 @@
-
+
-
-
+
+
-
-
-
+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
diff --git a/TV Player Avalonia/scripts/fetch-vlc-macos.sh b/TV Player Avalonia/scripts/fetch-vlc-macos.sh
new file mode 100644
index 0000000..3404b1d
--- /dev/null
+++ b/TV Player Avalonia/scripts/fetch-vlc-macos.sh
@@ -0,0 +1,68 @@
+#!/bin/sh
+set -eu
+
+project_dir="$1"
+lib_dir="$project_dir/natives/macos/lib"
+plugins_dir="$project_dir/natives/macos/plugins"
+
+if [ -f "$lib_dir/libvlccore.dylib" ] && [ -f "$lib_dir/libvlc.dylib" ] && [ -d "$plugins_dir" ]; then
+ echo "[VLC] macOS native libs and plugins already bundled"
+ exit 0
+fi
+
+download_dir="$project_dir/obj/vlc-download"
+mount_point="$download_dir/mount"
+mkdir -p "$download_dir" "$project_dir/natives/macos"
+
+arch="$(uname -m)"
+case "$arch" in
+ arm64|aarch64)
+ dmg_name="vlc-3.0.21-arm64.dmg"
+ ;;
+ x86_64)
+ dmg_name="vlc-3.0.21-intel64.dmg"
+ ;;
+ *)
+ echo "[VLC] Unsupported macOS architecture: $arch"
+ exit 1
+ ;;
+esac
+
+url="https://get.videolan.org/vlc/3.0.21/macosx/$dmg_name"
+dmg_path="$download_dir/$dmg_name"
+
+if [ ! -f "$dmg_path" ]; then
+ echo "[VLC] Downloading $url"
+ curl -L --fail --retry 3 --retry-delay 2 -o "$dmg_path" "$url"
+fi
+
+if [ -d "$mount_point" ]; then
+ hdiutil detach "$mount_point" >/dev/null 2>&1 || true
+fi
+mkdir -p "$mount_point"
+
+cleanup() {
+ hdiutil detach "$mount_point" >/dev/null 2>&1 || true
+}
+trap cleanup EXIT
+
+echo "[VLC] Mounting DMG"
+hdiutil attach "$dmg_path" -nobrowse -readonly -mountpoint "$mount_point" >/dev/null
+
+if [ ! -d "$mount_point/VLC.app/Contents/MacOS/lib" ]; then
+ echo "[VLC] Could not find VLC libs inside mounted image"
+ exit 1
+fi
+
+echo "[VLC] Copying native libraries and plugins"
+rm -rf "$project_dir/natives/macos/lib"
+rm -rf "$project_dir/natives/macos/plugins"
+cp -R "$mount_point/VLC.app/Contents/MacOS/lib" "$project_dir/natives/macos/"
+if [ -d "$mount_point/VLC.app/Contents/MacOS/plugins" ]; then
+ cp -R "$mount_point/VLC.app/Contents/MacOS/plugins" "$project_dir/natives/macos/"
+fi
+
+hdiutil detach "$mount_point" >/dev/null 2>&1 || true
+trap - EXIT
+
+echo "[VLC] Bundled macOS native runtime under $project_dir/natives/macos"
\ No newline at end of file