using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using Android.App;
using Android.Content;
using Android.Util;
using Android.Views;
using Android.Widget;
using AndroidX.Media3.Common;
using AndroidX.Media3.Common.Text;
using AndroidX.Media3.Common.Util;
using AndroidX.Media3.DataSource;
using AndroidX.Media3.ExoPlayer;
using AndroidX.Media3.ExoPlayer.Source;
using AndroidX.Media3.Session;
using AndroidX.Media3.UI;
using CommunityToolkit.Maui.Media.Services;
using CommunityToolkit.Maui.Services;
using CommunityToolkit.Maui.Views;
using Microsoft.Extensions.Logging;
using AudioAttributes = AndroidX.Media3.Common.AudioAttributes;
using DeviceInfo = AndroidX.Media3.Common.DeviceInfo;
using MediaMetadata = AndroidX.Media3.Common.MediaMetadata;
namespace CommunityToolkit.Maui.Core.Views;
public partial class MediaManager : Java.Lang.Object, IPlayerListener
{
const int bufferState = 2;
const int readyState = 3;
const int endedState = 4;
readonly HttpClient client = new();
readonly SemaphoreSlim seekToSemaphoreSlim = new(1, 1);
bool isAndroidForegroundServiceEnabled = false;
double? previousSpeed;
float volumeBeforeMute = 1;
TaskCompletionSource? seekToTaskCompletionSource;
CancellationTokenSource? cancellationTokenSource;
MediaSession? session;
MediaItem.Builder? mediaItem;
BoundServiceConnection? connection;
StreamDataSourceFactory? currentStreamDataSourceFactory;
///
/// The platform native counterpart of .
///
protected PlayerView? PlayerView { get; set; }
///
/// Occurs when ExoPlayer changes the playback parameters.
///
/// Object containing the new playback parameter values.
///
/// This is part of the implementation.
/// While this method does not seem to have any references, it's invoked at runtime.
///
public void OnPlaybackParametersChanged(PlaybackParameters? playbackParameters)
{
if (playbackParameters is null || AreFloatingPointNumbersEqual(playbackParameters.Speed, MediaElement.Speed))
{
return;
}
MediaElement.Speed = playbackParameters.Speed;
}
public void UpdateNotifications()
{
if (connection?.Binder?.Service is null || !isAndroidForegroundServiceEnabled)
{
return;
}
if (session is not null && Player is not null)
{
connection.Binder.Service.UpdateNotifications(session, Player);
}
}
///
/// Occurs when ExoPlayer changes the player state.
///
/// Indicates whether the player should start playing the media whenever the media is ready.
/// The state that the player has transitioned to.
///
/// This is part of the implementation.
/// While this method does not seem to have any references, it's invoked at runtime.
///
public void OnPlayerStateChanged(bool playWhenReady, int playbackState)
{
if (Player is null || MediaElement.Source is null)
{
return;
}
var newState = playbackState switch
{
PlaybackState.StateFastForwarding
or PlaybackState.StateRewinding
or PlaybackState.StateSkippingToNext
or PlaybackState.StateSkippingToPrevious
or PlaybackState.StateSkippingToQueueItem
or PlaybackState.StatePlaying => playWhenReady
? MediaElementState.Playing
: MediaElementState.Paused,
PlaybackState.StatePaused => MediaElementState.Paused,
PlaybackState.StateConnecting
or PlaybackState.StateBuffering => MediaElementState.Buffering,
PlaybackState.StateNone => MediaElementState.None,
PlaybackState.StateStopped => MediaElement.CurrentState is not MediaElementState.Failed
? MediaElementState.Stopped
: MediaElementState.Failed,
PlaybackState.StateError => MediaElementState.Failed,
_ => MediaElementState.None,
};
MediaElement.CurrentStateChanged(newState);
if (playbackState is readyState)
{
MediaElement.Duration = TimeSpan.FromMilliseconds(Player.Duration < 0 ? 0 : Player.Duration);
MediaElement.Position = TimeSpan.FromMilliseconds(Player.CurrentPosition < 0 ? 0 : Player.CurrentPosition);
}
}
///
/// Creates the corresponding platform view of on Android.
///
/// The platform native counterpart of .
/// Thrown when is or when the platform view could not be created.
[MemberNotNull(nameof(Player), nameof(PlayerView), nameof(session))]
public (PlatformMediaElement platformView, PlayerView PlayerView) CreatePlatformView(AndroidViewType androidViewType, bool isAndroidServiceEnabled)
{
Player = new ExoPlayerBuilder(MauiContext.Context).Build() ?? throw new InvalidOperationException("Player cannot be null");
Player.AddListener(this);
this.isAndroidForegroundServiceEnabled = isAndroidServiceEnabled;
if (androidViewType is AndroidViewType.SurfaceView)
{
PlayerView = new PlayerView(MauiContext.Context)
{
Player = Player,
UseController = false,
ControllerAutoShow = false,
LayoutParameters = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MatchParent, ViewGroup.LayoutParams.MatchParent)
};
}
else if (androidViewType is AndroidViewType.TextureView)
{
if (MauiContext.Context?.Resources is null)
{
throw new InvalidOperationException("Unable to retrieve Android Resources");
}
var resources = MauiContext.Context.Resources;
var xmlResource = resources.GetXml(Microsoft.Maui.Resource.Layout.textureview);
xmlResource.Read();
var attributes = Xml.AsAttributeSet(xmlResource)!;
PlayerView = new PlayerView(MauiContext.Context, attributes)
{
Player = Player,
UseController = false,
ControllerAutoShow = false,
LayoutParameters = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MatchParent, ViewGroup.LayoutParams.MatchParent)
};
}
else
{
throw new NotSupportedException($"{androidViewType} is not yet supported");
}
var mediaSession = new MediaSession.Builder(Platform.AppContext, Player);
mediaSession.SetId(Convert.ToBase64String(Guid.NewGuid().ToByteArray())[..8]);
session ??= mediaSession.Build() ?? throw new InvalidOperationException("Session cannot be null");
ArgumentNullException.ThrowIfNull(session.Id);
return (Player, PlayerView);
}
///
/// Occurs when ExoPlayer changes the playback state.
///
/// The state that the player has transitioned to.
///
/// This is part of the implementation.
/// While this method does not seem to have any references, it's invoked at runtime.
///
public void OnPlaybackStateChanged(int playbackState)
{
if (MediaElement.Source is null)
{
return;
}
MediaElementState newState = MediaElement.CurrentState;
switch (playbackState)
{
case bufferState:
newState = MediaElementState.Buffering;
break;
case endedState:
newState = MediaElementState.Stopped;
MediaElement.MediaEnded();
break;
case readyState:
seekToTaskCompletionSource?.TrySetResult();
break;
}
MediaElement.CurrentStateChanged(newState);
}
///
/// Occurs when ExoPlayer encounters an error.
///
/// An instance of containing details of the error.
///
/// This is part of the implementation.
/// While this method does not seem to have any references, it's invoked at runtime.
///
public void OnPlayerError(PlaybackException? error)
{
var errorMessage = string.Empty;
var errorCode = string.Empty;
var errorCodeName = string.Empty;
if (!string.IsNullOrWhiteSpace(error?.LocalizedMessage))
{
errorMessage = $"Error message: {error.LocalizedMessage}";
}
if (error?.ErrorCode is not null)
{
errorCode = $"Error code: {error.ErrorCode}";
}
if (!string.IsNullOrWhiteSpace(error?.ErrorCodeName))
{
errorCodeName = $"Error codename: {error.ErrorCodeName}";
}
var message = string.Join(", ", new[]
{
errorCodeName,
errorCode,
errorMessage
}.Where(static s => !string.IsNullOrEmpty(s)));
MediaElement.MediaFailed(new MediaFailedEventArgs(message));
Logger.LogError("{LogMessage}", message);
}
public void OnVideoSizeChanged(VideoSize? videoSize)
{
MediaElement.MediaWidth = videoSize?.Width ?? 0;
MediaElement.MediaHeight = videoSize?.Height ?? 0;
}
///
/// Occurs when ExoPlayer changes volume.
///
/// The new value for volume.
///
/// This is part of the implementation.
/// While this method does not seem to have any references, it's invoked at runtime.
///
public void OnVolumeChanged(float volume)
{
if (Player is null)
{
return;
}
// When currently muted, ignore
if (MediaElement.ShouldMute)
{
return;
}
MediaElement.Volume = volume;
}
protected virtual partial void PlatformPlay()
{
if (Player is null || MediaElement.Source is null)
{
return;
}
Player.Prepare();
Player.Play();
}
protected virtual partial void PlatformPause()
{
if (Player is null || MediaElement.Source is null)
{
return;
}
Player.Pause();
}
[MemberNotNull(nameof(Player))]
protected virtual async partial Task PlatformSeek(TimeSpan position, CancellationToken token)
{
if (Player is null)
{
throw new InvalidOperationException($"{nameof(IExoPlayer)} is not yet initialized");
}
await seekToSemaphoreSlim.WaitAsync(token);
seekToTaskCompletionSource = new();
try
{
Player.SeekTo((long)position.TotalMilliseconds);
// Here, we don't want to throw an exception
// and to keep the execution on the thread that called this method
await seekToTaskCompletionSource.Task.WaitAsync(TimeSpan.FromMinutes(2), token).ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing | ConfigureAwaitOptions.ContinueOnCapturedContext);
MediaElement.SeekCompleted();
}
finally
{
seekToSemaphoreSlim.Release();
}
}
protected virtual partial void PlatformStop()
{
if (Player is null || MediaElement.Source is null)
{
return;
}
Player.SeekTo(0);
Player.Stop();
MediaElement.Position = TimeSpan.Zero;
}
protected virtual async partial ValueTask PlatformUpdateSource()
{
var hasSetSource = false;
if (Player is null)
{
return;
}
if (connection is null && isAndroidForegroundServiceEnabled)
{
StartService();
}
if (MediaElement.Source is null)
{
Player.ClearMediaItems();
MediaElement.Duration = TimeSpan.Zero;
MediaElement.CurrentStateChanged(MediaElementState.None);
currentStreamDataSourceFactory?.Dispose();
currentStreamDataSourceFactory = null;
return;
}
// Clear previous stream data source if switching sources
if (MediaElement.Source is not StreamMediaSource)
{
currentStreamDataSourceFactory?.Dispose();
currentStreamDataSourceFactory = null;
}
MediaElement.CurrentStateChanged(MediaElementState.Opening);
Player.PlayWhenReady = MediaElement.ShouldAutoPlay;
cancellationTokenSource ??= new();
// ConfigureAwait(true) is required to prevent crash on startup
var result = await SetPlayerData(cancellationTokenSource.Token).ConfigureAwait(true);
var item = result?.Build();
if (item?.MediaMetadata is not null)
{
// If we have a custom stream data source, we need to set the media source differently
if (currentStreamDataSourceFactory is not null && MediaElement.Source is StreamMediaSource)
{
var mediaSource = new AndroidX.Media3.ExoPlayer.Source.ProgressiveMediaSource.Factory(currentStreamDataSourceFactory)
.CreateMediaSource(item);
Player.SetMediaSource(mediaSource);
}
else if (MediaElement.Source is UriMediaSource uriMediaSource && uriMediaSource.HttpHeaders.Count > 0)
{
var httpDataSourceFactory = new DefaultHttpDataSource.Factory();
httpDataSourceFactory.SetDefaultRequestProperties(uriMediaSource.HttpHeaders);
var mediaSourceFactory = new DefaultMediaSourceFactory(httpDataSourceFactory);
var mediaSource = mediaSourceFactory.CreateMediaSource(item);
Player.SetMediaSource(mediaSource);
}
else
{
Player.SetMediaItem(item);
}
Player.Prepare();
hasSetSource = true;
}
if (hasSetSource)
{
if (Player.PlayerError is null)
{
MediaElement.MediaOpened();
}
if (isAndroidForegroundServiceEnabled)
{
UpdateNotifications();
}
}
}
protected virtual partial void PlatformUpdateAspect()
{
if (PlayerView is null)
{
return;
}
PlayerView.ResizeMode = MediaElement.Aspect switch
{
Aspect.AspectFill => AspectRatioFrameLayout.ResizeModeZoom,
Aspect.Fill => AspectRatioFrameLayout.ResizeModeFill,
Aspect.Center or Aspect.AspectFit => AspectRatioFrameLayout.ResizeModeFit,
_ => throw new NotSupportedException($"{nameof(Aspect)}: {MediaElement.Aspect} is not yet supported")
};
}
protected virtual partial void PlatformUpdateSpeed()
{
if (Player is null)
{
return;
}
// First time we're getting a playback speed, set initial value
previousSpeed ??= MediaElement.Speed;
if (MediaElement.Speed > 0)
{
Player.SetPlaybackSpeed((float)MediaElement.Speed);
if (previousSpeed is 0)
{
Player.Play();
}
previousSpeed = MediaElement.Speed;
}
else
{
previousSpeed = 0;
Player.Pause();
}
}
protected virtual partial void PlatformUpdateShouldShowPlaybackControls()
{
if (PlayerView is null)
{
return;
}
PlayerView.UseController = MediaElement.ShouldShowPlaybackControls;
}
protected virtual partial void PlatformUpdatePosition()
{
if (Player is null)
{
return;
}
if (MediaElement.Duration != TimeSpan.Zero)
{
MediaElement.Position = TimeSpan.FromMilliseconds(Player.CurrentPosition);
}
}
protected virtual partial void PlatformUpdateVolume()
{
if (Player is null)
{
return;
}
// If the user changes while muted, change the internal field
// and do not update the actual volume.
if (MediaElement.ShouldMute)
{
volumeBeforeMute = (float)MediaElement.Volume;
return;
}
Player.Volume = (float)MediaElement.Volume;
}
protected virtual partial void PlatformUpdateShouldKeepScreenOn()
{
if (PlayerView is null)
{
return;
}
PlayerView.KeepScreenOn = MediaElement.ShouldKeepScreenOn;
}
protected virtual partial void PlatformUpdateShouldMute()
{
if (Player is null)
{
return;
}
// We're going to mute state. Capture the current volume first so we can restore later.
if (MediaElement.ShouldMute)
{
volumeBeforeMute = Player.Volume;
}
else if (!AreFloatingPointNumbersEqual(volumeBeforeMute, Player.Volume) && Player.Volume > 0)
{
volumeBeforeMute = Player.Volume;
}
Player.Volume = MediaElement.ShouldMute ? 0 : volumeBeforeMute;
}
protected virtual partial void PlatformUpdateShouldLoopPlayback()
{
if (Player is null)
{
return;
}
Player.RepeatMode = MediaElement.ShouldLoopPlayback ? RepeatModeUtil.RepeatToggleModeOne : RepeatModeUtil.RepeatToggleModeNone;
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (disposing)
{
session?.Release();
session?.Dispose();
session = null;
cancellationTokenSource?.Dispose();
cancellationTokenSource = null;
if (connection is not null)
{
StopService(connection);
connection.Dispose();
connection = null;
}
currentStreamDataSourceFactory?.Dispose();
currentStreamDataSourceFactory = null;
client.Dispose();
}
}
async Task GetBytesFromMetadataArtworkUrl(string url, CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(url))
{
return [];
}
Stream? stream = null;
Uri.TryCreate(url, UriKind.Absolute, out var uri);
try
{
byte[] artworkData = [];
long? contentLength = null;
// HTTP or HTTPS URL
if (uri is not null &&
(uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps))
{
var request = new HttpRequestMessage(HttpMethod.Head, url);
var contentLengthResponse = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
contentLength = contentLengthResponse.Content.Headers.ContentLength ?? 0;
var response = await client.GetAsync(url, HttpCompletionOption.ResponseContentRead, cancellationToken).ConfigureAwait(false);
stream = response.IsSuccessStatusCode ? await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false) : null;
}
// Absolute File Path
else if (uri is not null && uri.Scheme == Uri.UriSchemeFile)
{
var normalizedFilePath = NormalizeFilePath(url);
stream = File.OpenRead(normalizedFilePath);
contentLength = await GetByteCountFromStream(stream, cancellationToken);
}
// Relative File Path
else if (Uri.TryCreate(url, UriKind.Relative, out _))
{
var normalizedFilePath = NormalizeFilePath(url);
stream = Platform.AppContext.Assets?.Open(normalizedFilePath) ?? throw new InvalidOperationException("Assets cannot be null");
contentLength = await GetByteCountFromStream(stream, cancellationToken);
}
if (stream is not null)
{
if (!contentLength.HasValue)
{
throw new InvalidOperationException($"{nameof(contentLength)} must be set when {nameof(stream)} is not null");
}
artworkData = new byte[contentLength.Value];
using var memoryStream = new MemoryStream(artworkData);
await stream.CopyToAsync(memoryStream, cancellationToken).ConfigureAwait(false);
}
return artworkData;
}
catch (Exception e)
{
Trace.WriteLine($"Unable to retrieve {nameof(MediaElement.MetadataArtworkUrl)} for {url}.{e}\n");
return [];
}
finally
{
if (stream is not null)
{
stream.Close();
await stream.DisposeAsync();
}
}
static string NormalizeFilePath(string filePath) => filePath.Replace('\\', Path.DirectorySeparatorChar).Replace('/', Path.DirectorySeparatorChar);
static async ValueTask GetByteCountFromStream(Stream stream, CancellationToken token)
{
if (stream.CanSeek)
{
return stream.Length;
}
long countedStreamBytes = 0;
var buffer = new byte[8192];
int bytesRead;
while ((bytesRead = await stream.ReadAsync(buffer, token)) > 0)
{
countedStreamBytes += bytesRead;
}
return countedStreamBytes;
}
}
void StartService()
{
if (!isAndroidForegroundServiceEnabled)
{
return;
}
var intent = new Intent(global::Android.App.Application.Context, typeof(MediaControlsService));
connection = new BoundServiceConnection(this);
connection.MediaControlsServiceTaskRemoved += HandleMediaControlsServiceTaskRemoved;
global::Android.App.Application.Context.StartForegroundService(intent);
global::Android.App.Application.Context.ApplicationContext?.BindService(intent, connection, Bind.AutoCreate);
}
void StopService(in BoundServiceConnection boundServiceConnection)
{
if (!isAndroidForegroundServiceEnabled)
{
return;
}
boundServiceConnection.MediaControlsServiceTaskRemoved -= HandleMediaControlsServiceTaskRemoved;
var serviceIntent = new Intent(Platform.AppContext, typeof(MediaControlsService));
global::Android.App.Application.Context.StopService(serviceIntent);
Platform.AppContext.UnbindService(boundServiceConnection);
}
void HandleMediaControlsServiceTaskRemoved(object? sender, EventArgs e) => Player?.Stop();
async Task SetPlayerData(CancellationToken cancellationToken = default)
{
if (MediaElement.Source is null)
{
return null;
}
switch (MediaElement.Source)
{
case UriMediaSource uriMediaSource:
{
var uri = uriMediaSource.Uri;
if (!string.IsNullOrWhiteSpace(uri?.AbsoluteUri))
{
return await CreateMediaItem(uri.AbsoluteUri, cancellationToken).ConfigureAwait(false);
}
break;
}
case FileMediaSource fileMediaSource:
{
var filePath = fileMediaSource.Path;
if (!string.IsNullOrWhiteSpace(filePath))
{
return await CreateMediaItem(filePath, cancellationToken).ConfigureAwait(false);
}
break;
}
case ResourceMediaSource resourceMediaSource:
{
var package = PlayerView?.Context?.PackageName ?? "";
var path = resourceMediaSource.Path;
if (!string.IsNullOrWhiteSpace(path))
{
var assetFilePath = $"asset://{package}{Path.PathSeparator}{path}";
return await CreateMediaItem(assetFilePath, cancellationToken).ConfigureAwait(false);
}
break;
}
case StreamMediaSource streamMediaSource:
{
if (streamMediaSource.Stream is not null)
{
return await CreateMediaItemFromStream(streamMediaSource.Stream, cancellationToken).ConfigureAwait(false);
}
break;
}
default:
throw new NotSupportedException($"{MediaElement.Source.GetType().FullName} is not yet supported for {nameof(MediaElement.Source)}");
}
return mediaItem;
}
async Task CreateMediaItem(string url, CancellationToken cancellationToken = default)
{
MediaMetadata.Builder mediaMetaData = new();
mediaMetaData.SetArtist(MediaElement.MetadataArtist);
mediaMetaData.SetTitle(MediaElement.MetadataTitle);
var data = await GetBytesFromMetadataArtworkUrl(MediaElement.MetadataArtworkUrl, cancellationToken).ConfigureAwait(true);
if (data is not null && data.Length > 0)
{
mediaMetaData.SetArtworkData(data, (Java.Lang.Integer)MediaMetadata.PictureTypeFrontCover);
}
mediaItem = new MediaItem.Builder();
mediaItem.SetUri(url);
mediaItem.SetMediaId(url);
mediaItem.SetMediaMetadata(mediaMetaData.Build());
return mediaItem;
}
async Task CreateMediaItemFromStream(Stream stream, CancellationToken cancellationToken)
{
MediaMetadata.Builder mediaMetaData = new();
mediaMetaData.SetArtist(MediaElement.MetadataArtist);
mediaMetaData.SetTitle(MediaElement.MetadataTitle);
var data = await GetBytesFromMetadataArtworkUrl(MediaElement.MetadataArtworkUrl, cancellationToken).ConfigureAwait(true);
if (data is not null && data.Length > 0)
{
mediaMetaData.SetArtworkData(data, (Java.Lang.Integer)MediaMetadata.PictureTypeFrontCover);
}
// Create MediaItem with metadata
// The stream will be handled via custom data source factory when needed
mediaItem = new MediaItem.Builder();
mediaItem.SetUri("stream://media");
mediaItem.SetMediaId("stream://media");
mediaItem.SetMediaMetadata(mediaMetaData.Build());
currentStreamDataSourceFactory?.Dispose();
currentStreamDataSourceFactory = null;
// Store the stream for later use with custom MediaSource
currentStreamDataSourceFactory = new StreamDataSourceFactory(stream);
return mediaItem;
}
#region PlayerListener implementation method stubs
public void OnAudioAttributesChanged(AudioAttributes? audioAttributes)
{
}
public void OnAudioSessionIdChanged(int audioSessionId)
{
}
public void OnAvailableCommandsChanged(PlayerCommands? player)
{
}
public void OnCues(CueGroup? cues)
{
}
public void OnDeviceInfoChanged(DeviceInfo? deviceInfo)
{
}
public void OnDeviceVolumeChanged(int volume, bool muted)
{
}
public void OnEvents(IPlayer? player, PlayerEvents? playerEvents)
{
}
public void OnIsLoadingChanged(bool isLoading)
{
}
public void OnIsPlayingChanged(bool isPlaying)
{
}
public void OnLoadingChanged(bool isLoading)
{
}
public void OnMaxSeekToPreviousPositionChanged(long maxSeekToPreviousPositionMs)
{
}
public void OnMediaItemTransition(MediaItem? mediaItem, int reason)
{
}
public void OnMediaMetadataChanged(MediaMetadata? mediaMetadata)
{
}
public void OnMetadata(Metadata? metadata)
{
}
public void OnPlayWhenReadyChanged(bool playWhenReady, int reason)
{
}
public void OnPositionDiscontinuity(PlayerPositionInfo? oldPosition, PlayerPositionInfo? newPosition, int reason)
{
}
public void OnPlaybackSuppressionReasonChanged(int playbackSuppressionReason)
{
}
public void OnPlayerErrorChanged(PlaybackException? error)
{
}
public void OnPlaylistMetadataChanged(MediaMetadata? mediaMetadata)
{
}
public void OnRenderedFirstFrame()
{
}
public void OnRepeatModeChanged(int repeatMode)
{
}
public void OnSeekBackIncrementChanged(long seekBackIncrementMs)
{
}
public void OnSeekForwardIncrementChanged(long seekForwardIncrementMs)
{
}
public void OnShuffleModeEnabledChanged(bool shuffleModeEnabled)
{
}
public void OnSkipSilenceEnabledChanged(bool skipSilenceEnabled)
{
}
public void OnSurfaceSizeChanged(int width, int height)
{
}
public void OnTimelineChanged(Timeline? timeline, int reason)
{
}
public void OnTrackSelectionParametersChanged(TrackSelectionParameters? trackSelectionParameters)
{
}
public void OnTracksChanged(Tracks? tracks)
{
}
#endregion
static class PlaybackState
{
public const int StateBuffering = 6;
public const int StateConnecting = 8;
public const int StateFailed = 7;
public const int StateFastForwarding = 4;
public const int StateNone = 0;
public const int StatePaused = 2;
public const int StatePlaying = 3;
public const int StateRewinding = 5;
public const int StateSkippingToNext = 10;
public const int StateSkippingToPrevious = 9;
public const int StateSkippingToQueueItem = 11;
public const int StateStopped = 1;
public const int StateError = 7;
}
}